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

An open pull's page renders in under 20 ms: its `render` phase is 60–70 ms today because reads hide inside it #8404

Merged opened this

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

Merged

An open pull's page now does all its reads before `render`, and each read reports as its own Server-Timing phase: `session, diff, checks, tells, preview, head, gate, decide, looks, reviews, render`. The issue named `src/web/pages/pull.ts`, but the page that answers `/:owner/:name/pulls/:number` is `changeConversation` in `src/web/studio/change.ts`, so that is where the change is.

shipped production is serving it deploy fb6f20c

opened this

An open pull's page now does all its reads before render, and each read reports as its own Server-Timing phase: session, diff, checks, tells, preview, head, gate, decide, looks, reviews, render. The issue named src/web/pages/pull.ts, but the page that answers /:owner/:name/pulls/:number is changeConversation in src/web/studio/change.ts, so that is where the change is.

What changed

  • src/web/studio/change.ts: every card that reads the database is built first, under a named phase:

- tells: the tell line. - preview: the Preview card. - head: the repo header, change head (step, next actor, crew line), tabs and palette. - gate: hold, approve-here, teams and the wall card. - decide: the decision, the vision preview, Take it back and the variants. - looks: the Look card. - reviews: reviews, the conversation and the composer with its crew note.

render is now only the HTML template plus page(). The markup, its order and the first paint are unchanged.

  • The slowest read is narrowed: to find this one pull's decision, the page used to work out every decision in the repository: up to 50 open pulls with their checks, holds, handovers and review waits, plus pauses, parked issues, planner declines, pending waves and assignments. decisionsFor and scoredFor now take an optional pull, and askCandidatesFor uses it to look only at that pull. It still filters from the same 50-pull list, so the page reaches the same decision as before. The asks that belong to no pull (pauses, parked issues, declines, waves) moved unchanged into repoAsksInto, which the page no longer calls. The home and asks lists call it exactly as before.
  • Before/after (local seed, open pull with a check, a preview deploy and a look):
phasebeforeafter
render8–12 ms~4 ms
diff~1.3 ms~1.3 ms
checks~0.9 ms~0.9 ms

The 8–12 ms is now split across head ~3, decide ~2.5, reviews ~1–3.7 and under 0.5 ms each for the rest. I could not get production numbers: the lab app's small database doesn't reproduce the forge's 60 ms. The issue's target of under 60 ms total read on the forge's own data is still unmeasured. Read it from the new phases once this is live.

Tests

  • New, test/pull-page-phases.test.ts: seeds an open pull on an app with a check, a preview deploy and a look. It asserts the phase names in order, that they add up to no more than app, and that the Look, Preview and check word are drawn. It also asserts the median render over five requests is under 20 ms. I used the median because a single request can spike: one local run showed 27.9 ms.
  • Changed, test/pull-timing.test.ts: one expectation lists the new phases. It pinned the exact old five-phase list, so the issue's two asks ("new phases" and "existing tests unchanged") can't both hold. No other test was edited.
  • Run: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts are clean. 64 pull/studio/decision test files: 489 of 490 passed. The one failure, pull-merge-states-cache.test.ts, hit its 5 s timeout under parallel load and passes on its own. 40 home/asks test files covering pauses, parked issues, declines and waves: all 577 passed. scripts/fast.ts named 1,247 files (too broad), so I picked these instead.

Left out: page() itself (nav and session chrome, shared by every page) is still counted in render. It's part of the ~4 ms.

To try it, open any open pull on the preview and read Server-Timing in devtools.

🤖 Generated with Claude Code

Closes #8389

commented

An open pull's page now does all its reads before render, and each read reports as its own Server-Timing phase: session, diff, checks, tells, preview, head, gate, decide, looks, reviews, render. The issue named src/web/pages/pull.ts, but the page that answers /:owner/:name/pulls/:number is changeConversation in src/web/studio/change.ts, so that is where the change is.

What changed

  • src/web/studio/change.ts: every card that reads the database is built first, under a named phase:

