1use std::{
2 collections::BTreeSet,
3 env,
4 path::Path,
5 process::Command,
6 time::{Duration, Instant},
7};
8
9use weavatrix_git::Repository;
10
11fn main() -> Result<(), Box<dyn std::error::Error>> {
12 let args = env::args().skip(1).collect::<Vec<_>>();
13 let path = args.first().map_or(".", String::as_str);
14 let iterations = args
15 .get(1)
16 .map(|value| value.parse())
17 .transpose()?
18 .unwrap_or(50);
19 let repository = Repository::open(path)?;
20 let head = repository.resolve("HEAD")?;
21
22 let expected_objects = git_object_ids(path)?;
23 let actual_objects = repository
24 .bitmap_reachable(head)?
25 .ok_or("repository has no reachability bitmap")?
26 .into_iter()
27 .map(|id| id.to_string())
28 .collect::<BTreeSet<_>>();
29 if actual_objects != expected_objects {
30 return Err("bitmap reachability differs from git rev-list --objects".into());
31 }
32 let expected_paths = git_paths(path)?;
33 let actual_paths = repository
34 .index()?
35 .entries()
36 .iter()
37 .map(|entry| entry.path.clone())
38 .collect::<Vec<_>>();
39 if actual_paths != expected_paths {
40 return Err("index paths differ from git ls-files".into());
41 }
42 let expected_status = git_status(path)?;
43 let actual_status = repository.status()?;
44 if !expected_status.is_empty() || !actual_status.is_empty() {
45 return Err(format!(
46 "status benchmark requires a clean tracked worktree (git={} bytes, weavatrix={actual_status:?})",
47 expected_status.len()
48 )
49 .into());
50 }
51
52 println!("operation,engine,p50_ms,p95_ms,items");
53 row(
54 "reachability",
55 "weavatrix-git",
56 &measure(iterations, || {
57 repository
58 .bitmap_reachable(head)
59 .map(|value| value.map_or(0, |ids| ids.len()))
60 })?,
61 actual_objects.len(),
62 );
63 row(
64 "reachability",
65 "git.exe",
66 &measure(iterations, || git_object_ids(path).map(|ids| ids.len()))?,
67 expected_objects.len(),
68 );
69 row(
70 "index",
71 "weavatrix-git",
72 &measure(iterations, || {
73 repository.index().map(|index| index.entries().len())
74 })?,
75 actual_paths.len(),
76 );
77 row(
78 "index",
79 "git.exe",
80 &measure(iterations, || git_paths(path).map(|paths| paths.len()))?,
81 expected_paths.len(),
82 );
83 row(
84 "tracked-status",
85 "weavatrix-git",
86 &measure(iterations, || {
87 repository.status().map(|entries| entries.len())
88 })?,
89 0,
90 );
91 row(
92 "tracked-status",
93 "git.exe",
94 &measure(iterations, || git_status(path).map(|entries| entries.len()))?,
95 0,
96 );
97 row(
98 "cached-commit",
99 "weavatrix-git",
100 &measure(iterations, || repository.commit(head).map(|_| 1))?,
101 1,
102 );
103 row(
104 "cached-commit",
105 "git.exe",
106 &measure(iterations, || git_exists(path, &head.to_string()))?,
107 1,
108 );
109 Ok(())
110}
111
112fn git_object_ids(path: impl AsRef<Path>) -> Result<BTreeSet<String>, Box<dyn std::error::Error>> {
113 Ok(git(path, &["rev-list", "--objects", "HEAD"])?
114 .lines()
115 .filter_map(|line| line.split_ascii_whitespace().next())
116 .map(str::to_owned)
117 .collect())
118}
119
120fn git_paths(path: impl AsRef<Path>) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> {
121 Ok(git_bytes(path, &["ls-files", "-z"])?
122 .split(|byte| *byte == 0)
123 .filter(|path| !path.is_empty())
124 .map(<[u8]>::to_vec)
125 .collect())
126}
127
128fn git_status(path: impl AsRef<Path>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
129 git_bytes(path, &["status", "--porcelain=v1", "-uno"])
130}
131
132fn git_exists(path: impl AsRef<Path>, id: &str) -> Result<usize, Box<dyn std::error::Error>> {
133 let _ = git_bytes(path, &["cat-file", "-e", id])?;
134 Ok(1)
135}
136
137fn git(path: impl AsRef<Path>, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
138 Ok(String::from_utf8(git_bytes(path, args)?)?)
139}
140
141fn git_bytes(path: impl AsRef<Path>, args: &[&str]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
142 let output = Command::new("git")
143 .arg("-C")
144 .arg(path.as_ref())
145 .args(args)
146 .output()?;
147 if !output.status.success() {
148 return Err(String::from_utf8_lossy(&output.stderr).into_owned().into());
149 }
150 Ok(output.stdout)
151}
152
153fn measure<T, E>(
154 iterations: usize,
155 mut operation: impl FnMut() -> Result<T, E>,
156) -> Result<Vec<Duration>, E> {
157 for _ in 0..3 {
158 let _ = operation()?;
159 }
160 let mut values = Vec::with_capacity(iterations);
161 for _ in 0..iterations {
162 let start = Instant::now();
163 let _ = operation()?;
164 values.push(start.elapsed());
165 }
166 values.sort_unstable();
167 Ok(values)
168}
169
170fn row(operation: &str, engine: &str, values: &[Duration], items: usize) {
171 let p50 = values[values.len() / 2].as_secs_f64() * 1_000.0;
172 let p95 = values[(values.len() * 95 / 100).min(values.len() - 1)].as_secs_f64() * 1_000.0;
173 println!("{operation},{engine},{p50:.3},{p95:.3},{items}");
174}