diff --git a/Cargo.lock b/Cargo.lock index 332a5e4fef..4629683574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -41,6 +50,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.18" @@ -116,6 +131,7 @@ name = "asyncgit" version = "0.28.1" dependencies = [ "bitflags 2.10.0", + "criterion", "crossbeam-channel", "dirs", "easy-cast", @@ -308,6 +324,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +[[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.3" @@ -351,6 +373,33 @@ dependencies = [ "windows-link", ] +[[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.5.57" @@ -459,6 +508,39 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -538,6 +620,12 @@ dependencies = [ "winapi", ] +[[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.6" @@ -1153,7 +1241,7 @@ dependencies = [ "git2-testing", "indexmap", "insta", - "itertools", + "itertools 0.14.0", "log", "notify", "notify-debouncer-mini", @@ -1528,7 +1616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b305d85504de270ad3525d726a6b69cc59ee7b2269b014387651107ab9f0755b" dependencies = [ "bstr", - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -1966,6 +2054,17 @@ dependencies = [ "parking_lot", ] +[[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 = "hash32" version = "0.3.1" @@ -2290,6 +2389,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2766,6 +2874,12 @@ dependencies = [ "pkg-config", ] +[[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" @@ -2809,6 +2923,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -3093,7 +3217,7 @@ dependencies = [ "compact_str", "hashbrown 0.16.1", "indoc", - "itertools", + "itertools 0.14.0", "kasuari", "lru", "serde", @@ -3160,7 +3284,7 @@ dependencies = [ "hashbrown 0.16.1", "indoc", "instability", - "itertools", + "itertools 0.14.0", "line-clipping", "ratatui-core", "serde", @@ -3862,6 +3986,16 @@ dependencies = [ "zerovec", ] +[[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 = "tinyvec" version = "1.8.1" @@ -3948,7 +4082,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 0.2.0", ] @@ -4638,6 +4772,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 99e55b6d1a..cc52bc5aa8 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -11,6 +11,14 @@ license = "MIT" categories = ["asynchronous", "concurrency"] keywords = ["git"] +[[bench]] +harness = false +name = "logwalker_without_filter" + +[[bench]] +harness = false +name = "logwalker_with_commit_message_filter" + [dependencies] bitflags = "2" crossbeam-channel = "0.5" @@ -35,6 +43,7 @@ unicode-truncate = "2.0" url = "2.5" [dev-dependencies] +criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support"] } env_logger = "0.11" invalidstring = { path = "../invalidstring", version = "0.1" } pretty_assertions = "1.4" diff --git a/asyncgit/benches/logwalker_with_commit_message_filter.rs b/asyncgit/benches/logwalker_with_commit_message_filter.rs new file mode 100644 index 0000000000..7411efdacf --- /dev/null +++ b/asyncgit/benches/logwalker_with_commit_message_filter.rs @@ -0,0 +1,126 @@ +use asyncgit::sync::{ + filter_commit_by_search, CommitId, LogFilterSearch, + LogFilterSearchOptions, LogWalker, SearchFields, SearchOptions, + SharedCommitFilterFn, +}; +use criterion::{ + criterion_group, criterion_main, BatchSize, Criterion, +}; +use git2::{Repository, RepositoryOpenFlags}; +use std::{ + env, + hint::black_box, + path::{Path, PathBuf}, +}; + +const REPO_ENV_VAR: &str = "GITUI_BENCH_REPO"; +const FILTER_ENV_VAR: &str = "GITUI_BENCH_COMMIT_MESSAGE_FILTER"; +const DEFAULT_FILTER: &str = "fix"; +const LIMIT_COUNT: usize = 3000; + +fn bench_repo_path() -> PathBuf { + env::var_os(REPO_ENV_VAR).map_or_else( + || { + panic!( + "{REPO_ENV_VAR} must point at the repository to benchmark, \ + for example: {REPO_ENV_VAR}=/path/to/repo cargo bench \ + -p asyncgit --bench logwalker_with_commit_message_filter" + ); + }, + PathBuf::from, + ) +} + +fn bench_filter_pattern() -> String { + env::var(FILTER_ENV_VAR) + .unwrap_or_else(|_| String::from(DEFAULT_FILTER)) +} + +fn open_repo(path: &Path) -> Repository { + Repository::open_ext( + path, + RepositoryOpenFlags::FROM_ENV, + Vec::<&Path>::new(), + ) + .unwrap_or_else(|err| { + panic!( + "failed to open repository from {REPO_ENV_VAR}={}: {err}", + path.display() + ); + }) +} + +fn commit_message_filter( + search_pattern: String, +) -> SharedCommitFilterFn { + filter_commit_by_search(LogFilterSearch::new( + LogFilterSearchOptions { + search_pattern, + fields: SearchFields::MESSAGE_SUMMARY + | SearchFields::MESSAGE_BODY, + options: SearchOptions::default(), + }, + )) +} + +// Mirrors `AsyncLog::fetch_helper_with_filter`'s read loop, but without `thread::sleep`, so the +// timing focuses on reading and filtering the full history. +fn read_filtered_history( + repo: &Repository, + filter: SharedCommitFilterFn, +) -> asyncgit::Result { + let mut entries = vec![CommitId::default(); LIMIT_COUNT]; + entries.clear(); + + let mut commits = Vec::with_capacity(LIMIT_COUNT); + let mut walker = + LogWalker::new(repo, LIMIT_COUNT)?.filter(Some(filter)); + + loop { + entries.clear(); + let read = walker.read(&mut entries)?; + commits.extend(entries.iter().copied()); + + if read == 0 { + break; + } + } + + let commit_count = commits.len(); + black_box(commits); + + Ok(commit_count) +} + +fn logwalker_with_commit_message_filter_full_history( + c: &mut Criterion, +) { + let repo_path = bench_repo_path(); + let filter = commit_message_filter(bench_filter_pattern()); + + c.bench_function( + "logwalker_with_commit_message_filter_full_history", + |b| { + b.iter_batched( + || (open_repo(&repo_path), filter.clone()), + |(repo, filter)| { + black_box( + read_filtered_history(&repo, filter) + .unwrap_or_else(|err| { + panic!( + "failed to read and filter repository history: {err}" + ); + }), + ); + }, + BatchSize::PerIteration, + ); + }, + ); +} + +criterion_group!( + benches, + logwalker_with_commit_message_filter_full_history +); +criterion_main!(benches); diff --git a/asyncgit/benches/logwalker_without_filter.rs b/asyncgit/benches/logwalker_without_filter.rs new file mode 100644 index 0000000000..7720d570d5 --- /dev/null +++ b/asyncgit/benches/logwalker_without_filter.rs @@ -0,0 +1,86 @@ +use asyncgit::sync::{CommitId, LogWalkerWithoutFilter}; +use criterion::{ + criterion_group, criterion_main, BatchSize, Criterion, +}; +use std::{ + env, + hint::black_box, + path::{Path, PathBuf}, +}; + +const REPO_ENV_VAR: &str = "GITUI_BENCH_REPO"; +const LIMIT_COUNT: usize = 3000; + +fn bench_repo_path() -> PathBuf { + env::var_os(REPO_ENV_VAR).map_or_else( + || { + panic!( + "{REPO_ENV_VAR} must point at the repository to benchmark, \ + for example: {REPO_ENV_VAR}=/path/to/repo cargo bench \ + -p asyncgit --bench logwalker_without_filter" + ); + }, + PathBuf::from, + ) +} + +fn open_repo(path: &Path) -> gix::Repository { + gix::ThreadSafeRepository::discover_with_environment_overrides( + path, + ) + .unwrap_or_else(|err| { + panic!( + "failed to open repository from {REPO_ENV_VAR}={}: {err}", + path.display() + ); + }) + .into() +} + +// Mirrors `AsyncLog::fetch_helper_without_filter`'s read loop, but without `thread::sleep`, so the +// timing focuses on reading the full history. +fn read_full_history( + repo: &mut gix::Repository, +) -> asyncgit::Result { + let mut entries = vec![CommitId::default(); LIMIT_COUNT]; + entries.clear(); + + let mut commits: Vec = Vec::with_capacity(LIMIT_COUNT); + let mut walker = LogWalkerWithoutFilter::new(repo, LIMIT_COUNT)?; + + loop { + entries.clear(); + let read = walker.read(&mut entries)?; + commits.extend(entries.iter()); + + if read == 0 { + break; + } + } + + let commit_count = commits.len(); + black_box(commits); + + Ok(commit_count) +} + +fn logwalker_without_filter_full_history(c: &mut Criterion) { + let repo_path = bench_repo_path(); + + c.bench_function("logwalker_without_filter_full_history", |b| { + b.iter_batched_ref( + || open_repo(&repo_path), + |repo| { + black_box(read_full_history(repo).unwrap_or_else( + |err| { + panic!("failed to read repository history: {err}"); + }, + )); + }, + BatchSize::PerIteration, + ); + }); +} + +criterion_group!(benches, logwalker_without_filter_full_history); +criterion_main!(benches);