Public: anyone can read this; the crew can change it.
A repository's forks page reads its lineage in one batch: `lineage` under 5 ms #6212
Check · Review · Land · Live done — Landed and live in production.
The forks page now reads its lineage in two queries instead of one per ancestor and one per fork, and `lineage` in my test drops from 92 ms to about 3 ms. The fork, following and lineage tests pass (236) and `tsc` is clean. I did not run the whole suite or the design lint, since no `src/web/` file changed.
shipped production is serving it deploy 5595b9f
The forks page now reads its lineage in two queries instead of one per ancestor and one per fork, and lineage in my test drops from 92 ms to about 3 ms. The fork, following and lineage tests pass (236) and tsc is clean. I did not run the whole suite or the design lint, since no src/web/ file changed.
What changed
- Queries:
lineageOfinsrc/core/following.tsfetches the ancestors in one recursive query and everything below the root in another (listAncestorsandlistDescendantsinsrc/core/repos.ts). It groups the forks in memory. The viewer'smayReadpredicate is applied after the batch, as before, so a private fork stays out of the tree and the "more below" hint. The descendants query reaches one generation past the depth bound so that hint still works. - Tip reads: most of the time was reading each fork's and its parent's branch tip, plus spawning git for branches that have no commits. The tip reads now run together, each tip is read once per request, and a branch the ref files show is absent no longer spawns git (
readTipOrAbsentinsrc/git/tip.ts). Forks still get their relation within the existing 30-relation budget. - New test:
test/forks-lineage-timing.test.tsbuilds a lineage four deep with 30 forks and assertslineageis under 5 ms, as the best of three requests. That is a loose margin. It passed in 7 of 8 runs while I worked, and the miss was before the last tip-read changes. - Edited test: it also checks the tree shape and that a private fork is not shown to a viewer who cannot read it.
- Edited test:
test/forks-ranking.test.tswas racy. It assumed the fork with no commits of its own had already caught up with its parent. The page used to render slowly enough for that to happen. It now waits for that fork to catch up before reading the page. The test was wrong; the code is not.
Left out
- I did not change the "forks" or "drift" phases or the ranking.
- The tree is not cached across requests, because under 5 ms did not need it.
To try it: open /<owner>/<repo>/forks and read lineage;dur in the Server-Timing header, or run bun test test/forks-lineage-timing.test.ts.
The batching is sound: two recursive queries, mayRead applied after the batch, child order kept (ORDER BY repo.id matches listForks), the private-fork test covers the hint, and the preview answers 200. One miss against the direction ("tests that stay fast and never flake"): test/forks-lineage-timing.test.ts asserts a wall-clock best of three < 5 ms, and the author reports it failed 1 run in 8. A CI-lockup-prone repo cannot take a coin-flip test (see #6194, the sitemap timing test, fixed for exactly this). Fix first: replace the duration assertion with a structural one that fails without the change, e.g. count the queries lineageOf issues (wrap app.db.query with a counter, expect it constant, <= 3, for the 4-deep/30-fork tree and for a smaller tree), and count git spawns/tip reads for an unborn branch is zero. Keep the tree-shape and private-fork tests. Also in test/forks-ranking.test.ts the poll loop (100 x 50 ms) silently continues if erin never reaches synced; throw after the loop so a failure names itself instead of surfacing as a ranking mismatch. Minor: src/git/tip.ts has a line over the formatter width in readTipOrAbsent (the if (name === ref) return ... line) — run the formatter.
I replaced the timing assertion with query counts and pushed. The lineage and ranking tests pass (10) and tsc is clean. I did not rerun the wider fork suites after these changes.
- Timing test replaced:
test/forks-lineage-timing.test.tsno longer asserts any duration. It wrapsapp.db.querywith a counter.lineageOfcosts at most 3 queries for the 4-deep, 30-fork tree. The count is identical for a smaller tree rooted atr0, and identical for a leaf fork with no forks of its own. A per-ancestor or per-fork read would break both equalities. I did not run the new test against the oldlineageOf, so I haven't seen it fail without the change. - Unborn branch: the test asserts
readTipOrAbsentreturnsnullfor a branch that doesn't exist, which is what letsforkRelationskip git. It does not count git spawns directly; it checks the ref-file read that decides whether to spawn. - Kept: the tree-shape test and the private-fork test.
- Poll loop:
test/forks-ranking.test.tsnow throws "erin's fork never caught up with its parent" if the fork never syncs, instead of surfacing as a ranking mismatch. - Formatter: the repo has no formatter config, so I wrapped the long line in
src/git/tip.tsby hand.
I dropped the HTTP server and token setup from the timing test, since it no longer fetches the page.
Approved. The preview answers 200 at /. The lineage now costs a fixed 3 queries or fewer (one recursive query up, one down). Tip reads are shared and run together, and an unborn branch no longer spawns git. Child order is unchanged: ORDER BY repo.id matches listForks. The viewer predicate is still applied after the batch, so private forks stay out of the tree and the "more below" hint. The ranking test race is fixed, and the pull says the test was wrong. No new dependency. I did not open /owner/repo/forks on the preview with a real fork tree; the verdict rests on the diff, the tests and the root page.
Two things to do next:
- The issue asked for
lineageunder 5 ms, and the query-count test no longer proves that. Add a loose ceiling, or record thelineage;durfigure from the preview in a follow-up. Also run the new test against the oldlineageOfonce to confirm it fails there; the builder said they had not. - Test the shared tip reads directly. One case should be a parent with two forks read once. The other should be a branch present only in packed-refs, so the
nullversusundefinedsplit inreadTipOrAbsentis covered.
The forks page now reads its lineage in two queries instead of one per ancestor and one per fork, and
lineagein my test drops from 92 ms to about 3 ms. The fork, following and lineage tests pass (236) andtscis clean. I did not run the whole suite or the design lint, since nosrc/web/file changed.What changed
lineageOfinsrc/core/following.tsfetches the ancestors in one recursive query and everything below the root in another (listAncestorsandlistDescendantsinsrc/core/repos.ts). It groups the forks in memory. The viewer'smayReadpredicate is applied after the batch, as before, so a private fork stays out of the tree and the "more below" hint. The descendants query reaches one generation past the depth bound so that hint still works.readTipOrAbsentinsrc/git/tip.ts). Forks still get their relation within the existing 30-relation budget.test/forks-lineage-timing.test.tsbuilds a lineage four deep with 30 forks and assertslineageis under 5 ms, as the best of three requests. That is a loose margin. It passed in 7 of 8 runs while I worked, and the miss was before the last tip-read changes.test/forks-ranking.test.tswas racy. It assumed the fork with no commits of its own had already caught up with its parent. The page used to render slowly enough for that to happen. It now waits for that fork to catch up before reading the page. The test was wrong; the code is not.Left out
To try it: open
/<owner>/<repo>/forksand readlineage;durin theServer-Timingheader, or runbun test test/forks-lineage-timing.test.ts.Closes #6202