1use std::fs;
7use std::path::{Path, PathBuf};
8use std::time::Instant;
9
10use anyhow::Result;
11use tempfile::TempDir;
12
13use crate::db;
14use crate::indexer::{self, IndexOptions};
15use crate::query;
16use crate::types::{BenchQuery, BenchResult, BenchSavings};
17
18pub struct BenchOptions {
19 pub path: Option<PathBuf>,
20 pub probe_symbol: Option<String>,
21 pub scale: Option<usize>,
23}
24
25pub fn run(options: BenchOptions) -> Result<BenchResult> {
26 let (path, _synthetic_dir) = match options.path {
27 Some(p) => (p, None),
28 None => {
29 let dir = TempDir::new()?;
30 generate_synthetic_repo(dir.path(), options.scale.unwrap_or(50))?;
31 let path = dir.path().to_path_buf();
32 (path, Some(dir))
33 }
34 };
35
36 let db_dir = tempfile::tempdir()?;
37 let db_path = db_dir.path().join("bench.db");
38
39 let full = indexer::index_path_with(
41 &path,
42 &db_path,
43 IndexOptions {
44 full: true,
45 build_snapshot: true,
46 },
47 )?;
48
49 let incremental = indexer::index_path_with(
51 &path,
52 &db_path,
53 IndexOptions {
54 full: false,
55 build_snapshot: false,
56 },
57 )?;
58
59 let conn = db::open(&db_path)?;
60 let probe = options
63 .probe_symbol
64 .unwrap_or_else(|| pick_called_symbol(&conn).unwrap_or_else(|_| "main".to_string()));
65
66 let queries = vec![
67 bench_query(&conn, "find_definition", || {
68 let r = query::find_definition_conn(&conn, &probe)?;
69 Ok(estimate(&r))
70 })?,
71 bench_query(&conn, "find_references", || {
72 let r = query::find_references_conn(&conn, &probe)?;
73 Ok(estimate(&r))
74 })?,
75 bench_query(&conn, "get_outline", || {
76 let r = query::get_outline_conn(&conn, Path::new("."))?;
77 Ok(estimate(&r))
78 })?,
79 bench_query(&conn, "impact", || {
80 let r = query::impact_conn(&conn, &probe, 4)?;
81 Ok(estimate(&r))
82 })?,
83 bench_query(&conn, "validate", || {
84 let r = query::validate_conn(&conn, &probe)?;
85 Ok(estimate(&r))
86 })?,
87 ];
88
89 let raw_callers_tokens = estimate_raw_grep_tokens(&path, &probe)?;
93 let impact_tokens = queries
94 .iter()
95 .find(|q| q.name == "impact")
96 .map(|q| q.tokens)
97 .unwrap_or(1);
98
99 let raw_definition_tokens = estimate_mean_file_tokens(&path)?;
102 let find_def_tokens = queries
103 .iter()
104 .find(|q| q.name == "find_definition")
105 .map(|q| q.tokens)
106 .unwrap_or(1);
107
108 let savings = vec![
109 BenchSavings {
110 label: format!("\"who calls {}?\"", probe),
111 raw_tokens: raw_callers_tokens,
112 tessera_tokens: impact_tokens,
113 ratio: ratio(raw_callers_tokens, impact_tokens),
114 },
115 BenchSavings {
116 label: format!("\"where is {} defined?\"", probe),
117 raw_tokens: raw_definition_tokens,
118 tessera_tokens: find_def_tokens,
119 ratio: ratio(raw_definition_tokens, find_def_tokens),
120 },
121 ];
122
123 let chart = render_chart(
124 &path,
125 full.files_indexed + full.files_reused,
126 full.symbols_indexed,
127 full.references_indexed,
128 full.elapsed_ms,
129 incremental.elapsed_ms,
130 &savings,
131 &queries,
132 );
133
134 Ok(BenchResult {
135 path: path.to_string_lossy().to_string(),
136 files: full.files_indexed + full.files_reused,
137 symbols: full.symbols_indexed,
138 references: full.references_indexed,
139 index_full_ms: full.elapsed_ms,
140 index_incremental_ms: incremental.elapsed_ms,
141 queries,
142 savings,
143 chart,
144 })
145}
146
147fn bench_query<F>(_conn: &rusqlite::Connection, name: &str, mut run_one: F) -> Result<BenchQuery>
148where
149 F: FnMut() -> Result<usize>,
150{
151 let iterations: u128 = 3;
152 let started = Instant::now();
153 let mut tokens = 0;
154 for _ in 0..iterations {
155 tokens = run_one()?;
156 }
157 let elapsed_ms = started.elapsed().as_millis() / iterations;
158 Ok(BenchQuery {
159 name: name.to_string(),
160 ms: elapsed_ms,
161 tokens,
162 })
163}
164
165fn estimate<T: serde::Serialize>(value: &T) -> usize {
166 serde_json::to_vec(value)
167 .map(|bytes| (bytes.len() / 4).max(1))
168 .unwrap_or(1)
169}
170
171fn estimate_raw_grep_tokens(root: &Path, symbol: &str) -> Result<usize> {
172 let mut total = 0usize;
176 for entry in walkdir::WalkDir::new(root)
177 .into_iter()
178 .filter_map(Result::ok)
179 .filter(|e| e.file_type().is_file())
180 {
181 let Some(ext) = entry.path().extension().and_then(|e| e.to_str()) else {
182 continue;
183 };
184 if crate::types::Language::from_extension(ext).is_none() {
185 continue;
186 }
187 let Ok(content) = fs::read_to_string(entry.path()) else {
188 continue;
189 };
190 if content.contains(symbol) {
191 total += content.len() / 4;
192 }
193 }
194 Ok(total.max(1))
195}
196
197fn estimate_mean_file_tokens(root: &Path) -> Result<usize> {
198 let mut total = 0usize;
201 let mut count = 0usize;
202 for entry in walkdir::WalkDir::new(root)
203 .into_iter()
204 .filter_map(Result::ok)
205 .filter(|e| e.file_type().is_file())
206 {
207 let Some(ext) = entry.path().extension().and_then(|e| e.to_str()) else {
208 continue;
209 };
210 if crate::types::Language::from_extension(ext).is_none() {
211 continue;
212 }
213 if let Ok(meta) = entry.metadata() {
214 total += (meta.len() as usize) / 4;
215 count += 1;
216 }
217 }
218 Ok(total.checked_div(count).unwrap_or(1))
219}
220
221fn ratio(raw: usize, tessera: usize) -> f32 {
222 if tessera == 0 {
223 return f32::INFINITY;
224 }
225 raw as f32 / tessera as f32
226}
227
228fn pick_called_symbol(conn: &rusqlite::Connection) -> Result<String> {
229 if let Ok(name) = conn.query_row::<String, _, _>(
232 "
233 SELECT s.name
234 FROM symbols s
235 JOIN edges e ON e.to_symbol_name = s.name OR e.to_symbol_name = s.qualified_name
236 WHERE s.kind IN ('function', 'method')
237 GROUP BY s.name
238 ORDER BY COUNT(*) DESC, s.name
239 LIMIT 1
240 ",
241 [],
242 |row| row.get(0),
243 ) {
244 return Ok(name);
245 }
246 let name: String = conn.query_row(
247 "
248 SELECT name FROM symbols
249 WHERE kind IN ('function', 'method')
250 GROUP BY name
251 ORDER BY COUNT(*) DESC
252 LIMIT 1
253 ",
254 [],
255 |row| row.get(0),
256 )?;
257 Ok(name)
258}
259
260#[allow(clippy::too_many_arguments)]
261fn render_chart(
262 path: &Path,
263 files: usize,
264 symbols: usize,
265 refs: usize,
266 full_ms: u128,
267 inc_ms: u128,
268 savings: &[BenchSavings],
269 queries: &[BenchQuery],
270) -> String {
271 const BAR_W: usize = 32;
272 let mut out = String::new();
273
274 out.push_str(&format!("Tessera v{} bench\n", env!("CARGO_PKG_VERSION")));
275 out.push_str("─────────────────────\n");
276 out.push_str(&format!("Repo: {}\n", short_path(path)));
277 out.push_str(&format!(
278 " {} files · {} symbols · {} references\n\n",
279 thousands(files),
280 thousands(symbols),
281 thousands(refs)
282 ));
283
284 out.push_str("Index time\n");
285 let max_ms = full_ms.max(inc_ms).max(1) as f32;
286 out.push_str(&format!(
287 " full {} {:>6} ms\n",
288 bar(full_ms as f32 / max_ms, BAR_W),
289 full_ms
290 ));
291 out.push_str(&format!(
292 " incremental {} {:>6} ms",
293 bar(inc_ms as f32 / max_ms, BAR_W),
294 inc_ms
295 ));
296 if inc_ms > 0 && full_ms > inc_ms {
297 out.push_str(&format!(
298 " · {:.0}× faster",
299 full_ms as f32 / inc_ms.max(1) as f32
300 ));
301 }
302 out.push_str("\n\n");
303
304 for s in savings {
305 out.push_str(&format!("{}\n", s.label));
306 let max_tokens = s.raw_tokens.max(s.tessera_tokens).max(1) as f32;
307 out.push_str(&format!(
308 " raw grep + read {} {:>8} tokens\n",
309 bar(s.raw_tokens as f32 / max_tokens, BAR_W),
310 thousands(s.raw_tokens)
311 ));
312 out.push_str(&format!(
313 " tessera {} {:>8} tokens · {:.0}× cheaper\n\n",
314 bar(s.tessera_tokens as f32 / max_tokens, BAR_W),
315 thousands(s.tessera_tokens),
316 s.ratio
317 ));
318 }
319
320 if !queries.is_empty() {
321 out.push_str("Per-query latency · median of 3 runs\n");
322 for q in queries {
323 out.push_str(&format!(
324 " {:<18} {:>3} ms ~{:>5} tokens\n",
325 q.name,
326 q.ms,
327 thousands(q.tokens)
328 ));
329 }
330 }
331
332 out
333}
334
335fn bar(fraction: f32, width: usize) -> String {
336 let fraction = fraction.clamp(0.0, 1.0);
337 let filled =
338 ((fraction * width as f32).round() as usize).max(if fraction > 0.0 { 1 } else { 0 });
339 let mut s = String::with_capacity(width);
340 for _ in 0..filled {
341 s.push('█');
342 }
343 for _ in filled..width {
344 s.push(' ');
345 }
346 s
347}
348
349fn thousands(n: usize) -> String {
350 let s = n.to_string();
351 let bytes = s.as_bytes();
352 let mut out = String::with_capacity(s.len() + s.len() / 3);
353 let len = bytes.len();
354 for (i, b) in bytes.iter().enumerate() {
355 if i > 0 && (len - i) % 3 == 0 {
356 out.push(',');
357 }
358 out.push(*b as char);
359 }
360 out
361}
362
363fn short_path(path: &Path) -> String {
364 let s = path.to_string_lossy().to_string();
365 if s.contains("/tmp/") || s.contains("/T/") {
367 return "synthetic repo".to_string();
368 }
369 s
370}
371
372fn generate_synthetic_repo(root: &Path, file_count: usize) -> Result<()> {
373 fs::create_dir_all(root)?;
379 fs::write(root.join("util.ts"), synthetic_util())?;
380 for i in 0..file_count {
381 let path = root.join(format!("module_{i:03}.ts"));
382 let content = synthetic_module(i, file_count);
383 fs::write(path, content)?;
384 }
385 Ok(())
386}
387
388fn synthetic_util() -> String {
389 "// Shared utility module — imported across the synthetic repo.
390// In real codebases, files like this are the heaviest blast-radius targets
391// for any refactor, which is why Tessera ranks their callers by PageRank.
392
393export function sharedHelper(x: number): number {
394 // Inline computation; mirrors the kind of common helper found in real
395 // codebases that ends up called from dozens or hundreds of places.
396 return (x * 13 + 7) ^ 0;
397}
398
399export function formatRow(row: { id: number; name: string }): string {
400 return `${row.id}\\t${row.name}`;
401}
402
403export function parseInput(raw: string): number {
404 return Number.parseInt(raw, 10);
405}
406"
407 .to_string()
408}
409
410fn synthetic_module(index: usize, total: usize) -> String {
411 let prev = (index + total - 1) % total;
412 let next = (index + 1) % total;
413 format!(
414 "// Module {index} of {total} — autogenerated for `tessera bench`.
415// Each module imports the shared utility module and forwards through a
416// couple of bridge functions, modelling the call topology of real services
417// where a small set of helpers is called from many places.
418
419import {{ sharedHelper, formatRow, parseInput }} from \"./util\";
420
421export interface Module{index}Input {{
422 raw: string;
423 label: string;
424}}
425
426export function moduleEntry{index}(input: Module{index}Input): string {{
427 const parsed = parseInput(input.raw);
428 const value = helper{index}(parsed);
429 return formatRow({{ id: value, name: input.label }});
430}}
431
432function helper{index}(x: number): number {{
433 // Cross-cuts: every helper depends on the shared utility plus its two
434 // neighbours, so the call graph has both a popular target and a chain.
435 return sharedHelper(x) + bridge{prev}(x) + bridge{next}(x);
436}}
437
438export function bridge{index}(x: number): number {{
439 return sharedHelper(x + {index});
440}}
441"
442 )
443}