You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Umbrella issue for the large-repo experience. #473 and #474 are the specific
defects underneath it; this is the shape of the whole problem, what the fix
ladder looks like, and what "better" should mean in numbers.
All figures are from pnpm bench (#257) on an Apple M4 Pro; method in docs/dev/performance.md.
Where we actually are
repository
first screen (the 11 reads refreshAll issues, at once)
50,000 commits
255 ms
5,001 branches / 2,000 tags
219 ms
55,000 changed files
5.42 s (= status alone)
torvalds/linux (1.48M commits)
15.8 s
Ten pages into the kernel's history: 2 minutes 38 seconds.
This is one problem, not a class of them. Everything else measured is fine
or proportional: opening a repository is 0.11 ms, the eleven concurrent reads
cost what the slowest costs rather than the sum, IPC encoding is lost in the
noise, and status is 1.8× git's own work on a question where git itself takes
three seconds. On the kernel, log_page alone is 15.95 s of the 15.8 s screen.
Fix history and the large-repo problem is essentially fixed.
Root cause, and why the obvious fixes do not work
There is no cheaper ordering to switch to
log_page walks with Sort::TIME | Sort::TOPOLOGICAL because the commit
graph's lane assignment needs parents to come after their children. The
tempting fix is "use a cheaper ordering". Measured on the kernel:
git log -500 (default)
0.285 s
git log --date-order -500
12.3 s
git log --topo-order -500
10.3 s
--date-order is no cheaper than --topo-order. The cost is not topological
ordering — it is any ordering that guarantees parents after children, which
requires walking the whole reachable graph. git's default is fast only because
it uses a date-based slop heuristic that makes no such guarantee, and we cannot
draw lanes from it.
The commit-graph is git's answer, and it does nothing for us
git makes that walk affordable with generation numbers from the commit-graph
file:
no commit-graph
with commit-graph
git log --topo-order -500, kernel
9.51 s
21 ms
git log --topo-order -500, 50k fixture
271 ms
29 ms
Writing one for the kernel takes 14 seconds, once.
It buys us nothing. Measured on the 50k fixture, where the effect is
unambiguous:
no commit-graph
with commit-graph
git log --topo-order -500
271 ms
29 ms (9.3× faster)
our log_page, first page
252 ms
259 ms (unchanged)
our log_page, tenth page
2.40 s
2.48 s (unchanged)
The reason is in libgit2 1.9.7 (what git2 0.21 vendors), and it is worth
writing down because it rules out a day of work:
commit_list.cdoes read the commit-graph — git_commit_list_parse
takes parents, commit time and generation from it instead of the odb. So the
file is consumed, and per-commit parsing is cheaper.
revwalk.c contains zero references to the commit-graph or to generation
numbers. Across all of src/, ->generation is read in exactly two places: graph.c (git_graph_descendant_of) and merge.c (merge bases).
So the walk still visits every reachable commit. Generation numbers are
populated and then ignored by the one code path that would benefit most.
Adopting the commit-graph is therefore not the fix on its own. Anything
that starts "let's run git commit-graph write in the background" has to be
paired with something that can actually use it.
The ladder
Four tiers, cheapest first. Each is independently shippable and each is worth
doing on its own.
Tier 1 — stop paying for the same work twice (#473, #474)
Do not re-pay the sort per page. The per-page cost is flat in depth: ten
pages cost ten times one page, because each log_page rebuilds the walk. The
cursor already carries the frontier, so the walk logically continues. This
alone turns 2 m 38 s into ~16 s on the kernel and 2.40 s into ~250 ms on the
50k fixture. Best win per unit of effort in this whole list.
Cache the ref map per repository, invalidated on ref writes. collect_ref_map currently runs on every page: 16× git's work on a
2,000-commit repository with 7,001 refs.
Tier 2 — stop making the user wait for work that is already done
Independent of making the walk fast, and probably the largest perceived gain:
Stream the page. We wait for all 500 commits before showing any. The
first rows are available almost immediately even today — emitting them as they
are walked turns "15.8 s of nothing" into "history appears, and fills in".
Show the repository before history arrives. The other ten reads finish in
milliseconds even on the kernel; there is no reason the branch list, status
and HEAD wait behind the log.
Make the walk cancellable. Scrolling away, switching ref scope or closing
the tab should abandon it rather than let it run to completion.
Tier 3 — make the walk genuinely fast
The real fix, and the one that needs a decision. Options, in the order I would
try them:
Shell out to git log for the paged walk, and keep a commit-graph warm.
This repository already shells out where libgit2 falls short (it is stated
policy in CLAUDE.md, and bisect_status already does it). git log --topo-order --format=… with parents gives everything the lane layout needs,
and with a commit-graph present it is 21 ms on the kernel. Pair it with git commit-graph write --reachable run in the background on open — which is
what git maintenance does anyway — and fall back to libgit2 when git is
missing. Cost: one subprocess per page (~12 ms here), against 15.8 s.
Teach libgit2's revwalk to use generation numbers.commit->generation
is already populated; the pruning logic is what is missing. This is the right
long-term fix and is upstreamable, but it is a patch to a vendored C
dependency and a much longer road.
Maintain our own generation index. Most control, most work, most
invalidation risk. Only if 1 and 2 are both refused.
Tier 4 — do not regress
pnpm bench exists now. Re-run it with --linux when touching the log walk, status or the refresh path, and commit what it writes — the numbers in docs/dev/performance.md are the record that makes a regression visible.
What "better" should mean
Concrete acceptance criteria, so this issue can be closed on evidence:
First screen on torvalds/linux under 1 second (today: 15.8 s).
Tenth page under 500 ms (today: 2 m 38 s).
No regression on the generated fixtures: 50k commits stays ≈255 ms, 7,001
refs stays ≈219 ms.
Note the sequencing: once the log is fixed, the kernel's first screen becomes
bounded by status at 989 ms on a clean 96,034-file tree. That is the next
target after this one, and it is a different problem — status is already only
1.8× git's own work, so it needs a cheaper question (untracked cache, fsmonitor)
rather than a faster answer.
Not in scope here
Making status faster on huge trees — separate, and it is proportional today.
Umbrella issue for the large-repo experience. #473 and #474 are the specific
defects underneath it; this is the shape of the whole problem, what the fix
ladder looks like, and what "better" should mean in numbers.
All figures are from
pnpm bench(#257) on an Apple M4 Pro; method indocs/dev/performance.md.Where we actually are
refreshAllissues, at once)statusalone)torvalds/linux(1.48M commits)Ten pages into the kernel's history: 2 minutes 38 seconds.
This is one problem, not a class of them. Everything else measured is fine
or proportional: opening a repository is 0.11 ms, the eleven concurrent reads
cost what the slowest costs rather than the sum, IPC encoding is lost in the
noise, and
statusis 1.8× git's own work on a question where git itself takesthree seconds. On the kernel,
log_pagealone is 15.95 s of the 15.8 s screen.Fix history and the large-repo problem is essentially fixed.
Root cause, and why the obvious fixes do not work
There is no cheaper ordering to switch to
log_pagewalks withSort::TIME | Sort::TOPOLOGICALbecause the commitgraph's lane assignment needs parents to come after their children. The
tempting fix is "use a cheaper ordering". Measured on the kernel:
git log -500(default)git log --date-order -500git log --topo-order -500--date-orderis no cheaper than--topo-order. The cost is not topologicalordering — it is any ordering that guarantees parents after children, which
requires walking the whole reachable graph. git's default is fast only because
it uses a date-based slop heuristic that makes no such guarantee, and we cannot
draw lanes from it.
The commit-graph is git's answer, and it does nothing for us
git makes that walk affordable with generation numbers from the commit-graph
file:
git log --topo-order -500, kernelgit log --topo-order -500, 50k fixtureWriting one for the kernel takes 14 seconds, once.
It buys us nothing. Measured on the 50k fixture, where the effect is
unambiguous:
git log --topo-order -500log_page, first pagelog_page, tenth pageThe reason is in libgit2 1.9.7 (what
git2 0.21vendors), and it is worthwriting down because it rules out a day of work:
commit_list.cdoes read the commit-graph —git_commit_list_parsetakes parents, commit time and
generationfrom it instead of the odb. So thefile is consumed, and per-commit parsing is cheaper.
revwalk.ccontains zero references to the commit-graph or to generationnumbers. Across all of
src/,->generationis read in exactly two places:graph.c(git_graph_descendant_of) andmerge.c(merge bases).populated and then ignored by the one code path that would benefit most.
Adopting the commit-graph is therefore not the fix on its own. Anything
that starts "let's run
git commit-graph writein the background" has to bepaired with something that can actually use it.
The ladder
Four tiers, cheapest first. Each is independently shippable and each is worth
doing on its own.
Tier 1 — stop paying for the same work twice (#473, #474)
pages cost ten times one page, because each
log_pagerebuilds the walk. Thecursor already carries the frontier, so the walk logically continues. This
alone turns 2 m 38 s into ~16 s on the kernel and 2.40 s into ~250 ms on the
50k fixture. Best win per unit of effort in this whole list.
collect_ref_mapcurrently runs on every page: 16× git's work on a2,000-commit repository with 7,001 refs.
file_historyand take it off the exclusive lock (file_history walks all of history on a file with fewer than 500 commits, holding the exclusive lock #474).Tier 2 — stop making the user wait for work that is already done
Independent of making the walk fast, and probably the largest perceived gain:
first rows are available almost immediately even today — emitting them as they
are walked turns "15.8 s of nothing" into "history appears, and fills in".
milliseconds even on the kernel; there is no reason the branch list, status
and HEAD wait behind the log.
the tab should abandon it rather than let it run to completion.
Tier 3 — make the walk genuinely fast
The real fix, and the one that needs a decision. Options, in the order I would
try them:
git logfor the paged walk, and keep a commit-graph warm.This repository already shells out where libgit2 falls short (it is stated
policy in CLAUDE.md, and
bisect_statusalready does it).git log --topo-order --format=…with parents gives everything the lane layout needs,and with a commit-graph present it is 21 ms on the kernel. Pair it with
git commit-graph write --reachablerun in the background on open — which iswhat
git maintenancedoes anyway — and fall back to libgit2 when git ismissing. Cost: one subprocess per page (~12 ms here), against 15.8 s.
commit->generationis already populated; the pruning logic is what is missing. This is the right
long-term fix and is upstreamable, but it is a patch to a vendored C
dependency and a much longer road.
invalidation risk. Only if 1 and 2 are both refused.
Tier 4 — do not regress
pnpm benchexists now. Re-run it with--linuxwhen touching the log walk,statusor the refresh path, and commit what it writes — the numbers indocs/dev/performance.mdare the record that makes a regression visible.What "better" should mean
Concrete acceptance criteria, so this issue can be closed on evidence:
torvalds/linuxunder 1 second (today: 15.8 s).refs stays ≈219 ms.
Note the sequencing: once the log is fixed, the kernel's first screen becomes
bounded by
statusat 989 ms on a clean 96,034-file tree. That is the nexttarget after this one, and it is a different problem —
statusis already only1.8× git's own work, so it needs a cheaper question (untracked cache, fsmonitor)
rather than a faster answer.
Not in scope here
statusfaster on huge trees — separate, and it is proportional today.work.