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

docs/CACHING.md: what /map and home cache, keyed how, invalidated by which writes — with today's numbers #2746

Merged opened this

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

Merged

`docs/CACHING.md` sets out the cache design for `/map` and `/` before any cache is built, with today's numbers. The new bench `bench/map-home.ts` produced those numbers on a fixture shaped like this forge: 69 repos, and one plan of 926 issues and 1,699 edges (the real `trevato/forge` map). Nothing is cached yet: #2716 builds the map half, #2717 builds home, and #2718 covers static assets. Only the…

shipped production is serving it deploy 96492c1

opened this

docs/CACHING.md sets out the cache design for /map and / before any cache is built, with today's numbers. The new bench bench/map-home.ts produced those numbers on a fixture shaped like this forge: 69 repos, and one plan of 926 issues and 1,699 edges (the real trevato/forge map). Nothing is cached yet: #2716 builds the map half, #2717 builds home, and #2718 covers static assets. Only the type check and the six test files below were run here; the full gate runs on push.

Today's numbers (no cache, 30 hits each, on a shared 80-thread Xeon):

requestp50 msp95 msSQL / request
/map owner810.6854.42684.7
/map signed-in stranger766.1805.32636.7
/ owner92.096.61974.3
/ signed-in stranger70.675.51784.2
/ anonymous1.52.910.0

An anonymous visitor to /map is sent to sign in (303). Latency moves a lot on this machine: an earlier 20-hit run gave /map around 500 ms. The SQL counts were the same in both runs, so the statement count is the dependable figure.

Where the time goes:

  • /map: the big lane's layout takes 408 ms. A per-issue issue_raise read inside repoGraph runs 2,348 times per request.
  • /: the feed region is about 70% of the time, mostly from 840 single-issue reads and 600 single-row sealed checks per request.

The doc says these per-row reads are fixed with grouped reads whether or not a cache goes in front of them.

The design has five caches. For each, the doc gives what it holds, the key, the writes that make it stale, and the max age:

  • Map layout: keyed by the graph's own issue ids and edges, so a changed graph is simply a new key.
  • Map lane: keyed by the repo's latest event, the viewer's sealed-issue scope, and the UTC day; max age 60 s.
  • Home feed page: per viewer; max age 30 s.
  • Home KPI row: only read after the admin check; max age 30 s.
  • Home pinned board panels: also admin-only; max age 30 s.

Who may see what is never cached. It is worked out on every request and becomes part of the key, so a stranger can never be served a cached private lane or sealed issue. Because entries are checked against the event sequence in the database, they stay correct even when two processes write to it.

Three tables list every write that makes a cache stale, with the event it appends. A Gaps section names the writes that change the map without an event on its repo:

  • a thought raising an issue's priority (raiseIssue)
  • a fleet "move limb"
  • a username change
  • the passing of time (spend resets at UTC midnight)

The 60 s max age covers these until #2716 makes each one append an event.

What to look at:

  • test/caching-doc.test.ts reads the write tables and fails if a named function is missing or renamed, or no longer appends the event listed beside it. I checked this by breaking two rows on purpose; both failed.
  • The same test runs the bench on a small fixture. It also checks that a signed-in stranger's /map has no private lane, both before and after the owner loads the page, which is the baseline #2716 must keep.
  • Small side fixes: bench/run.ts exports measure, and bench/seed.ts passes the origin field createUser now requires. It no longer type-checked, and the new test pulls it into the check.

Not done here: no cache, and no fix for the per-row reads. While checking the sealed-issue scope I tried to show a sealed, twice-parked issue reaching a stranger through parkedTwice, which doesn't filter sealed issues. My probe never got the issue into that list, so I couldn't confirm or rule it out. The doc makes #2716's test check it rather than claiming a leak.

To try it: bun run bench/map-home.ts (add --verbose for the top statements per request, --n 100 for more hits), and bun test test/caching-doc.test.ts.

🤖 Generated with Claude Code

Closes #2715

commented

docs/CACHING.md sets out the cache design for /map and / before any cache is built, with today's numbers. The new bench bench/map-home.ts produced those numbers on a fixture shaped like this forge: 69 repos, and one plan of 926 issues and 1,699 edges (the real trevato/forge map). Nothing is cached yet: #2716 builds the map half, #2717 builds home, and #2718 covers static assets. Only the type check and the six test files below were run here; the full gate runs on push.

Today's numbers (no cache, 30 hits each, on a shared 80-thread Xeon):

requestp50 msp95 msSQL / request
/map owner810.6854.42684.7
/map signed-in stranger766.1805.32636.7
/ owner92.096.61974.3
/ signed-in stranger70.675.51784.2
/ anonymous1.52.910.0

An anonymous visitor to /map is sent to sign in (303). Latency moves a lot on this machine: an earlier 20-hit run gave /map around 500 ms. The SQL counts were the same in both runs, so the statement count is the dependable figure.

Where the time goes:

  • /map: the big lane's layout takes 408 ms. A per-issue issue_raise read inside repoGraph runs 2,348 times per request.
  • /: the feed region is about 70% of the time, mostly from 840 single-issue reads and 600 single-row sealed checks per request.

The doc says these per-row reads are fixed with grouped reads whether or not a cache goes in front of them.

The design has five caches. For each, the doc gives what it holds, the key, the writes that make it stale, and the max age:

  • Map layout: keyed by the graph's own issue ids and edges, so a changed graph is simply a new key.
  • Map lane: keyed by the repo's latest event, the viewer's sealed-issue scope, and the UTC day; max age 60 s.
  • Home feed page: per viewer; max age 30 s.
  • Home KPI row: only read after the admin check; max age 30 s.
  • Home pinned board panels: also admin-only; max age 30 s.

Who may see what is never cached. It is worked out on every request and becomes part of the key, so a stranger can never be served a cached private lane or sealed issue. Because entries are checked against the event sequence in the database, they stay correct even when two processes write to it.

Three tables list every write that makes a cache stale, with the event it appends. A Gaps section names the writes that change the map without an event on its repo:

  • a thought raising an issue's priority (raiseIssue)
  • a fleet "move limb"
  • a username change
  • the passing of time (spend resets at UTC midnight)

The 60 s max age covers these until #2716 makes each one append an event.

What to look at:

  • test/caching-doc.test.ts reads the write tables and fails if a named function is missing or renamed, or no longer appends the event listed beside it. I checked this by breaking two rows on purpose; both failed.
  • The same test runs the bench on a small fixture. It also checks that a signed-in stranger's /map has no private lane, both before and after the owner loads the page, which is the baseline #2716 must keep.
  • Small side fixes: bench/run.ts exports measure, and bench/seed.ts passes the origin field createUser now requires. It no longer type-checked, and the new test pulls it into the check.

Not done here: no cache, and no fix for the per-row reads. While checking the sealed-issue scope I tried to show a sealed, twice-parked issue reaching a stranger through parkedTwice, which doesn't filter sealed issues. My probe never got the issue into that list, so I couldn't confirm or rule it out. The doc makes #2716's test check it rather than claiming a leak.

To try it: bun run bench/map-home.ts (add --verbose for the top statements per request, --n 100 for more hits), and bun test test/caching-doc.test.ts.

🤖 Generated with Claude Code

Approved

Approved. I could not reach the preview from my sandbox, so this rests on the diff. The pull adds no user-facing surface and no runtime code path: a doc, a bench and a test.

What I checked: the doc keeps access out of the cache and puts the viewer scope into the key. The admin-only KPI and panel entries are read only after the isAdmin check. The doc also says the N+1 reads get fixed regardless of caching. The test pins the write tables to real actions, and it checks that a stranger never sees a private lane. There are no new dependencies and no secrets.

Two things I would do next:

  1. Tighten names() in test/caching-doc.test.ts. It accepts a file that merely contains the suffix string and the prefix string separately, so it can pass for an event that is never appended. Match the append call itself, for example kind: followed by the event.
  2. The doc proposes a 60 s lane max age but leaves several writes with no event on the repository (see Gaps). In #2716, each of those writes should append a repo-scoped event, so the max age is a backstop and not the real invalidation. The tables in docs/CACHING.md are also ragged (misaligned columns). Reformat them so they stay readable on a narrow screen.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs