1use std::{
2 env,
3 path::Path,
4 process::Command,
5 time::{Duration, Instant},
6};
7
8use weavatrix_git::{HistoryOptions, Repository};
9
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11 let args = env::args().skip(1).collect::<Vec<_>>();
12 let path = args.first().map_or(".", String::as_str);
13 let max_count = parse(&args, 1, 500)?;
14 let iterations = parse(&args, 2, 20)?;
15 let expected = git_ids(path, max_count)?;
16 let repository = Repository::open(path)?;
17 let start = repository.resolve("HEAD")?;
18 let options = HistoryOptions {
19 max_commits: max_count,
20 first_parent: true,
21 ..HistoryOptions::default()
22 };
23 let actual = repository
24 .history_ids(start, options)?
25 .into_iter()
26 .map(|id| id.to_string())
27 .collect::<Vec<_>>();
28 if actual != expected {
29 return Err("history identifiers differ from git rev-list".into());
30 }
31
32 let warm = measure(iterations, || {
33 repository
34 .history_ids(start, options)
35 .map(|identifiers| identifiers.len())
36 })?;
37 let reopen = measure(iterations, || {
38 let repo = Repository::open(path)?;
39 repo.history_ids(repo.resolve("HEAD")?, options)
40 .map(|identifiers| identifiers.len())
41 })?;
42 let git = measure(iterations, || git_ids(path, max_count).map(|ids| ids.len()))?;
43 println!("engine,mode,p50_ms,p95_ms,commits");
44 print_row("weavatrix-git", "warm", &warm, actual.len());
45 print_row("weavatrix-git", "reopen", &reopen, actual.len());
46 print_row("git.exe", "process", &git, expected.len());
47 Ok(())
48}
49
50fn parse(
51 args: &[String],
52 index: usize,
53 fallback: usize,
54) -> Result<usize, Box<dyn std::error::Error>> {
55 Ok(args
56 .get(index)
57 .map(|value| value.parse())
58 .transpose()?
59 .unwrap_or(fallback))
60}
61
62fn git_ids(path: impl AsRef<Path>, max: usize) -> Result<Vec<String>, Box<dyn std::error::Error>> {
63 let output = Command::new("git")
64 .arg("-C")
65 .arg(path.as_ref())
66 .args(["rev-list", "--first-parent", "--max-count"])
67 .arg(max.to_string())
68 .arg("HEAD")
69 .output()?;
70 if !output.status.success() {
71 return Err(String::from_utf8_lossy(&output.stderr).into_owned().into());
72 }
73 Ok(String::from_utf8(output.stdout)?
74 .lines()
75 .map(str::to_owned)
76 .collect())
77}
78
79fn measure<T, E>(
80 iterations: usize,
81 mut operation: impl FnMut() -> Result<T, E>,
82) -> Result<Vec<Duration>, E> {
83 for _ in 0..3 {
84 let _ = operation()?;
85 }
86 let mut values = Vec::with_capacity(iterations);
87 for _ in 0..iterations {
88 let start = Instant::now();
89 let _ = operation()?;
90 values.push(start.elapsed());
91 }
92 values.sort_unstable();
93 Ok(values)
94}
95
96fn print_row(engine: &str, mode: &str, values: &[Duration], commits: usize) {
97 let p50 = values[values.len() / 2].as_secs_f64() * 1_000.0;
98 let p95 = values[(values.len() * 95 / 100).min(values.len() - 1)].as_secs_f64() * 1_000.0;
99 println!("{engine},{mode},{p50:.3},{p95:.3},{commits}");
100}