- tells: the tell line. - preview: the Preview card. - head: the repo header, change head (step, next actor, crew line), tabs and palette. - gate: hold, approve-here, teams and the wall card. - decide: the decision, the vision preview, Take it back and the variants. - looks: the Look card. - reviews: reviews, the conversation and the composer with its crew note.

render is now only the HTML template plus page(). The markup, its order and the first paint are unchanged.

  • The slowest read is narrowed: to find this one pull's decision, the page used to work out every decision in the repository: up to 50 open pulls with their checks, holds, handovers and review waits, plus pauses, parked issues, planner declines, pending waves and assignments. decisionsFor and scoredFor now take an optional pull, and askCandidatesFor uses it to look only at that pull. It still filters from the same 50-pull list, so the page reaches the same decision as before. The asks that belong to no pull (pauses, parked issues, declines, waves) moved unchanged into repoAsksInto, which the page no longer calls. The home and asks lists call it exactly as before.
  • Before/after (local seed, open pull with a check, a preview deploy and a look):
phasebeforeafter
render8–12 ms~4 ms
diff~1.3 ms~1.3 ms
checks~0.9 ms~0.9 ms

The 8–12 ms is now split across head ~3, decide ~2.5, reviews ~1–3.7 and under 0.5 ms each for the rest. I could not get production numbers: the lab app's small database doesn't reproduce the forge's 60 ms. The issue's target of under 60 ms total read on the forge's own data is still unmeasured. Read it from the new phases once this is live.

Tests

  • New, test/pull-page-phases.test.ts: seeds an open pull on an app with a check, a preview deploy and a look. It asserts the phase names in order, that they add up to no more than app, and that the Look, Preview and check word are drawn. It also asserts the median render over five requests is under 20 ms. I used the median because a single request can spike: one local run showed 27.9 ms.
  • Changed, test/pull-timing.test.ts: one expectation lists the new phases. It pinned the exact old five-phase list, so the issue's two asks ("new phases" and "existing tests unchanged") can't both hold. No other test was edited.
  • Run: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts are clean. 64 pull/studio/decision test files: 489 of 490 passed. The one failure, pull-merge-states-cache.test.ts, hit its 5 s timeout under parallel load and passes on its own. 40 home/asks test files covering pauses, parked issues, declines and waves: all 577 passed. scripts/fast.ts named 1,247 files (too broad), so I picked these instead.

Left out: page() itself (nav and session chrome, shared by every page) is still counted in render. It's part of the ~4 ms.

To try it, open any open pull on the preview and read Server-Timing in devtools.

🤖 Generated with Claude Code

Changes requested — before the latest push

The restructuring is sound and I found no security or design-system problem: the markup and its order are unchanged, nothing new reaches the page's auth, and only filters the same 50-pull list. Three things to fix in one round.

  1. (First) The narrowing has no test. decisionsFor(..., {pull}) / scoredFor(..., only) is the real speed-up and the riskiest change, yet no test would fail if only were ignored or if it dropped a decision. Add a test that scoredFor(db, user, repos, now, pullId) returns exactly the asks the full read returns for that pull (a handover or hold, and an assigned row), and none for other pulls or the repo-level asks (pause, parked issue).
  2. The render < 20 median assertion is a wall-clock check on shared CI. The pull itself saw a 27.9 ms spike locally. The direction says tests never flake. Assert structure (phases named, ordered, summing to <= app) and keep the budget out of the suite, or give it a generous bound on a larger sample.
  3. Cleanup: export const PHASES in the test file is exported for nothing, and pull-timing.test.ts repeats the same list by hand. Make it a plain const, and share one list between the two tests, or have one of them not pin the list. Also wrap the new over-long lines in change.ts (decisionsFor(...) and the tick(spent, "preview"...) line) as the surrounding code does.

Note: the preview is the lab app and has no pull to open, so I could not read Server-Timing there. I judged from the diff and the description. The under-20 ms target on the forge's own data is, as the author says, still unmeasured.

commented

