Two shots one picture: count loads per URL, wait for the physics worker and the second take - #187
Open
abernier wants to merge 2 commits into
Open
Two shots one picture: count loads per URL, wait for the physics worker and the second take#187abernier wants to merge 2 commits into
abernier wants to merge 2 commits into
Conversation
abernier
added a commit
that referenced
this pull request
Aug 13, 2026
* Report the shot at the commit that has a branch Chromatic posts `UI Tests` and `UI Review` on whatever commit the CLI is given, and on a `pull_request` event that is `GITHUB_SHA` -- the merge commit GitHub builds for the run, which exists on no branch. The pull request displays them anyway, so nothing looked wrong. But a required status check is read off the head commit, and on #187 the two sets never met: twelve Actions check runs on `5520f5f2`, three Chromatic statuses on the merge commit `aefcbe4`. Adding `UI Tests` to the ruleset today would have waited for a status that never lands there, on every pull request, forever. So hand Chromatic the head commit, plus the branch and slug that have to travel with it or the build attaches to the wrong ref. All three are empty on a push, where `GITHUB_SHA` is already the commit itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Take the documented values, fallbacks and all `github.repository` rather than the head repo's slug, and a fallback on each so a push to main keeps reporting where it always did. This is the snippet Chromatic's GitHub Actions guide gives for `pull_request` workflows, which is what this one is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`waitForDecodes` asks the page for one number: how many items the default loading manager still has in flight. That number is kept by counting `itemStart` up and `itemEnd` down -- which trusts every loader to call both, and one of them does not. `postprocessing` 6.39's `LUTCubeLoader.load()` calls `itemEnd` on a URL it never called `itemStart` for; 6.36, the version on this branch today, called both. A single counter cannot survive that. It reads 0 at whatever moment exactly one real item is in flight -- the shot going off in the middle of a decode, which is the whole thing this wait exists to prevent -- and -1 the rest of the time, which is a wait that cannot end. Measured on the three-0.181 branch, where 6.39 arrives: `glass-flower` and `nextjs-prism` each sat on the full 300s budget and failed, and the four other `.cube` examples shot early without saying anything. So count per URL and ignore an `itemEnd` for a URL nobody started. No behaviour change here -- 6.36 is balanced -- this is the harness holding its own invariant rather than borrowing a loader's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abernier
force-pushed
the
e2e-inflight-per-url
branch
from
August 13, 2026 15:51
5520f5f to
f3c154d
Compare
Chromatic flagged basic-ballpit against a baseline nobody touched, twice, while e2e-flaky swore it was stable locally. Both were right: two mechanisms, both invisible on an idle machine, both measured under CPU throttle. @react-three/cannon steps by ping-pong -- each frame posts `step` and *transfers* the position buffers to its worker; until the `frame` reply hands them back, every further step is silently skipped and its 1/60th of simulation dropped, not deferred. The picture is spawn + completed round trips x 1/60, and how many round trips fit into thirty pumped frames is the scheduler's call. basic-ballpit: 30/30 at full speed, 28 under x8 throttle, each side perfectly reproducible -- two stable pictures, chosen by machine speed. So the exchange is counted, per worker, and the pump holds the next frame until the count settles: one step per frame, every machine. terminate() forgives what a dying worker owes, and a step posted to an already-terminated worker is never counted -- posting into a dead worker is a silent void, and counting it held the pump for the full 300s budget. And the remount's flushSync returns when the *DOM* side has committed; the scene lives behind the <Canvas> bridge in r3f's own root, whose render is scheduled, not flushed. trails, throttled: both flushSyncs long returned, and the second take still assembled itself at frames 1-2 of the pump -- worker created, connected and populated across running frames, with the first take's worker, not yet unmounted, stepping in the meantime. So the take announces when it has *finished* mounting (Probe, last in the keyed fragment: by the time its effect runs, every sibling's have), and the shot waits for the announcement. Measured across the twelve cannon examples, full speed and x8, two runs each: one hash per example, 30/30 round trips, racing-game (29 posted, 28 answered before) and pmndrs-vercel and trails (three distinct pictures) included. basic-ballpit holds one hash at x1/x4/x8/x20 -- unchanged from before the fix, so CI converges back to the picture already measured. object-clump still moves under x8 -- a different, pre-existing channel (its seeded initial positions shift with load timing); at full speed its hash is unchanged and stable. clones, springy-boxes and video-cookies unchanged as non-cannon controls; arkanoid stays in EXCEPTIONS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Two harness fixes, one per commit -- both found by this stack's own CI, both invisible to a local sweep.
Count what loads per URL, and never below zero
waitForDecodes-- the wait that keeps a shot from going off mid-decode -- asks the page for one number: how many itemsDefaultLoadingManagerstill has in flight.deterministic.jskeeps that number by countingitemStartup anditemEnddown, which trusts every loader to call both.One of them does not.
postprocessing6.39'sLUTCubeLoader.load()callsexternalManager.itemEnd(url)on a URL it never calleditemStartfor:6.36.6 -- the version on
maintoday -- called both, so this changes no behaviour here. It matters on #166, which movespostprocessingto 6.39.4 (6.37+ requires three >= 0.174), and it is worth landing on its own: the harness should hold this invariant itself rather than borrow a loader's.A single counter sits at
real - 1for the rest of the page's life, and that is wrong in both directions: it reads 0 while one real item is still in flight (the shot fires mid-decode -- two of the four.cubeexamples did this silently) and -1 the rest of the time (a wait that cannot end --glass-flowerandnextjs-prismeach held the full 300s budget and failed). So: count per URL, ignore anitemEndfor a URL nobody started.On the #166 branch, the four
.cubeexamples the sweep covers, before -> after:nextjs-prismglass-flowercolor-gradinginstanced-particles-effectsHold each frame for the physics worker, and the shot for the second take
Chromatic flagged
basic-ballpitagainst a baseline nobody touched, twice in a row, whilee2e-flakyswore it was stable locally. Both were right -- two mechanisms, both measured under CPU throttle:1. cannon's steps are a ping-pong the pump never waited for.
@react-three/cannonpostsstepand transfers the position buffers to its worker; until the worker'sframereply hands them back, every further step is silently skipped (byteLength === 0) and its 1/60th of simulation is dropped, not deferred. The picture is spawn + (completed round trips) x 1/60 -- and how many round trips fit into thirty pumped frames is the scheduler's call. Measured onbasic-ballpit: 30/30 round trips at full speed, 28 under a x8 throttle, each side perfectly reproducible. Two stable pictures, chosen by machine speed; Chromatic's runner sits in between and picks per build.2. the second take can finish mounting inside the shot. The remount's
flushSyncreturns when the DOM side has committed -- but the scene lives behind the<Canvas>bridge in r3f's own root, whose render is scheduled, not flushed. Under throttle,trailswas measured with both flushSyncs long returned and the second take still assembling itself at frames 1-2 of the pump: physics worker created, connected and populated across running frames, with the first take's worker, not yet unmounted, stepping in the meantime -- and a step posted to a worker that gets terminated the next frame is a reply that never comes.The fix, in two halves:
deterministic.jscounts the exchange --stepout,frameback, per worker -- and the pump holds the next frame until the count settles: every machine completes exactly one step per frame.terminate()forgives what a dying worker still owes, and a step posted to an already-terminated worker is never counted.CheesyCanvasgains aProbe, last in the keyed fragment: its effect runs when the take has finished mounting -- render committed, every sibling's effects run.shoot.mjswaits for it after the remount, so the pump starts against a fully assembled scene.Verification
Every cannon example, two runs at full speed and two under x8 CPU throttle, canvas hashed byte-for-byte through the same
shoot()the test uses -- one hash per example, 30/30 round trips, across every rate and run:basic-ballpit(also held at x4 and x20; hash unchanged from before the fix, so CI converges back to the picture already measured),racing-game(29 posted / 28 answered before),trails(three distinct pictures before; 8/8 runs after),pmndrs-vercel,ragdoll-physics,pinball-in-70-lines,trigger-meshes,simple-physics-example,simple-physics-example-with-debug-bounds,physics-with-convex-polyhedrons.Still true and worth writing down:
object-clumpmoves under x8 -- a different, pre-existing channel (its seeded initial positions shift with load timing), stable at full speed and on every CI build to date.arkanoidis inEXCEPTIONS(throws on mount) and stays there. Non-cannon controlsclones,springy-boxes,video-cookies: unchanged and stable; realpnpm testgreen onbasic-ballpitandtrails.馃 Generated with Claude Code