Skip to content

feat(ppr): capture scheduling telemetry, bake-cost attribution, inert-store guard - #803

Merged
ivogt merged 2 commits into
mainfrom
feat/ppr-capture-operability
Jul 18, 2026
Merged

ivogt merged 2 commits into
mainfrom
feat/ppr-capture-operability

Conversation

@ivogt

@ivogt ivogt commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Problem

Three operability gaps around the PPR capture queue, all surfaced by the #801 incident:

  1. A dropped capture didn't explain itself. skip-queue-timeout reported only how long the capture waited — not its priority class or how many captures were ahead when it enqueued. Diagnosing the prefetch-starvation incident required a trace deep-dive that these two numbers would have answered immediately.
  2. Bake cost was invisible. By the hole doctrine, top-level pushed handle promises and bake-lane loader containers are awaited at capture. A route with a 5s handle + 2s Meta chain pays ~7s of serialized queue occupancy on EVERY capture — while its served response reads 13ms TTFB, because captures are detached. No timing anywhere showed this.
  3. Inert stores burned full captures. A CFCacheStore without a KV namespace warns that its shell family is a no-op — but the scheduler still ran a complete background render per ppr MISS whose putShell could only discard the result, occupying the queue for nothing.

Before vs. after

Debug event for a queue-dropped capture:

# before
skip-queue-timeout queue-wait=15000ms
# after
skip-queue-timeout queue-wait=15000ms queue-priority=document queue-ahead=4

Stored attempt on a promise-heavy route (and, in dev past 2s, a once-per-key console warning naming the source):

# before
stored attempt=1 7150ms prelude=48213b
# after
stored attempt=1 7150ms prelude=48213b bake=6987ms

KV-less CFCacheStore + ppr route, per MISS:

# before: full background render runs, putShell no-ops, once-per-isolate warning
# after:  skip-inert-store — the render is never scheduled

Consumer-visible surface

  • ShellCaptureDebugEvent: new optional fields queuePriority, queueAhead, bakeWaitMs; new outcome skip-inert-store. Additive — existing sinks are unaffected.
  • rango.background span: new rango.background.queue_priority and rango.background.queue_ahead attributes, set at enqueue.
  • SegmentCacheStore.shellFamilyInert?: boolean — a store can declare its shell family present-but-inert; CFCacheStore sets it when constructed without KV. Custom stores that no-op their shell family under some configuration should declare it to get the same scheduling skip.
  • Dev-only, once per key: a console warning when the slowest bake source held the capture gate ≥ 2s, naming the source (top-level pushed handle promises / bake-lane segment loader "<id>") and the remedy ladder — nest it ({ data: promise } → per-request hole), cache() it (capture replays the value), or loading() it (live lane).

End-to-end consumer example

// urls.tsx — the shape from the incident: a 5s promise pushed as a top-level handle
handler: (ctx) => {
  const stats = ctx.use(StatsHandle);
  stats.push(loadDashboardStats());        // 5s, awaited and BAKED at capture
  return <Dashboard />;
}

Dev console after the first capture:

[rango] Shell capture for "host/dashboard:shell" waited 5012ms for top-level
pushed handle promises to settle before the shell could freeze. This cost
recurs on every capture of the route and occupies the per-isolate capture
queue; the served response never shows it. … nest it ({ data: promise }) …
wrap the work in cache() … add a loading() boundary …

Semantics

Case Before After
Queue-dropped capture (skip-queue-timeout) wait ms only + priority class and backlog at enqueue (event + span)
Stored capture, bake sources settled fast (<1ms) — unchanged; no bakeWaitMs reported (noise floor)
Stored capture, slow bake source silent bakeWaitMs on event; dev warning past 2s, once per key
Refused/no-shell capture own warnings unchanged — bake fold runs only on success
Store without putShell full render, silent skipped write skip-inert-store, render never scheduled
Store with shellFamilyInert: true (KV-less CFCacheStore) full render + once-per-isolate warning same warning on reads, but captures skip at the gate
Viable store scheduled unchanged
Production builds — warning code folded away (NODE_ENV guard); event/span fields remain

