use std::{
collections::BTreeSet,
env,
path::Path,
process::Command,
time::{Duration, Instant},
};
use weavatrix_git::Repository;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = env::args().skip(1).collect::<Vec<_>>();
let path = args.first().map_or(".", String::as_str);
let iterations = args
.get(1)
.map(|value| value.parse())
.transpose()?
.unwrap_or(50);
let repository = Repository::open(path)?;
let head = repository.resolve("HEAD")?;
let expected_objects = git_object_ids(path)?;
let actual_objects = repository
.bitmap_reachable(head)?
.ok_or("repository has no reachability bitmap")?
.into_iter()
.map(|id| id.to_string())
.collect::<BTreeSet<_>>();
if actual_objects != expected_objects {
return Err("bitmap reachability differs from git rev-list --objects".into());
}
let expected_paths = git_paths(path)?;
let actual_paths = repository
.index()?
.entries()
.iter()
.map(|entry| entry.path.clone())
.collect::<Vec<_>>();
if actual_paths != expected_paths {
return Err("index paths differ from git ls-files".into());
}
let expected_status = git_status(path)?;
let actual_status = repository.status()?;
if !expected_status.is_empty() || !actual_status.is_empty() {
return Err(format!(
"status benchmark requires a clean tracked worktree (git={} bytes, weavatrix={actual_status:?})",
expected_status.len()
)
.into());
}
println!("operation,engine,p50_ms,p95_ms,items");
row(
"reachability",
"weavatrix-git",
&measure(iterations, || {
repository
.bitmap_reachable(head)
.map(|value| value.map_or(0, |ids| ids.len()))
})?,
actual_objects.len(),
);
row(
"reachability",
"git.exe",
&measure(iterations, || git_object_ids(path).map(|ids| ids.len()))?,
expected_objects.len(),
);
row(
"index",
"weavatrix-git",
&measure(iterations, || {
repository.index().map(|index| index.entries().len())
})?,
actual_paths.len(),
);
row(
"index",
"git.exe",
&measure(iterations, || git_paths(path).map(|paths| paths.len()))?,
expected_paths.len(),
);
row(
"tracked-status",
"weavatrix-git",
&measure(iterations, || {
repository.status().map(|entries| entries.len())
})?,
0,
);
row(
"tracked-status",
"git.exe",
&measure(iterations, || git_status(path).map(|entries| entries.len()))?,
0,
);
row(
"cached-commit",
"weavatrix-git",
&measure(iterations, || repository.commit(head).map(|_| 1))?,
1,
);
row(
"cached-commit",
"git.exe",
&measure(iterations, || git_exists(path, &head.to_string()))?,
1,
);
Ok(())
}
fn git_object_ids(path: impl AsRef<Path>) -> Result<BTreeSet<String>, Box<dyn std::error::Error>> {
Ok(git(path, &["rev-list", "--objects", "HEAD"])?
.lines()
.filter_map(|line| line.split_ascii_whitespace().next())
.map(str::to_owned)
.collect())
}
fn git_paths(path: impl AsRef<Path>) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> {
Ok(git_bytes(path, &["ls-files", "-z"])?
.split(|byte| *byte == 0)
.filter(|path| !path.is_empty())
.map(<[u8]>::to_vec)
.collect())
}
fn git_status(path: impl AsRef<Path>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
git_bytes(path, &["status", "--porcelain=v1", "-uno"])
}
fn git_exists(path: impl AsRef<Path>, id: &str) -> Result<usize, Box<dyn std::error::Error>> {
let _ = git_bytes(path, &["cat-file", "-e", id])?;
Ok(1)
}
fn git(path: impl AsRef<Path>, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
Ok(String::from_utf8(git_bytes(path, args)?)?)
}
fn git_bytes(path: impl AsRef<Path>, args: &[&str]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let output = Command::new("git")
.arg("-C")
.arg(path.as_ref())
.args(args)
.output()?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).into_owned().into());
}
Ok(output.stdout)
}
fn measure<T, E>(
iterations: usize,
mut operation: impl FnMut() -> Result<T, E>,
) -> Result<Vec<Duration>, E> {
for _ in 0..3 {
let _ = operation()?;
}
let mut values = Vec::with_capacity(iterations);
for _ in 0..iterations {
let start = Instant::now();
let _ = operation()?;
values.push(start.elapsed());
}
values.sort_unstable();
Ok(values)
}
fn row(operation: &str, engine: &str, values: &[Duration], items: usize) {
let p50 = values[values.len() / 2].as_secs_f64() * 1_000.0;
let p95 = values[(values.len() * 95 / 100).min(values.len() - 1)].as_secs_f64() * 1_000.0;
println!("{operation},{engine},{p50:.3},{p95:.3},{items}");
}