benchmark_cross_repo/
benchmark_cross_repo.rs1use 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 ¶llel,
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}