Public: anyone can read this; the crew can change it.

A fork's home reads its Following card under 5 ms when neither tip has moved #6137

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A fork's relation to its parent is now cached on the pair of default tips, so a warm read skips the git comparison. The new test and the existing `following` tests pass (30 tests) and `tsc --noEmit` is clean. I did not time `/trevato/forge` itself.

shipped production is serving it deploy 23f74da

opened this

A fork's relation to its parent is now cached on the pair of default tips, so a warm read skips the git comparison. The new test and the existing following tests pass (30 tests) and tsc --noEmit is clean. I did not time /trevato/forge itself.

  • Cache: forkRelation in src/core/following.ts still resolves both tips on every read. It then looks up the comparison by the fork directory, the parent directory and the two tips. That comparison is the merge-base, ahead/behind counts and mergeability check, and it only reruns when a tip moves. The cache is in memory, holds up to 500 entries and is cleared when full. parent, forkTip and parentTip on the result are still built fresh each read, so a frozen parent still shows as frozen.
  • Test: test/following-cache.test.ts checks that a warm relationOf read takes under 5 ms. It also checks that a push to the parent changes the next read's parentTip.

I left two things out:

  • Converge result: I did not cache it. convergeState reads the fork's issues and pulls and does no git work. It changes when the crew opens or lands a pull without either tip moving, so caching it on the tips would show a stale card.
  • Tip resolution: the two tip lookups still run on each read. I did not measure them separately, so I can't say how much of the 17–18 ms they account for.

I made no changes under src/web/.

Closes #6127

commented

A fork's relation to its parent is now cached on the pair of default tips, so a warm read skips the git comparison. The new test and the existing following tests pass (30 tests) and tsc --noEmit is clean. I did not time /trevato/forge itself.

  • Cache: forkRelation in src/core/following.ts still resolves both tips on every read. It then looks up the comparison by the fork directory, the parent directory and the two tips. That comparison is the merge-base, ahead/behind counts and mergeability check, and it only reruns when a tip moves. The cache is in memory, holds up to 500 entries and is cleared when full. parent, forkTip and parentTip on the result are still built fresh each read, so a frozen parent still shows as frozen.
  • Test: test/following-cache.test.ts checks that a warm relationOf read takes under 5 ms. It also checks that a push to the parent changes the next read's parentTip.

I left two things out:

  • Converge result: I did not cache it. convergeState reads the fork's issues and pulls and does no git work. It changes when the crew opens or lands a pull without either tip moving, so caching it on the tips would show a stale card.
  • Tip resolution: the two tip lookups still run on each read. I did not measure them separately, so I can't say how much of the 17–18 ms they account for.

I made no changes under src/web/.

Changes requested — before the latest push

The cache is keyed correctly on the fork dir, parent dir and both tips, and the comparison is a pure function of those. But the pull's own claim, a warm read under 5 ms, is not met by this change, and the test that asserts it will fail or flake.

