Skip to main content

benchmark_cross_repo/
benchmark_cross_repo.rs

1use std::{
2    env,
3    path::Path,
4    process::Command,
5    time::{Duration, Instant},
6};
7
8use weavatrix_git::{HistoryOptions, RepositoryHistory, RepositorySet};
9
10fn main() -> Result<(), Box<dyn std::error::Error>> {
11    let args = env::args().skip(1).collect::<Vec<_>>();
12    if args.len() < 3 {
13        return Err("usage: benchmark_cross_repo <max-count> <iterations> <repo>...".into());
14    }
15    let max_count = args[0].parse()?;
16    let iterations = args[1].parse()?;
17    let paths = &args[2..];
18    let options = HistoryOptions {
19        max_commits: max_count,
20        first_parent: true,
21        ..HistoryOptions::default()
22    };
23    let set = open(paths)?;
24    let expected = git_histories(paths, max_count)?;
25    let actual = set.histories_parallel(options)?;
26    for (history, expected) in actual.iter().zip(&expected) {
27        let actual = history
28            .commits
29            .iter()
30            .map(ToString::to_string)
31            .collect::<Vec<_>>();
32        if actual != *expected {
33            return Err("cross-repository history differs from git rev-list".into());
34        }
35    }
36
37    let serial = measure(iterations, || {
38        set.histories(options).map(|items| item_count(&items))
39    })?;
40    let parallel = measure(iterations, || {
41        set.histories_parallel(options)
42            .map(|items| item_count(&items))
43    })?;
44    let reopen = measure(iterations, || {
45        open(paths)?
46            .histories_parallel(options)
47            .map(|items| item_count(&items))
48    })?;
49    let git = measure(iterations, || {
50        git_histories(paths, max_count).map(|items| items.iter().map(Vec::len).sum::<usize>())
51    })?;
52    println!("engine,mode,p50_ms,p95_ms,repositories,commits");
53    print_row(
54        "weavatrix-git",
55        "serial",
56        &serial,
57        paths.len(),
58        item_count(&actual),
59    );
60    print_row(
61        "weavatrix-git",
62        "parallel",
63        &parallel,
64        paths.len(),
65        item_count(&actual),
66    );
67    print_row(
68        "weavatrix-git",
69        "reopen-parallel",
70        &reopen,
71        paths.len(),
72        item_count(&actual),
73    );
74    print_row(
75        "git.exe",
76        "sequential-processes",
77        &git,
78        paths.len(),
79        item_count(&actual),
80    );
81    Ok(())
82}
83
84fn open(paths: &[String]) -> weavatrix_git::Result<RepositorySet> {
85    RepositorySet::open(
86        paths
87            .iter()
88            .enumerate()
89            .map(|(index, path)| (format!("r{index}"), path)),
90    )
91}
92
93fn git_histories(
94    paths: &[String],
95    max: usize,
96) -> Result<Vec<Vec<String>>, Box<dyn std::error::Error>> {
97    paths.iter().map(|path| git_ids(path, max)).collect()
98}
99
100fn git_ids(path: impl AsRef<Path>, max: usize) -> Result<Vec<String>, Box<dyn std::error::Error>> {
101    let output = Command::new("git")
102        .arg("-C")
103        .arg(path.as_ref())
104        .args(["rev-list", "--first-parent", "--max-count"])
105        .arg(max.to_string())
106        .arg("HEAD")
107        .output()?;
108    if !output.status.success() {
109        return Err(String::from_utf8_lossy(&output.stderr).into_owned().into());
110    }
111    Ok(String::from_utf8(output.stdout)?
112        .lines()
113        .map(str::to_owned)
114        .collect())
115}
116
117fn item_count(items: &[RepositoryHistory]) -> usize {
118    items.iter().map(|item| item.commits.len()).sum()
119}
120
121fn measure<T, E>(
122    iterations: usize,
123    mut operation: impl FnMut() -> Result<T, E>,
124) -> Result<Vec<Duration>, E> {
125    for _ in 0..3 {
126        let _ = operation()?;
127    }
128    let mut values = Vec::with_capacity(iterations);
129    for _ in 0..iterations {
130        let start = Instant::now();
131        let _ = operation()?;
132        values.push(start.elapsed());
133    }
134    values.sort_unstable();
135    Ok(values)
136}
137
138fn print_row(engine: &str, mode: &str, values: &[Duration], repositories: usize, commits: usize) {
139    let p50 = values[values.len() / 2].as_secs_f64() * 1_000.0;
140    let p95 = values[(values.len() * 95 / 100).min(values.len() - 1)].as_secs_f64() * 1_000.0;
141    println!("{engine},{mode},{p50:.3},{p95:.3},{repositories},{commits}");
142}