test(rendering): pin and instrument the retained capture-margin contract - #581
Merged
Conversation
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
enabled auto-merge (squash)
August 20, 2026 19:48
Bundle ReportChanges will increase total bundle size by 396 bytes (0.0%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: site-server-esmAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships the measurement work behind the retained cull-margin decision. The margin
constant is unchanged:
1/16per view axis stays, and this PR is what makes thatchoice 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:
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
zIndexor a non-group sibling, which a z-sortedscene 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 as1/r.Where the knee is
Marginal amortized CPU per step, 100 000 nodes, speed 8:
Above
1/16every further step buys at most 6-18% amortized CPU, all inside theCPU 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:
The optimum moves with scroll rate, as the geometry predicts. At the archetype's
design speed and at half of it,
1/16is at or below the optimum, and1/8and1/4are worse at speed 4 while permanently drawing 56% / 125% more nodes. Asingle 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
frameMsP95of 122 ms atr = 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 thewarmup/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.tsandcullMarginProbe.tspin the capture-margin contract and keep the probe honestabout which tier served a frame.
run-cull-margin.ts/run-cull-margin-cell.tsplusperf:renderers:cull-marginandperf:renderers:cull-margin:cellmake thesweep reproducible. One cell per process, for the reasons the allocation
harness already documents.
cull-margin-*bench arms, deliberately outsideADAPTER_CAPABILITIES:they answer an engine-tuning question, not a cross-library one, and enter the
matrix only when
--confignames them.Validation
pnpm verify:quick- all 16 gates passvitest --project=rendering-perf cull-margin capture-kept-bounds- 24 tests passpnpm perf:renderers:cull-marginsmoke run on the current base: 17 replay / 3miss frames at
r = 1/16, capture area 1.266x, as expected