Skip to content
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,19 @@ Each rule's full story (why, traps, tests that pin it) is in the named doc.
nothing (commits are immutable), the decorations by a ref fingerprint — add a
third cached thing and it needs its own answer to "what makes this wrong?".
(`docs/dev/backend.md`, `docs/dev/performance.md`)
- **…and the ORDER itself comes from `git rev-list --date-order`**
(`git/log_walk.rs`, #483), because that libgit2 walk costs 15.7 s on the
kernel and never reads the commit-graph that makes git's answer 188 ms.
**`--date-order`, NEVER `--topo-order`:** they are different questions, and
on the kernel they share only 1,627 of the first 2,000 oids — swapping them
silently changes which commits the first page shows. `log_walk_ordering.rs`
pins it, with a fixture whose branches interleave on purpose; one that does
not would pass against the mistake. Only oids cross over, commit data still
comes from libgit2, and any failure falls back to the libgit2 walk — a slow
page, never a failed one. `git/commit_graph.rs` keeps the `--split`
commit-graph that makes it fast (the plain `--reachable` form re-pays 14.3 s
even when nothing changed) and honours `core.commitGraph`.
(`docs/dev/backend.md`, `docs/dev/performance.md`)
- **A commit row's columns have a YIELD ORDER, and it is the template.** Every
track in `commitRowGrid` but the subject and the author is a fixed width, so
a new fixed column — or a wider one — comes straight out of the subject,
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,18 @@ disagree — so no figure here can be nudged by hand.

<!-- BEGIN BENCHMARK SUMMARY — generated by scripts/bench.sh, do not edit -->

Measured on Apple M4 Pro (14 cores, 48 GB, macos/aarch64) with git version 2.50.1 (Apple Git-155), on 2026-09-17 — medians over repeat calls against the real backend. `pnpm bench` reproduces the generated fixtures in about a minute; the kernel clone is opt-in.
Measured on Apple M4 Pro (14 cores, 48 GB, macos/aarch64) with git version 2.50.1 (Apple Git-155), on 2026-09-18 — medians over repeat calls against the real backend. `pnpm bench` reproduces the generated fixtures in about a minute; the kernel clone is opt-in.

| Repository | First screen | Status | First page of history | …vs `git` |
| --- | --- | --- | --- | --- |
| **torvalds/linux**<br>1,482,923 commits · 96,034 files · 946 tags · 13 changed | 15.84 s | 989 ms | 15.95 s | 1.7× |
| **deep**<br>50,000 commits · 16 files | 253 ms | 0.53 ms | 249 ms | 1.3× |
| **wide**<br>1 commit · 50,000 files · 55,000 changed | 5.42 s | 5.42 s | 0.25 ms | — |
| **refs**<br>2,000 commits · 32 files · 5,001 branches · 2,000 tags | 219 ms | 0.55 ms | 135 ms | 16× |
| **torvalds/linux**<br>1,482,923 commits · 96,034 files · 946 tags · 13 changed | 999 ms | 1.03 s | 8.70 ms | 0.30× |
| **deep**<br>50,000 commits · 16 files | 59.0 ms | 0.54 ms | 3.21 ms | |
| **wide**<br>1 commit · 50,000 files · 55,000 changed | 5.41 s | 5.38 s | 0.25 ms | — |
| **refs**<br>2,000 commits · 32 files · 5,001 branches · 2,000 tags | 224 ms | 0.56 ms | 121 ms | 28× |

**First screen** is the eleven reads the app issues when it opens a repository, issued at once — a composite, because the failure worth catching is one slow read blocking the other ten. **Status** returns per-file added and removed counts, so its baseline is `git status --porcelain` plus both `--numstat` diffs rather than a bare `git status`. Ratios are against git's *work*, with process start-up subtracted — deliberately the comparison that flatters us least — and a dash is a baseline too small to divide by. No figure here includes the UI: the benchmark drives the git backend directly, with no webview in it.

**torvalds/linux is the bad case, and publishing it is the point.** The first screen costs 15.84 s there, and reaching ten pages into its history costs 157.67 s: a sorted libgit2 revwalk pre-walks all 1,482,923 commits before it yields one, and the next page pays for that again. The developer who opens a repository that size and waits is the one this was written for, so the number belongs here rather than in a backlog.
**torvalds/linux is the case that matters, and publishing it is the point.** The first screen costs 999 ms on 1,482,923 commits, and reaching ten pages into its history costs 116 ms — the log's order comes from git over a commit-graph the app maintains, because libgit2's own sorted revwalk pre-walks the entire graph before it yields a single commit and never reads that file (#483). What is slowest here now is "History of one file" at 16.88 s, and it is published for the same reason the fifteen seconds were: the developer who opens a repository this size is the one this was written for.

Every operation on every fixture, the `git` command behind each baseline, and what the numbers were read to mean: [`docs/dev/performance.md`](./docs/dev/performance.md).

Expand Down
67 changes: 67 additions & 0 deletions docs/dev/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,73 @@ walk a `Vec<RefInfo>` per commit — `{ name, kind }`, where `kind` is

`collect_ref_map` no longer runs per page — see below.

## The walk's ORDER comes from git, not from libgit2 (#483)

#473 stopped the sort being re-paid per page. It could not make the sort
itself cheaper, and on `torvalds/linux` that one preparation is 15.7 s — the
whole of what a user waits for on open, since everything else the first screen
needs finishes inside a second.

`git/log_walk.rs` takes the order from `git rev-list` instead. Measured on the
kernel, the 100,000-oid walk `MAX_ORDER` actually asks for:

| | no commit-graph | with commit-graph |
| --- | --- | --- |
| libgit2 `TIME \| TOPOLOGICAL` | 15,743 ms | 15,743 ms (it never reads the file) |
| `rev-list --date-order` | 10,123 ms | **188 ms** |

**`--date-order`, and never `--topo-order`.** `Sort::TIME | Sort::TOPOLOGICAL`
is Kahn's algorithm over a time-priority queue, which is precisely what git
calls `--date-order`. `--topo-order` answers a different question — it also
refuses to intermix independent lines of history — and on the kernel the two
share only 1,627 of the first 2,000 oids. It does not reorder the same commits,
it returns different ones, so taking it would silently change which commits the
first page shows. #473 and #476 both proposed it.
`tests/log_walk_ordering.rs` pins the mapping in both directions, and its
fixture interleaves two branches' commit dates on purpose — one that does not
produces the same sequence either way and would pass against the mistake.

**Only oids cross over.** Commit metadata still comes from libgit2 via
`find_commit`, so there is no `--format` string to keep in sync with
`CommitInfo` and nothing downstream of `WalkOrder` changes.

**Every failure is a slow page, never a failed one.** Git missing, git exiting
non-zero, or output that does not parse all return `None` and fall through to
the libgit2 revwalk, which is exactly the code that ran before this existed.
`PGIT_DISABLE_REV_LIST` forces that path for tests and for support.

The parser requires a FULL-LENGTH hex id rather than leaving it to
`Oid::from_str`, which accepts an abbreviated string and zero-pads it — so
output truncated mid-line would otherwise parse into a plausible order naming
an object that does not exist.

### The commit-graph is not an optimisation on top of this, it IS it

Without one, `rev-list --date-order` costs 10,123 ms on the kernel and the whole
change buys nothing. A fresh clone has none — `git clone` does not write one and
`gc --auto` does not fire on a single packfile — so `git/commit_graph.rs` keeps
one, in the user's own repository, exactly where `git gc` and `git maintenance`
put it and where it makes the user's own `git log` fast too.

**`--split` is not a preference.** Measured on the kernel:

| | cost |
| --- | --- |
| `commit-graph write --reachable`, cold | 14,509 ms |
| `commit-graph write --reachable`, **already fresh** | 14,305 ms |
| `commit-graph write --reachable --split`, cold | 14,531 ms |
| `commit-graph write --reachable --split`, nothing new | **59.9 ms** |

The plain form rewrites everything every time, so scheduling it on open would
burn fourteen seconds of CPU per open forever.

It is scheduled from `commands/repo.rs` AFTER the open resolves, on the blocking
pool, and nothing waits for it — the first write on a giant repository is ~14.5 s
and that open is served by the slow path, which is exactly as slow as it was
before any of this. It honours `core.commitGraph`: a user who turned git's own
commit-graph reading off gets no file, because they would get a file they did
not ask for AND no speedup.

## The paged log prepares ONE walk (#473)

`log_page` used to build a fresh revwalk per page. That reads like an obvious
Expand Down
Loading
Loading