1use std::io::Write;
5use std::path::Path;
6use std::time::Instant;
7
8pub struct BenchReport {
9 pub files: usize,
10 pub loc: usize,
11 pub cold_ms: u64,
12 pub incremental_ms: u64,
13 pub incremental_p95_ms: u64,
15 pub peak_rss_kib: u64,
17 pub task_pack_ms: u64,
18 pub db_bytes: u64,
19}
20
21fn peak_rss_kib() -> u64 {
24 let mut usage = unsafe { std::mem::zeroed::<libc::rusage>() };
25 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 {
26 return 0;
27 }
28 #[cfg(target_os = "macos")]
29 {
30 usage.ru_maxrss as u64 / 1024
31 }
32 #[cfg(not(target_os = "macos"))]
33 {
34 usage.ru_maxrss as u64
35 }
36}
37
38pub fn generate_repo(dir: &Path, files: usize, lines: usize) -> (usize, usize) {
41 std::fs::create_dir_all(dir).unwrap();
42 let mut loc = 0usize;
43 for i in 0..files {
44 let name = format!("mod_{i:04}");
45 let mut body = String::new();
46 body.push_str(&format!("# module {name}\n"));
47 if i > 0 {
48 body.push_str(&format!(
49 "from mod_{:04} import helper_{:04}\n",
50 i - 1,
51 i - 1
52 ));
53 }
54 let mut line = 3;
55 let symbol_count = (lines / 10).max(2);
57 for s in 0..symbol_count {
58 let sym = format!("func_{s:03}");
59 body.push_str(&format!(
60 "def {sym}(a: int, b: int) -> int:\n \"\"\"{sym} computes a value.\"\"\"\n"
61 ));
62 line += 2;
63 let mut cur = 0;
64 while cur < 6 {
65 if i > 0 && cur % 3 == 0 {
66 body.push_str(&format!(
67 " r = helper_{:04}(a, {cur})\n",
68 (i + cur) % files
69 ));
70 } else {
71 body.push_str(&format!(" r = a + {cur} * b\n"));
72 }
73 line += 1;
74 cur += 1;
75 }
76 body.push_str(" return r\n");
77 line += 1;
78 }
79 while line < lines {
81 body.push_str("# padding comment for realistic size\n");
82 line += 1;
83 }
84 loc += line;
85 let mut f = std::fs::File::create(dir.join(format!("{name}.py"))).unwrap();
86 f.write_all(body.as_bytes()).unwrap();
87 }
88 (files, loc)
89}
90
91pub fn bench_index(root: &Path, files: usize, lines: usize) -> crate::Result<BenchReport> {
93 let dir = root.join("repo");
94 let (file_count, loc) = generate_repo(&dir, files, lines);
95
96 let t = Instant::now();
98 crate::commands::cmd_index(&dir, true)?;
99 let cold_ms = t.elapsed().as_millis() as u64;
100 let peak_rss_kib = peak_rss_kib();
101
102 let mut incremental_durations: Vec<u64> = Vec::with_capacity(7);
105 for i in 0..7usize {
106 let rel = format!("mod_{:04}.py", i % file_count);
107 let extra = format!("\n# incremental edit {i}\n");
108 std::fs::OpenOptions::new()
109 .append(true)
110 .open(dir.join(&rel))?
111 .write_all(extra.as_bytes())?;
112 let t = Instant::now();
113 crate::commands::cmd_index_paths(&dir, &[rel], true)?;
114 incremental_durations.push(t.elapsed().as_millis() as u64);
115 }
116 incremental_durations.sort_unstable();
117 let incremental_ms = incremental_durations[0];
118 let incremental_p95_ms = incremental_durations[5];
119
120 let t = Instant::now();
122 crate::commands::cmd_context_task(&dir, "find the helper computation", &[], &[], None, true, false)?;
123 let task_pack_ms = t.elapsed().as_millis() as u64;
124
125 let _store = crate::open_store(&dir)?;
126 let db_bytes = std::fs::metadata(crate::db_path(&dir))
127 .map(|m| m.len())
128 .unwrap_or(0);
129
130 Ok(BenchReport {
131 files: file_count,
132 loc,
133 cold_ms,
134 incremental_ms,
135 incremental_p95_ms,
136 peak_rss_kib,
137 task_pack_ms,
138 db_bytes,
139 })
140}
141
142pub fn print_report(r: &BenchReport) {
143 println!("scc bench index");
144 println!(" files: {}", r.files);
145 println!(" LOC: {}", r.loc);
146 println!(" cold index: {} ms", r.cold_ms);
147 println!(" incremental: {} ms", r.incremental_ms);
148 println!(" incremental p95: {} ms", r.incremental_p95_ms);
149 println!(" task pack: {} ms", r.task_pack_ms);
150 println!(" db size: {} KiB", r.db_bytes / 1024);
151 println!(" peak RSS: {} MiB", r.peak_rss_kib / 1024);
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn generator_produces_indexable_repo() {
160 let dir = tempfile::TempDir::new().unwrap();
161 let (files, loc) = generate_repo(dir.path(), 5, 40);
162 assert_eq!(files, 5);
163 assert!(loc >= 5 * 40);
164 crate::commands::cmd_index(dir.path(), true).unwrap();
165 let store = crate::open_store(dir.path()).unwrap();
166 assert!(store.stats().unwrap()["files"] >= 5);
167 }
168
169 #[test]
170 fn bench_runs_end_to_end() {
171 let dir = tempfile::TempDir::new().unwrap();
172 let r = bench_index(dir.path(), 8, 50).unwrap();
173 assert_eq!(r.files, 8);
174 assert!(r.cold_ms > 0);
175 assert!(r.db_bytes > 0);
176 assert!(r.incremental_p95_ms > 0);
178 }
179}