Code changes

  • src/rsc/capture-queue.ts — exported captureQueueDepths() (waiting counts per class + running flag; lazily-pruned timed-out entries excluded).
  • src/rsc/shell-capture.ts — inert-store gate in scheduleShellCapture (same store defaulting as the write site); priority/backlog span attributes + skip-event fields at enqueue; bake observers around holdUntil's sources (side-channel .then(record, record) — the gate contract is untouched); fold into stats.bakeWaitMs + warnBakeCostOnce (threshold SHELL_CAPTURE_BAKE_WARN_MS = 2000, dev-only, once per key); new event fields/outcome documented on ShellCaptureDebugEvent.
  • src/cache/types.ts — SegmentCacheStore.shellFamilyInert contract.
  • src/cache/cf/cf-cache-store.ts — declares shellFamilyInert: true when constructed without KV.
  • Docs: ppr skill (operability bullets + new "Designing routes for cheap captures" cost-model section), ppr-shell-resume.md, feature-map.md.

Test plan

  • New/extended unit tests, red-proven on base — 7 fail without the src change: captureQueueDepths counts (new export), skip-event queuePriority/queueAhead (undefined on base), both inert-skip tests (base schedules the capture), bake attribution (no bakeWaitMs, no warning), CF shellFamilyInert flag (undefined on base). One additional base failure ("hands off before the completed capture's caller can enqueue more work") is fallout of the red run itself — the failed depths test leaves the module queue locked — not an independent regression.
  • Bake test pins: slowest source wins, sub-ms handles ignored, warning names the loader id and contains all three remedies, once-per-key dedupe.
  • pnpm run typecheck (16/16), pnpm run test:unit:all, pnpm run lint, pnpm run format, pnpm check:docs-api (182 identifiers).
  • E2e dev + production (cloudflare-basic, workerd): "a document capture overtakes queued navigation-only captures" extended to assert queue_priority === "document" and queue_ahead >= 1 on the terminal span — passed in both modes.

Notes

  • queueAhead is a snapshot at enqueue (active capture + waiting same-or-higher priority), not a live gauge; queueWaitMs remains the measured truth.
  • The bake fold only runs on stored outcomes: refusals and no-shell attempts keep their existing dedicated warnings.
  • The warning threshold is a module constant (2s), deliberately not a public option — it is a diagnostic, not a policy knob.
  • Prefetch-aware admission (skipping prefetch-commissioned captures under pressure) remains deferred; with queue_ahead on the span there is now data to justify it if pressure recurs.

…-store guard

Three operability gaps from the #801 incident: a queue-dropped capture
reported only its wait (now: priority class + backlog at enqueue on the
skip event and the rango.background span); bake cost was invisible (now:
bakeWaitMs names the slowest gate-holding source on stored attempts, with
a once-per-key dev warning past 2s carrying the nest/cache()/loading()
remedy ladder); and a store whose shell family can only no-op (KV-less
CFCacheStore, declared via SegmentCacheStore.shellFamilyInert) still
burned a full background render per MISS (now: skip-inert-store at the
scheduling gate).
@pkg-pr-new

pkg-pr-new Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@803

commit: 6f2f2e9

…ability

# Conflicts:
#	packages/rangojs-router/docs/internal/feature-map.md
#	packages/rangojs-router/src/rsc/__tests__/shell-capture.test.ts
@ivogt
ivogt force-pushed the feat/ppr-capture-operability branch from 51172b6 to 6f2f2e9 Compare July 18, 2026 15:19
@ivogt
ivogt merged commit 6cdaa77 into main Jul 18, 2026
36 checks passed
@ivogt
ivogt deleted the feat/ppr-capture-operability branch July 18, 2026 15:30
ivogt added a commit that referenced this pull request Sep 23, 2026
… ssr:false

Only loader(Def, { ssr: false }) bakes at PPR capture since #813/#820. Update the capture diagnostics (bake cost, no usable shell, identity refusal, rejected loader, redirect/notFound refusal) and the cookies()/headers() capture error, which still advised loading() or nested promises. Share one PPR_LANE_HINT. Make resolveLoaderData the canonical statement of the lane rule and point other comments and docs at it. Fix stale JSDoc examples, resolve the #803 conflict leftover in feature-map.md, and register the /shell-cache/guard fixture loader with ssr: false so the identity guard is exercised again.
ivogt added a commit that referenced this pull request Sep 23, 2026
… ssr:false (#871)

Only loader(Def, { ssr: false }) bakes at PPR capture since #813/#820. Update the capture diagnostics (bake cost, no usable shell, identity refusal, rejected loader, redirect/notFound refusal) and the cookies()/headers() capture error, which still advised loading() or nested promises. Share one PPR_LANE_HINT. Make resolveLoaderData the canonical statement of the lane rule and point other comments and docs at it. Fix stale JSDoc examples, resolve the #803 conflict leftover in feature-map.md, and register the /shell-cache/guard fixture loader with ssr: false so the identity guard is exercised again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant