Speed up git-ai stats range mode by batching authorship-note reads#1652
Open
jwiegley wants to merge 1 commit into
Open
Speed up git-ai stats range mode by batching authorship-note reads#1652jwiegley wants to merge 1 commit into
git-ai stats range mode by batching authorship-note reads#1652jwiegley wants to merge 1 commit into
Conversation
a601a01 to
fb5c37a
Compare
`git-ai stats <a>..<b>` was dominated by git-subprocess fan-out, not by any database work: the range path blames every changed file three times (diff_ai_accepted_stats + start/end VirtualAttributions), and each blame re-read every blamed commit's authorship note with a separate `git notes --ref=ai show` subprocess, uncached across files and passes. On a 100-commit range over this repo that is several thousand `git notes show` calls alone — the large majority of the total git subprocesses. (Profiling confirmed the cost is git subprocesses, not SQLite — the default GitNotes backend touches 0ms of SQLite on this path, so a #1630-style index PR would not help `git-ai stats`.) Two changes, both output-preserving: 1. Batch the note reads. Add `notes_api::read_authorship_v3_batch`, which resolves many commits' notes with one `git ls-tree` + `cat-file --batch` (via `refs::notes_for_commits`) instead of one `git notes show` per commit. It returns exactly what `read_authorship_v3(repo, sha).ok()` yields for each commit; the v3 parse is factored into `refs::parse_reference_as_authorship_log_v3` and shared by both paths, and the Http backend delegates per commit to keep its cache-hit semantics. Both blame overlays (`overlay_ai_authorship` and `populate_ai_human_authors`) pre-seed their existing per-commit cache from one batched read. 2. Skip dead work on the stats path. `blame()` with `no_output` discards the blame hunks, so the `populate_ai_human_authors` pass that annotates them with `ai_human_author` (a second per-commit note read) is wasted there. Add `GitAiBlameOptions::skip_human_author_population` (default false, debug-asserted to only be set on no_output paths) and set it on the two stats blame call sites. It is output-invariant because `overlay_ai_authorship` derives `line_authors` per line independently of hunk grouping/`ai_human_author`. Measured on the real repo (release build), same-run A/B (median of repeated runs, warm-up discarded), `range_stats` byte-identical. The speedup grows with range size because it eliminates per-commit note fan-out, which scales with the number of blamed commits. On `stats HEAD~100..HEAD` the per-commit `git notes show` calls collapse from ~7,200 to ~50 (the remainder shifts into a few batched `cat-file`/`ls-tree` reads); wall time: stats HEAD~100..HEAD 69.0s -> 40.0s (~1.7x; baseline noisy, 69-93s across runs) stats HEAD~1000..HEAD 272.3s -> 110.2s (~2.5x) Adds regression tests: the batched read equals the per-commit read for commits with notes, without notes, and non-existent commits; the empty-input fast path; and the skip-dead-pass gate is output-invariant (blame line authors identical with the pass on vs off). (Tests use the GitNotes backend; the Http delegation and batch-error fallback paths are correct by construction but not exercised.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYmBGrGCfKDY8NLz17TqxJ
82ebba2 to
1160141
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Makes
git-ai stats <a>..<b>(range mode) faster — ~1.7× on a 100-commit range up to ~2.5× on a 1000-commit range — by removing redundant git-subprocess fan-out in the blame path.range_statsoutput is byte-identical.Profiling (release build, real repo) showed the cost is git subprocesses, not SQLite — the default
GitNotesbackend touches 0 ms of SQLite on this path, so a #1630-style index would not helpgit-ai stats. The range path blames every changed file three times (diff_ai_accepted_stats+ start/endVirtualAttributions), and each blame re-read every blamed commit's authorship note with a separategit notes --ref=ai show— thousands of per-commit note spawns, uncached across files and passes.Changes (both output-preserving)
notes_api::read_authorship_v3_batchresolves many commits' notes with onegit ls-tree+cat-file --batch(viarefs::notes_for_commits) instead of onegit notes showper commit. It returns exactly whatread_authorship_v3(repo, sha).ok()yields per commit — the v3 parse is factored intorefs::parse_reference_as_authorship_log_v3and shared by both paths, and theHttpbackend delegates per commit to preserve its cache-hit semantics. Both blame overlays (overlay_ai_authorship,populate_ai_human_authors) pre-seed their existing per-commit cache from one batched read.blame()withno_outputdiscards the blame hunks, so thepopulate_ai_human_authorspass that annotates them withai_human_author(a second per-commit note read) is wasted there. AddsGitAiBlameOptions::skip_human_author_population(defaultfalse, debug-asserted to only be set onno_outputpaths) and sets it on the two stats blame call sites. It is output-invariant becauseoverlay_ai_authorshipderivesline_authorsper line independently of hunk grouping.Performance (release build, real repo, same-run A/B,
range_statsbyte-identical)The speedup grows with range size because it eliminates per-commit note fan-out, which scales with the number of blamed commits:
git-ai stats HEAD~100..HEADgit-ai stats HEAD~1000..HEADUnder a subprocess-counting harness on
HEAD~100..HEAD, per-commitgit notes showcollapses from ~7,200 to ~50 (the remainder shifts into batchedcat-file/ls-tree); total git spawns ~14.5k → ~9.3k same-harness. (Exact spawn totals are method-sensitive; thenotes showcollapse and the wall-time deltas are the robust figures.)Validation
task fmt,task lint(clippy-D warnings), fulltask test— green.range_statson real data across 100- and 1000-commit ranges (range_1000 = 120,390 AI lines across 32 tool/model combos — identical before/after).from_utf8_lossy(matching the existingCommitAuthorshippath) vs the per-commit strict decode — unreachable for real notes (always valid UTF-8 JSON). TheHttpand batch-error fallback arms delegate per commit to the existing reader (equivalent by construction; not exercised by tests).Note
The #1630 index pattern still applies — just to a different command,
git-ai usage(MetricsDatabase::get_metric_historyis a full-table scan that deserializes every row). Not touched here.🤖 Generated with Claude Code