Skip to content

test(rendering): pin and instrument the retained capture-margin contract - #581

Merged
Exoridus merged 4 commits into
mainfrom
test/o45-cull-margin-contract
Aug 20, 2026
Merged

test(rendering): pin and instrument the retained capture-margin contract#581
Exoridus merged 4 commits into
mainfrom
test/o45-cull-margin-contract

Conversation

@Exoridus

Copy link
Copy Markdown
Owner

Ships the measurement work behind the retained cull-margin decision. The margin
constant is unchanged: 1/16 per view axis stays, and this PR is what makes that
choice checkable instead of remembered.

No production source changes. Tests, a sweep harness and opt-in bench arms only.

Why a miss is not a collect

A margin miss no longer rebuilds the scene. Measured at 100 000 nodes, r = 1/16,
camera speed 8, one cell per process, 400 timed frames after 80 warmup frames:

tier replay frame miss frame ratio
slot path (indexed re-selection) 0.018 ms 0.667 ms ~37x
capture path (slot path refused) 0.024 ms 5.47 ms ~230x

The slot-path miss is a spatial-index query over the grown rect plus a bitset
delta plus one slot write per arriving item. The capture tier is reached by any
root whose scope has mixed zIndex or a non-group sibling, which a z-sorted
scene qualifies for, and that is where the guard band still buys something large
(19.3 ms per miss at 100 000 nodes).

Both tiers agree that a miss costs more the wider the margin is: arrivals per
miss scale as r*(1 + 2r) while misses per frame fall as 1/r.

Where the knee is

Marginal amortized CPU per step, 100 000 nodes, speed 8:

step CPU mean improvement drawn/frame per-frame upload
0 -> 1/64 0.3339 -> 0.2034 -39% +6% +6%
1/64 -> 1/32 0.2034 -> 0.1314 -35% +6% +6%
1/32 -> 3/64 0.1314 -> 0.1072 -18% +5% +5%
3/64 -> 1/16 0.1072 -> 0.0987 -8% +5% +5%
1/16 -> 3/32 0.0987 -> 0.0925 -6% +10% +10%
3/32 -> 1/8 0.0925 -> 0.0869 -6% +10% +10%
1/8 -> 3/16 0.0869 -> 0.0712 -18% +17% +17%
3/16 -> 1/4 0.0712 -> 0.0774 +9% +17% +17%

Above 1/16 every further step buys at most 6-18% amortized CPU, all inside the
CPU stub's noise floor, while permanently adding 10-17% to the nodes drawn on
every frame and to the order-stream bytes uploaded on every frame.

The margin is not overfit to one scroll rate

100 000 nodes, slot tier, CPU mean:

margin speed 4 speed 8 speed 16
1/32 0.0950 0.1314 0.2592
1/16 0.0693 0.0987 0.1836
1/8 0.0707 0.0869 0.1204
1/4 0.0745 0.0774 0.0986

The optimum moves with scroll rate, as the geometry predicts. At the archetype's
design speed and at half of it, 1/16 is at or below the optimum, and 1/8 and
1/4 are worse at speed 4 while permanently drawing 56% / 125% more nodes. A
single fixed margin must not be tuned on the fastest case.

The WebGPU column that must not be cited

The 1 000 000-node WebGPU sweep produced a frameMsP95 of 122 ms at r = 1/4.
That number is a harness artefact, not a margin cost: it is the one-shot
persistent-slot bootstrap upload charged to steady-state timed frames by a
harness that ran its warmup unpaced and then read a cumulative
queue.onSubmittedWorkDone. The harness now drains the queue at the
warmup/timing boundary, which drops the worst sample on that cell from 141.6 ms
to 27.2 ms. The decision rests on the knee, the speed sensitivity, the WebGL2
hardware-timer p95 and the miss cost - none of which come from that column.

What ships

  • test/perf/rendering/cull-margin-correctness.test.ts,
    capture-kept-bounds.test.ts, cull-margin-probe-fidelity.test.ts and
    cullMarginProbe.ts pin the capture-margin contract and keep the probe honest
    about which tier served a frame.
  • run-cull-margin.ts / run-cull-margin-cell.ts plus
    perf:renderers:cull-margin and perf:renderers:cull-margin:cell make the
    sweep reproducible. One cell per process, for the reasons the allocation
    harness already documents.
  • Opt-in cull-margin-* bench arms, deliberately outside ADAPTER_CAPABILITIES:
    they answer an engine-tuning question, not a cross-library one, and enter the
    matrix only when --config names them.

Validation

  • pnpm verify:quick - all 16 gates pass
  • vitest --project=rendering-perf cull-margin capture-kept-bounds - 24 tests pass
  • pnpm perf:renderers:cull-margin smoke run on the current base: 17 replay / 3
    miss frames at r = 1/16, capture area 1.266x, as expected

Exoridus added 4 commits August 20, 2026 21:37
Adds the probe scene and the tests that pin what the retained cull margin
actually governs, so the constant is re-checkable rather than asserted.

cullMarginProbe.ts builds the scrolling-world scene against the real
WebGl2Backend, injects a margin by patching the builder's cull-rect
inflation, and accounts which retention tier served each frame.

The tests answer three questions empirically:

- cull-margin-correctness: 15 camera operations (steps, boundary steps,
  teleports, zoom, resize, rotation) each assert BOTH the tier taken and
  that the submitted set covers every leaf the view can see. Nothing in
  it encodes 1/16; distances come from the rect the builder culls
  against, so it survives a recalibration.
- capture-kept-bounds: _culledDuringCapture is correctness (a capture
  that dropped a node may not replay under a wider view), while
  _keptBounds/_keptEmpty are a performance tolerance. Both are now
  pinned, so neither can be deleted on the strength of an old solution
  sketch.
- cull-margin-probe-fidelity: pins the probe scene against the bench's
  own world.ts, so the two instruments cannot drift apart silently.

Claude-Session: https://claude.ai/code/session_017WTi386yzejLaT9UUJXfU1
One sweep cell per process (a retained product's cost is a property of
its whole life, so cells that share a process are not independent
samples), JSON on stdout, and a driver that folds the cells into a
sweep table plus a raw cells.jsonl.

Reachable as `pnpm perf:renderers:cull-margin`; the cell runs with
--expose-gc and a raised heap so the 100k/1M cells fit.

Claude-Session: https://claude.ai/code/session_017WTi386yzejLaT9UUJXfU1
`cull-margin-<n>_<d>` arms measure the retained cull margin on real
hardware without one source edit and one build per sweep point: the arm
patches the builder's private cull-rect inflation, which the harness
page can reach because it imports engine source through the `#` alias.
Every other arm, the production default included, restores the engine's
own constant on init.

The arms live in a separate capability list and enter the matrix only
when --config names them, so what a reportable run covers is unchanged.
They cover only archetypes with genuine off-screen content, since on a
fully visible scene the margin cannot change the selected set.

Claude-Session: https://claude.ai/code/session_017WTi386yzejLaT9UUJXfU1
@Exoridus
Exoridus enabled auto-merge (squash) August 20, 2026 19:48
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 396 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
site-server-esm 32.13MB 396 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: site-server-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
prerender-entry.iR5_aBs9.mjs (New) 253.92kB 253.92kB 100.0% 🚀
chunks/ExampleBrowser_G__VIVdM.mjs (New) 65.57kB 65.57kB 100.0% 🚀
chunks/AppShell_BSX0zlAZ.mjs (New) 50.87kB 50.87kB 100.0% 🚀
chunks/DocsSidebar_BQ0Ma-RX.mjs (New) 26.63kB 26.63kB 100.0% 🚀
chunks/HomePage_BReClYxm.mjs (New) 15.48kB 15.48kB 100.0% 🚀
chunks/ApiSymbolPage_DO4V62HM.mjs (New) 15.4kB 15.4kB 100.0% 🚀
chunks/GuideChapterPage_DzPfr7Jr.mjs (New) 13.12kB 13.12kB 100.0% 🚀
chunks/ApiAllPage_6YfZK6WO.mjs (New) 12.01kB 12.01kB 100.0% 🚀
chunks/GuideIndexPage_C0eFGX_o.mjs (New) 8.35kB 8.35kB 100.0% 🚀
chunks/ApiIndexPage_Beyguicn.mjs (New) 8.27kB 8.27kB 100.0% 🚀
chunks/index_B_Lafb94.mjs (New) 1.6kB 1.6kB 100.0% 🚀
chunks/index_D7lo0xtu.mjs (New) 1.6kB 1.6kB 100.0% 🚀
chunks/index_DwByJ54l.mjs (New) 1.43kB 1.43kB 100.0% 🚀
chunks/index_P86qYIqi.mjs (New) 1.43kB 1.43kB 100.0% 🚀
chunks/_symbol__EANAHhra.mjs (New) 1.4kB 1.4kB 100.0% 🚀
chunks/_symbol__MLqoZS2g.mjs (New) 1.4kB 1.4kB 100.0% 🚀
chunks/index_BZqa5Inj.mjs (New) 952 bytes 952 bytes 100.0% 🚀
chunks/index_DwXvd0W2.mjs (New) 952 bytes 952 bytes 100.0% 🚀
chunks/index_BjLONX0c.mjs (New) 930 bytes 930 bytes 100.0% 🚀
chunks/index_DyV7lwVg.mjs (New) 930 bytes 930 bytes 100.0% 🚀
chunks/all_B8mJ4qEI.mjs (New) 914 bytes 914 bytes 100.0% 🚀
chunks/all_C7gX-NyR.mjs (New) 914 bytes 914 bytes 100.0% 🚀
chunks/index_CvyQyr_o.mjs (New) 892 bytes 892 bytes 100.0% 🚀
chunks/index_fHDbBEx0.mjs (New) 892 bytes 892 bytes 100.0% 🚀
prerender-entry.DcpsdGN6.mjs (Deleted) -253.92kB 0 bytes -100.0% 🗑️
chunks/ExampleBrowser_F3j8J0Ab.mjs (Deleted) -65.57kB 0 bytes -100.0% 🗑️
chunks/AppShell_DUdDhtHB.mjs (Deleted) -50.47kB 0 bytes -100.0% 🗑️
chunks/DocsSidebar_CdbO1JCl.mjs (Deleted) -26.63kB 0 bytes -100.0% 🗑️
chunks/HomePage_DFpXC_gW.mjs (Deleted) -15.48kB 0 bytes -100.0% 🗑️
chunks/ApiSymbolPage_DMvsAXq5.mjs (Deleted) -15.4kB 0 bytes -100.0% 🗑️
chunks/GuideChapterPage_BfvebRrR.mjs (Deleted) -13.12kB 0 bytes -100.0% 🗑️
chunks/ApiAllPage_kHkege_0.mjs (Deleted) -12.01kB 0 bytes -100.0% 🗑️
chunks/GuideIndexPage_lTzaYuqq.mjs (Deleted) -8.35kB 0 bytes -100.0% 🗑️
chunks/ApiIndexPage_DbnvT2Ga.mjs (Deleted) -8.27kB 0 bytes -100.0% 🗑️
chunks/index_DhtI-Eqz.mjs (Deleted) -1.6kB 0 bytes -100.0% 🗑️
chunks/index_GC-Q-WLT.mjs (Deleted) -1.6kB 0 bytes -100.0% 🗑️
chunks/index_D_ME9FSD.mjs (Deleted) -1.43kB 0 bytes -100.0% 🗑️
chunks/index_aaKywS3w.mjs (Deleted) -1.43kB 0 bytes -100.0% 🗑️
chunks/_symbol__BpJwIMAa.mjs (Deleted) -1.4kB 0 bytes -100.0% 🗑️
chunks/_symbol__CtTmBndn.mjs (Deleted) -1.4kB 0 bytes -100.0% 🗑️
chunks/index_Bvboeq3A.mjs (Deleted) -952 bytes 0 bytes -100.0% 🗑️
chunks/index_DW-0cBN8.mjs (Deleted) -952 bytes 0 bytes -100.0% 🗑️
chunks/index_BGdU17FO.mjs (Deleted) -930 bytes 0 bytes -100.0% 🗑️
chunks/index_ClDdrRnI.mjs (Deleted) -930 bytes 0 bytes -100.0% 🗑️
chunks/all_BLuUL1Tn.mjs (Deleted) -914 bytes 0 bytes -100.0% 🗑️
chunks/all_Cr2kKCJu.mjs (Deleted) -914 bytes 0 bytes -100.0% 🗑️
chunks/index_DzVhfsR6.mjs (Deleted) -892 bytes 0 bytes -100.0% 🗑️
chunks/index_VWIGaNWD.mjs (Deleted) -892 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@Exoridus
Exoridus merged commit 801401e into main Aug 20, 2026
20 checks passed
@Exoridus
Exoridus deleted the test/o45-cull-margin-contract branch August 20, 2026 20:00
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