An open pull's page now does all its reads before render, and each read reports as its own Server-Timing phase: session, diff, checks, tells, preview, head, gate, decide, looks, reviews, render. The page that answers /:owner/:name/pulls/:number is changeConversation in src/web/studio/change.ts, not src/web/pages/pull.ts as the issue says, so that is where the change is.

What changed

  • src/web/studio/change.ts: every card that reads the database is built first, under a named phase:

- tells: the tell line. - preview: the Preview card. - head: the repo header, change head (step, next actor, crew line), tabs and palette. - gate: hold, approve-here, teams and the wall card. - decide: the decision, the vision preview, Take it back and the variants. - looks: the Look card. - reviews: reviews, the conversation and the composer with its crew note.

render is now only the HTML template plus page(). The markup, its order and the first paint are unchanged.

  • The slowest read is narrowed: to find this one pull's decision, the page used to work out every decision in the repository: up to 50 open pulls, plus pauses, parked issues, planner declines, pending waves and assignments. decisionsFor and scoredFor now take an optional pull, and askCandidatesFor uses it to look only at that pull. It still filters from the same 50-pull list, so the page reaches the same decision as before. The asks that belong to no pull moved unchanged into repoAsksInto, which the page no longer calls. The home and asks lists call it exactly as before.
  • Before/after (local seed, open pull with a check, a preview deploy and a look):
phasebeforeafter
render8–12 ms~4 ms
diff~1.3 ms~1.3 ms
checks~0.9 ms~0.9 ms

The 8–12 ms is now split across head ~3, decide ~2.5, reviews ~1–3.7 and under 0.5 ms each for the rest. The issue's targets on the forge's own data (render under 20 ms, total read under 60 ms) are still unmeasured: the lab preview has no such pull to time. Read them from the new phases once this is live.

Changes from review

  • The narrowing has a test, test/asks-one-pull.test.ts: it seeds two green pulls, a pull assigned to the viewer, a pull the viewer holds and a parked issue. For each pull, the narrowed scoredFor must return exactly the ids and scores the full read gives that pull, and nothing else. The full read must contain the parked issue so the test checks something real. With only ignored, the test fails.
  • No wall-clock bound in the suite: test/pull-page-phases.test.ts now checks only the structure: the phases are named, in order, and add up to no more than app. It also checks that the seeded Look, Preview and check word are drawn. The 20 ms budget is left to production's Server-Timing.
  • One phase list: both pull page tests now take the list from test/helpers/pull-phases.ts instead of an exported constant and a copy written out by hand.
  • Line wrapping: the long decisionsFor(...), preview and look lines in change.ts are wrapped like the code around them.

Tests

  • test/pull-timing.test.ts changed for a reason worth reviewing: it pinned the exact old five-phase list, so adding phases had to change that expectation, and its title now matches.
  • bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts are clean.
  • Before the first push: 64 pull/studio/decision test files passed 489 of 490. The one failure, pull-merge-states-cache.test.ts, hit its 5 s timeout under parallel load and passes on its own. 40 home/asks files covering pauses, parked issues, declines and waves passed all 577.
  • After this round: the new asks test, both pull page timing tests, studio-changes and asks-bulk pass, 53 of 53.

Left out: page() itself (nav and session chrome, shared by every page) is still counted in render. It's part of the ~4 ms.

To try it, open any open pull on the preview and read Server-Timing in devtools.

🤖 Generated with Claude Code

Approved

Earlier asks are met: the narrowing now has test/asks-one-pull.test.ts (narrowed scoredFor equals the full read per pull, and the full read holds the parked issue); the wall-clock bound is gone from the phases test; the phase list lives once in test/helpers/pull-phases.ts; the long lines are wrapped. The phases are timed without overlap, render is the remainder, and markup and order are unchanged, so no design-system or security surface moved. The preview answers 200. Next, non-blocking: (1) read the new Server-Timing phases on production once live and open a follow-up if render or the total read is still over the 20/60 ms target; (2) move page() chrome out of render so the remaining ~4 ms is attributable.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs