Skip to main content

benchmark/
benchmark.rs

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}