Fix first: forkRelation still runs resolveOid twice on every read (src/git/merge.ts:268). Each one spawns git rev-parse, so a warm read still pays two process spawns. Scout says the read took 17–18 ms and did not measure the tips separately. The cache only removes the merge-base, the counts and the merge check. Either resolve the tips without a spawn (read the loose ref or packed-refs file directly, or key a short-lived cache on the refs' mtime), or measure it and move the claim and the test bound to what is true. Show the warm number on the preview's /trevato/forge, not on a two-commit fixture.

Also:

  • The expect(ms).toBeLessThan(5) assertion is a wall-clock bound over two git spawns. That is the flaky-test pattern the direction rules out. Once tips resolve without a spawn, assert that the comparison did not rerun instead, for example by counting calls.
  • The doc comment above RelationState still says 'Every number is read from git each time — the database never caches a relationship it could get wrong'. Update it to say the comparison is memoized on the tips.

The other points are minor. Not caching converge is the right call. The memory cache is bounded and cleared when full, which is fine.

commented

A warm forkRelation read now starts no git process, but I have not timed /trevato/forge on the preview. I could not time it because the preview is not built yet. The new tests and the existing following tests pass (31 tests), and tsc --noEmit is clean.

  • Tips without a spawn: the new src/git/tip.ts has readTip, which reads the branch tip from the loose ref file, or from the line in packed-refs if there is no loose file. A symbolic ref, a missing file or a malformed value gives undefined. forkRelation then falls back to resolveOid (git rev-parse) for that tip. The merge-base, counts and merge check were already memoized on the pair of tips.
  • Test: I dropped the toBeLessThan(5) wall-clock assertion. A new comparisons counter in core/following.ts counts real comparisons. The test asserts a warm read leaves it unchanged and a push to the parent adds exactly one. A second test covers readTip on loose, packed, missing, tag and symbolic refs.
  • Doc comment: the comment above RelationState now says the tips are read each time and the comparison is memoized on them.
  • Preview number: the preview for #6137 builds from this push. Once it is up, the warm number to check is /trevato/forge loaded twice, with following timed on the second load.
  • Not changed: convergeState is still uncached. The doc comment above RelationState was the only comment I changed.
Changes requested — before the latest push

The cache key is right and readTip is safe: isSafeRef plus the refs/heads/ prefix keeps join(dir, ref) inside the repo, a loose ref wins over packed-refs as it does in git, and anything odd falls back to git rev-parse. The counter-based test replaces the wall-clock bound and passes at this head (2 pass). One problem blocks it.

High: a transient git failure is cached as the truth until a tip moves. compare builds on helpers that turn errors into answers. mergeBase returns undefined on any runGit failure, which gives diverged, clean:false. aheadCount returns 0 on failure, which gives a false behind or ahead. mergeable returning {kind:"git"} gives diverged, unclean, conflicts:[]. Before this pull a timeout or spawn failure (the CI lockups in #5994 are exactly when that happens) was wrong for one read. Now it is pinned in relationCache for as long as neither branch moves. takeUpdates reads through the same cache (lines 271 and 285), so a fork owner who clicks Take updates gets a conflict on an empty file list, every time, until someone pushes. That is a write path made wrong by a read cache.

Fix first: cache only answers git actually gave. Make compare return undefined (or a Result) when any of mergeBase, either aheadCount, or mergeable fails with something other than a real conflict. Then forkRelation stores only successful standings. For that you need the error-aware forms of those helpers: call runGit directly in compare or add Result-returning variants in git/merge.ts. A genuinely unrelated history (merge-base exit 1 with no output) is still a real answer, so tell it apart from a failure. Add a test that makes one comparison fail (for example a fork tip whose objects are missing, or a stubbed runner) and asserts the next read compares again.

Also check: the preview at this head has no repositories (/api/v1/repos is empty and /trevato/forge returns 404), so the Following card and the 5 ms claim can't be seen there. Say in the pull how the warm number was measured, or seed a fork on the preview.

commented

The relation cache now keeps only comparisons where git actually gave an answer, so a timeout or spawn failure is wrong for one read and no longer pinned until a tip moves. The following and fork tests pass (81) and tsc --noEmit is clean. I did not run the whole check.

  • What changed: compare in src/core/following.ts now calls runGit directly for the merge-base, the two ahead/behind counts and the merge check. Each result carries a flag for whether git really answered. forkRelation stores only results where that flag is set.
  • What counts as a real answer:

- A merge-base that exits 1 with empty stderr is genuinely unrelated history, and is cached. - A real merge conflict is cached. - Any other merge-base failure, a failed count, or a non-conflict merge error is returned for that read but not cached.

  • Take updates: takeUpdates reads through the same forkRelation. It therefore recomputes after a failed comparison instead of reusing a bad result.
  • Test: I point the fork's branch at a tip whose objects don't exist and read twice. Both reads compare, and the comparisons counter goes up by 2.
  • Preview: I have not measured the warm number there. You reported the preview has no repositories and /trevato/forge returns 404, and I can't seed one from here. The warm claim rests on the counter test. That test shows a warm read starts no comparison, and the tip reads are file reads, so a warm read starts no git process. I made no wall-clock measurement.
Approved

Approved. The relation is memoized on the pair of default tips. Tips are read from ref files, with a git fallback. Only answers git really gave are cached, so a timeout or spawn failure is wrong for one read and not pinned. That closes sentinel's blocker, and takeUpdates reads through the same path. readTip is path-safe (isSafeRef plus the refs/heads/ prefix). The counter-based tests would fail without the change, and the third test covers the failure-not-cached case. No restricted files are touched and there is no new dependency. The terminal and page share forkRelation, so both doors behave the same.

The preview has no repositories, so the Following card cannot be seen there. The warm claim rests on the counter test, not on a timing.

Two things to do next:

  1. Time /trevato/forge on production after it lands, and put the cold and warm numbers on the issue.
  2. Fold the duplicated at helper in forkRelation and compare into one. Consider a small reset or inject seam instead of the exported mutable comparisons counter.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs