faece054
Add criterion benchmarks for core operations at scale
a73x 2026-03-21 16:19
Benchmarks list_issues, list_patches, walk_events, IssueState::from_ref, and PatchState::from_ref at various scales (10, 100, 500/1000 items). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/Cargo.lock b/Cargo.lock index a518e92..736f9df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,12 @@ dependencies = [ ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -173,6 +179,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "castaway" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -220,6 +232,33 @@ dependencies = [ ] [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -329,6 +368,67 @@ dependencies = [ ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools 0.10.5", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools 0.10.5", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -356,6 +456,12 @@ dependencies = [ ] [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -723,6 +829,7 @@ dependencies = [ "clap", "clap_complete", "clap_mangen", "criterion", "crossterm", "dirs", "ed25519-dalek", @@ -752,6 +859,17 @@ dependencies = [ ] [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", "zerocopy", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -778,6 +896,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -956,6 +1080,17 @@ dependencies = [ ] [[package]] name = "is-terminal" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -963,6 +1098,15 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" @@ -1251,6 +1395,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1424,6 +1574,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1598,7 +1776,7 @@ dependencies = [ "compact_str", "hashbrown 0.16.1", "indoc", "itertools", "itertools 0.14.0", "kasuari", "lru", "strum", @@ -1650,7 +1828,7 @@ dependencies = [ "hashbrown 0.16.1", "indoc", "instability", "itertools", "itertools 0.14.0", "line-clipping", "ratatui-core", "strum", @@ -1660,6 +1838,26 @@ dependencies = [ ] [[package]] name = "rayon" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1761,6 +1959,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2120,6 +2327,16 @@ dependencies = [ ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2155,7 +2372,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools", "itertools 0.14.0", "unicode-segmentation", "unicode-width", ] @@ -2239,6 +2456,16 @@ dependencies = [ ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2342,6 +2569,16 @@ dependencies = [ ] [[package]] name = "web-sys" version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "wezterm-bidi" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2430,6 +2667,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 89855b3..f5a21b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,8 @@ clap_mangen = "0.2" [dev-dependencies] tempfile = "3" proptest = "1" criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "core_ops" harness = false diff --git a/benches/core_ops.rs b/benches/core_ops.rs new file mode 100644 index 0000000..29f5f6d --- /dev/null +++ b/benches/core_ops.rs @@ -0,0 +1,258 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use ed25519_dalek::SigningKey; use git2::Repository; use rand_core::OsRng; use tempfile::TempDir; use git_collab::dag; use git_collab::event::{Action, Author, Event}; use git_collab::state::{IssueState, PatchState}; fn test_author() -> Author { Author { name: "Bench User".to_string(), email: "bench@example.com".to_string(), } } fn test_key() -> SigningKey { SigningKey::generate(&mut OsRng) } fn now() -> String { chrono::Utc::now().to_rfc3339() } fn init_repo(dir: &std::path::Path) -> Repository { let repo = Repository::init(dir).expect("init repo"); { let mut config = repo.config().unwrap(); config.set_str("user.name", "Bench User").unwrap(); config.set_str("user.email", "bench@example.com").unwrap(); } repo } /// Create N issues in a repo, each with no comments. Returns the repo and temp dir. fn setup_issues(n: usize) -> (Repository, TempDir) { let dir = TempDir::new().unwrap(); let repo = init_repo(dir.path()); let sk = test_key(); let author = test_author(); for i in 0..n { let event = Event { timestamp: now(), author: author.clone(), action: Action::IssueOpen { title: format!("Issue {}", i), body: format!("Body for issue {}", i), }, clock: 0, }; let oid = dag::create_root_event(&repo, &event, &sk).unwrap(); let ref_name = format!("refs/collab/issues/{}", oid); repo.reference(&ref_name, oid, false, "bench").unwrap(); } (repo, dir) } /// Create N patches in a repo. Returns the repo and temp dir. fn setup_patches(n: usize) -> (Repository, TempDir) { let dir = TempDir::new().unwrap(); let repo = init_repo(dir.path()); let sk = test_key(); let author = test_author(); for i in 0..n { let event = Event { timestamp: now(), author: author.clone(), action: Action::PatchCreate { title: format!("Patch {}", i), body: format!("Body for patch {}", i), base_ref: "main".to_string(), branch: format!("feature-{}", i), fixes: None, }, clock: 0, }; let oid = dag::create_root_event(&repo, &event, &sk).unwrap(); let ref_name = format!("refs/collab/patches/{}", oid); repo.reference(&ref_name, oid, false, "bench").unwrap(); } (repo, dir) } /// Create a single issue with N comment events appended. Returns (repo, ref_name, id, tempdir). fn setup_issue_with_comments(n: usize) -> (Repository, String, String, TempDir) { let dir = TempDir::new().unwrap(); let repo = init_repo(dir.path()); let sk = test_key(); let author = test_author(); let open_event = Event { timestamp: now(), author: author.clone(), action: Action::IssueOpen { title: "Big issue".to_string(), body: "An issue with many comments".to_string(), }, clock: 0, }; let oid = dag::create_root_event(&repo, &open_event, &sk).unwrap(); let id = oid.to_string(); let ref_name = format!("refs/collab/issues/{}", id); repo.reference(&ref_name, oid, false, "bench").unwrap(); for i in 0..n { let event = Event { timestamp: now(), author: author.clone(), action: Action::IssueComment { body: format!("Comment number {}", i), }, clock: 0, }; dag::append_event(&repo, &ref_name, &event, &sk).unwrap(); } (repo, ref_name, id, dir) } fn bench_list_issues(c: &mut Criterion) { let mut group = c.benchmark_group("list_issues"); for count in [10, 100, 1000] { let (repo, _dir) = setup_issues(count); group.bench_with_input( BenchmarkId::from_parameter(count), &count, |b, _| { b.iter(|| { let issues = git_collab::state::list_issues(&repo).unwrap(); assert_eq!(issues.len(), count); }); }, ); } group.finish(); } fn bench_list_patches(c: &mut Criterion) { let mut group = c.benchmark_group("list_patches"); for count in [10, 100, 1000] { let (repo, _dir) = setup_patches(count); group.bench_with_input( BenchmarkId::from_parameter(count), &count, |b, _| { b.iter(|| { let patches = git_collab::state::list_patches(&repo).unwrap(); assert_eq!(patches.len(), count); }); }, ); } group.finish(); } fn bench_walk_events(c: &mut Criterion) { let mut group = c.benchmark_group("walk_events"); for count in [10, 100, 500] { let (repo, ref_name, _id, _dir) = setup_issue_with_comments(count); group.bench_with_input( BenchmarkId::from_parameter(count), &count, |b, _| { b.iter(|| { let events = dag::walk_events(&repo, &ref_name).unwrap(); // 1 open + N comments assert_eq!(events.len(), count + 1); }); }, ); } group.finish(); } fn bench_issue_from_ref(c: &mut Criterion) { let mut group = c.benchmark_group("issue_from_ref"); for count in [10, 100, 500] { let (repo, ref_name, id, _dir) = setup_issue_with_comments(count); group.bench_with_input( BenchmarkId::from_parameter(count), &count, |b, _| { b.iter(|| { let state = IssueState::from_ref(&repo, &ref_name, &id).unwrap(); assert_eq!(state.comments.len(), count); }); }, ); } group.finish(); } fn bench_patch_from_ref(c: &mut Criterion) { let mut group = c.benchmark_group("patch_from_ref"); let dir = TempDir::new().unwrap(); let repo = init_repo(dir.path()); let sk = test_key(); let author = test_author(); // Create a patch with many comments for count in [10, 100, 500] { let create_event = Event { timestamp: now(), author: author.clone(), action: Action::PatchCreate { title: format!("Patch with {} comments", count), body: "A patch".to_string(), base_ref: "main".to_string(), branch: format!("feature-bench-{}", count), fixes: None, }, clock: 0, }; let oid = dag::create_root_event(&repo, &create_event, &sk).unwrap(); let id = oid.to_string(); let ref_name = format!("refs/collab/patches/{}", id); repo.reference(&ref_name, oid, false, "bench").unwrap(); for i in 0..count { let event = Event { timestamp: now(), author: author.clone(), action: Action::PatchComment { body: format!("Patch comment {}", i), }, clock: 0, }; dag::append_event(&repo, &ref_name, &event, &sk).unwrap(); } group.bench_with_input( BenchmarkId::from_parameter(count), &count, |b, _| { b.iter(|| { let state = PatchState::from_ref(&repo, &ref_name, &id).unwrap(); assert_eq!(state.comments.len(), count); }); }, ); } group.finish(); } criterion_group!( benches, bench_list_issues, bench_list_patches, bench_walk_events, bench_issue_from_ref, bench_patch_from_ref, ); criterion_main!(benches);