diff --git a/Cargo.lock b/Cargo.lock index c8b5c38c4..a6ce008d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2803,6 +2803,7 @@ dependencies = [ "jacquard-identity", "lexicons", "line-numbers", + "quick_cache", "reqwest 0.13.1", "rustc-hash", "serde_json", diff --git a/gitmirror/crates/gitmirror-xrpc/Cargo.toml b/gitmirror/crates/gitmirror-xrpc/Cargo.toml index 7c3ab90f3..a3ca75c0c 100644 --- a/gitmirror/crates/gitmirror-xrpc/Cargo.toml +++ b/gitmirror/crates/gitmirror-xrpc/Cargo.toml @@ -28,6 +28,7 @@ rustc-hash = "2.1.2" gix = { version = "0.84", features = ["parallel", "blob-diff", "blame", "merge", "sha1", "sha256", "revision", "tree-editor"] } gix-pack = { workspace = true } gix-transport = { workspace = true, features = ["async-client"] } +quick_cache = { workspace = true } futures-lite = { workspace = true } reqwest = { workspace = true } diff --git a/gitmirror/crates/gitmirror-xrpc/src/diff.rs b/gitmirror/crates/gitmirror-xrpc/src/diff.rs index 4b398a736..7823e5484 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/diff.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/diff.rs @@ -7,6 +7,7 @@ use tracing::info; /// Blobs larger than this are treated as binary. const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; +const STAT_CACHE_CAPACITY: usize = 65_536; #[derive(Debug)] pub struct FileContent { @@ -63,6 +64,25 @@ pub struct Stat { pub files_changed: u64, } +pub struct StatCache { + pub(crate) commits: quick_cache::sync::Cache, +} + +impl Default for StatCache { + fn default() -> Self { + Self { + commits: quick_cache::sync::Cache::new(STAT_CACHE_CAPACITY), + } + } +} + +impl StatCache { + pub fn commit(&self, repo: &gix::Repository, commit: &gix::Commit<'_>) -> anyhow::Result { + self.commits + .get_or_insert_with(&commit.id, || commit_stat(repo, commit)) + } +} + fn parent_tree<'repo>( repo: &'repo gix::Repository, commit: &gix::Commit<'_>, diff --git a/gitmirror/crates/gitmirror-xrpc/src/lib.rs b/gitmirror/crates/gitmirror-xrpc/src/lib.rs index 24fadc22a..9b12fcc7d 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/lib.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/lib.rs @@ -27,6 +27,7 @@ pub struct AppState { pub(crate) service_auth: service_auth::ServiceAuthConfig>, pub(crate) knot_policy: KnotPolicy, pub(crate) clock: Arc, + pub(crate) stats: Arc, } impl AppState { @@ -45,6 +46,7 @@ impl AppState { service_auth, knot_policy, clock, + stats: Arc::default(), } } } diff --git a/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs b/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs index 5365b3912..fabeac140 100644 --- a/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs +++ b/gitmirror/crates/gitmirror-xrpc/src/routes/git.rs @@ -614,6 +614,7 @@ fn commit_log_walk<'repo>( fn list_commits_inner( repo: &gix::Repository, + stats: &diff::StatCache, args: list_commits::ListCommits, ) -> Result, XrpcError> { let ranges: Vec> = args @@ -660,7 +661,7 @@ fn list_commits_inner( let mut row = list_commits::Commit::try_from(GixCommit(&commit)) .map_err(|e| XrpcError::Internal(e.to_string()))?; if args.include_stats.unwrap_or(false) { - let stat = diff::commit_stat(repo, &commit); + let stat = stats.commit(repo, &commit); row.stat = stat .inspect_err(|error| warn!(oid = %commit.id, %error, "commit stat failed")) .ok() @@ -682,7 +683,8 @@ pub(crate) async fn list_commits( detail: e.to_string(), })?; - tokio::task::spawn_blocking(move || list_commits_inner(&repo, args)) + let stats = state.stats.clone(); + tokio::task::spawn_blocking(move || list_commits_inner(&repo, &stats, args)) .await .map_err(|e| XrpcError::Internal(e.to_string()))? .map(|commits| { @@ -1329,9 +1331,15 @@ mod stat_tests { (root, repo) } - fn list(repo: &gix::Repository, range: &str, include_stats: bool) -> Vec { + fn list( + repo: &gix::Repository, + stats: &diff::StatCache, + range: &str, + include_stats: bool, + ) -> Vec { list_commits_inner( repo, + stats, list_commits::ListCommits { repo: Did::raw("did:plc:log".into()), ranges: Some(vec![range.into()]), @@ -1361,17 +1369,41 @@ mod stat_tests { #[test] fn stats_are_absent_unless_asked_for() { let (_root, repo) = fixture(); - assert_eq!(counts(&list(&repo, "HEAD", false)), [None, None, None]); + let stats = diff::StatCache::default(); + assert_eq!( + counts(&list(&repo, &stats, "HEAD", false)), + [None, None, None] + ); } #[test] fn each_commit_is_measured_against_its_first_parent() { let (_root, repo) = fixture(); + let stats = diff::StatCache::default(); assert_eq!( - counts(&list(&repo, "HEAD", true)), + counts(&list(&repo, &stats, "HEAD", true)), [Some((1, 0, 1)), Some((1, 1, 1)), Some((4, 0, 2))] ); } + + #[test] + fn a_measured_commit_is_served_from_the_cache() { + let (_root, repo) = fixture(); + let stats = diff::StatCache::default(); + let head = repo.head_commit().unwrap(); + stats.commits.insert( + head.id, + diff::Stat { + insertions: 7, + deletions: 7, + files_changed: 7, + }, + ); + assert_eq!( + counts(&list(&repo, &stats, "HEAD", true))[0], + Some((7, 7, 7)) + ); + } } #[cfg(test)]