Repository navigation
test(interaction-harness): a busy proof can bound its settle in the cast's own steps - #9545
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
There was a problem hiding this comment.
Changes requested. The opt-in pacing and the step-counted settle bound read well, and their unit tests pass, but two points need fixing first.
touchPacingStallsfails any arrival gap of 40 ms or more. Core's release velocity reads only the last two or three move intervals, and it drops to 0 only after a final move-to-lift gap longer than 40 ms. So an early stall fails a play whose velocity is intact, while a shorter stall or a 2 ms pair in the last two intervals passes and changes the release by half or more.- The description says everything that doesn't opt in behaves as before. But the held-frame pace in
busy.tsapplies to everybusyWindowProofwith a control: onbusy.webkitdecel.fixture.tswithsettledWithin: 4, the base fails and this head passes. Please reword the claim or gate the rule. - A gap caused by the page's own touch handler is reported as the runner's fault. The wheel path already tells those two causes apart.
[Automated review]
| const stalls = gapsMs.filter( | ||
| (gap, i) => gap >= maxGapMs && played[i + 1].t - played[i].t < maxGapMs, |
There was a problem hiding this comment.
This flags any arrival gap of 40 ms or more. Core's VelocityTracker returns 0 only when the last move-to-lift gap is longer than 40 ms, and its pan release blends only the last two move intervals. Feeding this loop to that tracker: a 30 ms stall on the first move throws, with the release still at 1,000 px/s. A 20 ms stall on the fourth move passes, with the release at 554 px/s. Consider checking the last two move intervals against their scripted intervals, and the final move-to-lift gap against the stillness interval (strictly longer).
There was a problem hiding this comment.
Fixed at b5e41e4. A paced play now reads the release as core does, from the page's own stamps: 0.2 × the newest move interval plus 0.8 × the one before, and 0 when the lift comes more than 40 ms after the last move. An earlier stall no longer fails the play. A rest always fails it; a stray release fails it only under the proof's own releaseTolerance. A 900 px/s flick scripted at 120 Hz reached WebKit's page as 274 to 2,160 px/s, 19 of 20 runs more than 20% off, while the busy proof judged them, so any default tolerance would fail nearly every WebKit run. The lift's PlayedEvent.release names what the page read.
| const paceBefore = pxPerFrameBefore(start); | ||
| const runFrames = (series[k].t - series[start].t) / cadence; | ||
| const pace = | ||
| paceBefore == null | ||
| ? null | ||
| : runFrames > 0 | ||
| ? Math.min(paceBefore, catchUp / runFrames) | ||
| : paceBefore; |
There was a problem hiding this comment.
This applies to every busyWindowProof with a control, not only to callers that opt in. On busy.webkitdecel.fixture.ts with settledWithin: 4, the base fails ("moved on 7 of the 8 frames the control moved") and this head passes. The description's "Everything that doesn't opt in behaves as before" needs rewording, or the rule needs gating behind settledWithinSteps.
There was a problem hiding this comment.
Gated at b5e41e4: the run pace applies only under settledWithinSteps. With settledWithin: 4, the decel fixture fails as on main ("moved on 7 of the 8 frames the control moved"); a new unit case pins it.
|
On the page-versus-runner point, fixed at b5e41e4. Window listeners before and after the page's own record when each touch arrived and when the page had handled it. A gap the release reads fails as |
There was a problem hiding this comment.
Changes requested. The opt-in spacing and the step-counted settle bound now behave as described, and the earlier gap check and held-frame claim are fixed, but three points remain.
TouchPacingErrorsays the runner failed after timing only touch listeners, while Core's gesture host samples a finger frompointermove: time touch pointers too, with a self-test whosepointermovelistener holds the thread, or narrow the claim to touch listeners.readReleasereturnsInfinitypx/s when two moves share a timestamp, where Core's tracker replaces the sample and reads 1,200 px/s on the same flick, and it reads a non-zero release after a single move where Core reads 0.touchArrivalProberepeatsarmWheelRun's capture/bubble recorder; one shared recorder that takes the event types keeps the wheel and touch paths from drifting.
[Automated review]
| }; | ||
| const host = window as unknown as {__harnessTouchRun?: Run}; | ||
| host.__harnessTouchRun?.stop(); | ||
| const types = ['touchstart', 'touchmove', 'touchend', 'touchcancel']; |
There was a problem hiding this comment.
Only touch events are timed here. Core's gesture host samples a finger from pointermove, so a gap that a component's pan handler causes is not counted as the page's, and the play throws TouchPacingError, which says the runner failed. Consider timing touch pointers with the same capture/bubble pair, or narrowing the error text to touch listeners.
There was a problem hiding this comment.
Done in 0bfa81b. The probe records each step's pointer events with its touch event, and touchArrivalsFrom gives the step the earliest stamp and the latest handling of them: WebKit stamps the touch event only after the page's pointer listeners. A WebKit self-test page whose pointermove listener holds the main thread 60 ms passed under touch-only timing (3 of 3) and now fails as TouchPageStallError.
| const seconds = (time(b.index) - time(a.index)) / 1000; | ||
| return seconds > 0 | ||
| ? {x: (b.x - a.x) / seconds, y: (b.y - a.y) / seconds} | ||
| : {x: Infinity, y: Infinity}; |
There was a problem hiding this comment.
Two moves that share a timestamp make this Infinity, so the lift's release reads Infinity and any releaseTolerance throws. Core's VelocityTracker replaces a moved sample that shares the newest one's time and reads 1,200 px/s on the same flick. With a single move, Core reads 0 where this reads 0.2 of that move.
There was a problem hiding this comment.
Done in 0bfa81b. readRelease reads the page and the script through Core's filter: a same-time moved sample replaces the earlier one, an interval across no time reads 0, and fewer than two moves read 0. Two moves at one stamp now read 1,000 px/s, a single move reads 0, and a page that read every move at one stamp fails as moves; each case was red on 18d3b24. Both readings use the same positions, rounded once, and a late lift after a single move is no rest.
| * passive listener on the window after them; an event whose propagation the | ||
| * page stops is never noted, and counts as handled at once). | ||
| */ | ||
| function touchArrivalProbe(page: Page): TouchArrivalProbe { |
There was a problem hiding this comment.
This repeats armWheelRun in webInput.ts: a capture listener that stamps each event with handledAt: NaN, a bubble listener that sets it, and the same stop(). One exported recorder that takes the event types would serve both paths.
There was a problem hiding this comment.
Done in 0bfa81b. armInputRun(page, types) in webInput.ts is the one recorder: the wheel driver arms it with wheel, and the touch probe with the touch and pointer types. Each event keeps its own record, untrusted events are ignored, and an event whose propagation the page stops is marked by a queued task, never later than the next event's arrival.
There was a problem hiding this comment.
Changes requested. The new still-run bound lets a step-counted busy proof pass a long hold near rest, and the three points from the last review are still open.
- With
settledWithinSteps, a zero pace after the change makes the bound inbusy.tsunlimited: on the WebKit tail fixture, holding 214 px for 82 or 139 ms before the 218 px change passes, while the previous commit fails both; bounding the pace by the run's own catch-up (Math.max(around, catchUp / runFrames)) keeps the existing cases passing and fails both holds. TouchPacingErrorstill blames the runner after timing only touch listeners, while Core's gesture host samples a finger frompointermove.readReleasestill readsInfinitypx/s when two moves share a timestamp (Core reads 1,200 px/s on the same flick) and a non-zero release after a single move (Core reads 0).touchArrivalProbestill repeatsarmWheelRun's capture/bubble recorder instead of sharing one recorder that takes the event types.
[Automated review]
| if (castStepPx != null && around != null && around < castStepPx) { | ||
| // The ratio is read to a millionth: a pace of 0.5 that sums to | ||
| // 0.4999… still allows two frames, not three. | ||
| return unchanged <= Math.ceil(castStepPx / around - 1e-6); |
There was a problem hiding this comment.
When the frames after the change are the settled plateau, pxPerFrameAfter(k) is 0, so around is 0 and this bound is Infinity. On busy.webkittail.fixture.ts with settledWithinSteps: 2, holding 214 px until the 218 px change for 82 or 139 ms passes, while the previous commit fails both. Consider bounding the pace by the run's own catch-up, Math.max(around, catchUp / runFrames): in a probe it keeps all 49 busy.test.ts cases passing and fails both holds.
There was a problem hiding this comment.
Done in 0bfa81b. The still-run bound reads the travel from the moving paces around the run, and never less than the run's own catch-up over its frames. A zero pace after the change is the settled plateau, so it no longer counts. Your two holds, 214 px for 82 and 139 ms before the change to 218 px, are must-fail cases now: both passed on 18d3b24 and fail here, and the other busy cases still pass.
18d3b24 to
0bfa81b
Compare
There was a problem hiding this comment.
Changes requested. The four points from the last review are fixed, but four remain.
- When the script rests before the lift,
readReleasetakes the script's reading for the page's, so a scripted 50 ms rest whose last move is answered 15 ms late reaches the page as a 36 ms gap, from which Core's tracker reads 906.7 px/s, yet the play passes and the lift'sreleasereports 0 px/s; read the page from its own stamps and fail a play whose page does not rest where the script does. - Under
settledWithinStepsa slow still run may last ceil(step / travel) frames whatever the run before it, but the README and thebusyWindowProofJSDoc still say every run is at most one frame longer than the one before, and neither documents the new allowance. armInputRunnow times a step whose propagation the page stops, so such a wheel stall fails asWheelPageStallErrorwithout a retake, while the README and thewheelGapCausesJSDoc still say it counts as the dispatch's stall, and no test covers that mark.coreReleasereimplements Core'sVelocityTrackerfilter with no note saying why it cannot call it and no SYNC note on the copied logic, so the two can drift silently.
[Automated review]
| positions.map(p => ({...p, time: steps[p.index].t})), | ||
| steps[lift].t, | ||
| ); | ||
| const page = script.rests |
There was a problem hiding this comment.
When the script rests, this takes the script's reading for the page's. On the flick lifted 50 ms after its last move, a last move answered 15 ms late reaches the page 36 ms before the lift: Core's VelocityTracker reads 906.7 px/s from those stamps, but this reports 0 and touchPacingFault(..., 0.2) returns null. Reading the page from its own stamps, and failing a play whose page does not rest where the script does, would catch it.
There was a problem hiding this comment.
Done in b8b0a85. readRelease reads the page from its own stamps whatever the script does, and a page that does not rest where the script does fails with the new throw kind. Your case (the flick lifted 50 ms after its last move, that move 15 ms late) now reads 903.2 px/s and fails; it read 0 and passed on 0bfa81b.
| * across no time reads 0. The release is 0 with fewer than two moves after | ||
| * the touch-down, or after a rest longer than the stillness interval. | ||
| */ | ||
| function coreRelease( |
There was a problem hiding this comment.
This reimplements VelocityTracker's add, releases and panVelocity from Core. Core's tracker is not exported, so a copy may be the only option, but a line saying so and a SYNC note naming VelocityTracker.ts would keep the two from drifting; today only the two constants carry SYNC notes.
There was a problem hiding this comment.
Done in b8b0a85. coreRelease says it copies Core's filter because VelocityTracker is internal to the core package, and both its doc and the file's SYNC note name VelocityTracker.ts (add, releases, panVelocity).
| /** When the page's wheel listeners had finished with each step; NaN until seen. */ | ||
| /** | ||
| * When the page's own listeners had finished with each event, NaN until | ||
| * seen. An event whose propagation the page stops is marked by a task |
There was a problem hiding this comment.
With this mark, a wheel step whose propagation the page stops can now fail as WheelPageStallError (no retake), where the old recorder left it NaN and wheelGapCauses called it the dispatch's. The README's wheel paragraph and the wheelGapCauses JSDoc still describe the old rule, and no test stops propagation.
There was a problem hiding this comment.
Done in b8b0a85. The README's wheel paragraph and the wheelGapCauses doc now say a stopped step is marked by a task queued at its arrival, so its handling counts as the page's. A new self-test stops each wheel step on the body and holds the main thread 150 ms: WebKit fails it as WheelPageStallError without a retake (3 of 3); on the previous recorder it was a re-taken WheelPacingError (3 of 3).
| itself (its catch-up over the frames it spans) where the curve slows faster | ||
| than those frames show, a frame delivered early can show no step too, so the change may carry the motion of every frame | ||
| the run held at that pace, to the nearest step. The run must also read as a | ||
| slowing curve, at most one frame longer than the component's run before it. A |
There was a problem hiding this comment.
Under settledWithinSteps, busy.ts now lets a slow still run last ceil(step / travel) frames whatever the run before it (the plateau case in busy.test.ts passes a three-frame run right after two one-frame runs). This sentence, and the same one in the busyWindowProof JSDoc, still read as a rule for every proof.
There was a problem hiding this comment.
Done in b8b0a85. The README and the busyWindowProof doc now say that under settledWithinSteps a run under one cast step a frame may last ceil(step ÷ travel) frames, whatever the run before it, with the travel defined as in busy.ts.
0bfa81b to
b8b0a85
Compare
There was a problem hiding this comment.
Changes requested: the paced touch play now reads the page's release the way Core's tracker does, and each point from the last review is addressed, but two things remain.
- Under
settledWithinSteps, the still-run allowance inbusy.ts(lines 987-1000) takes the slower of the paces before and after the run, so onbusy.webkittail.fixture.tsa stall that holds 212 px for four extra frames (82 ms) and then replays the rest of the coast passes, whilesettledWithin: 4, the same 4 px bound on that cast, fails it with "longest stall 82.2 ms". - The paced play's documented failures leave out the new
throwfault:touchPacingFaultstill says "null otherwise",PlayOptions.pacinglists only a resting finger and the tolerance, the README's engine-limits row lists a rest and too few moves, and thetouch.pacing.test.tsheader still says a scripted rest is the script's own.
For the first, capping the allowance at one frame past what the pace before the run allows, Math.ceil(castStepPx / Math.max(paceBefore, catchUp / runFrames) - 1e-6) + 1, keeps all 50 busy.test.ts cases passing and fails that stall, which would make a good must-fail case.
[Automated review]
| const moving = [paceBefore, pxPerFrameAfter(k)].filter( | ||
| (value): value is number => value != null && value > 0, | ||
| ); | ||
| const travel = | ||
| moving.length === 0 | ||
| ? null | ||
| : Math.max( | ||
| Math.min(...moving), | ||
| runFrames > 0 ? catchUp / runFrames : 0, | ||
| ); | ||
| if (castStepPx != null && travel != null && travel < castStepPx) { | ||
| // The ratio is read to a millionth: a pace of 0.5 that sums to | ||
| // 0.4999… still allows two frames, not three. | ||
| return unchanged <= Math.ceil(castStepPx / travel - 1e-6); |
There was a problem hiding this comment.
travel takes the slower of the paces before and after the run, so on an easing curve the slow tail after a stall sets the allowance. On busy.webkittail.fixture.ts, holding 212 px for four more frames (82 ms) and replaying the rest of the coast four frames late passes with settledWithinSteps: 2, while settledWithin: 4 (the same 4 px bound on this 2 px cast) fails it with "longest stall 82.2 ms". Capping at Math.ceil(castStepPx / Math.max(paceBefore, catchUp / runFrames) - 1e-6) + 1 keeps all 50 busy.test.ts cases passing and fails that stall.
There was a problem hiding this comment.
Done in 9304656, with your cap: the run may last at most one frame more than the pace into it allows, ceil(step ÷ max(pace before, catch-up per frame)) + 1. Your stall (212 px for four more frames, the coast replayed four frames late) is a must-fail case now: it passed on b8b0a85 and fails with "longest stall 82.2 ms", as settledWithin: 4 does. All the other busy cases still pass.
| * Reads a paced play's release as the page did and returns where the page | ||
| * read a resting finger or, given `tolerance`, a release that strays from | ||
| * the script's by more than that share of it; null otherwise. The release |
There was a problem hiding this comment.
This now also returns a throw fault (the script rests, the page reads a throw) and a moves fault without any tolerance, so "null otherwise" no longer holds. PlayOptions.pacing, the README's engine-limits row and the touch.pacing.test.ts header need the same four kinds.
There was a problem hiding this comment.
Done in 9304656. touchPacingFault's doc lists the four kinds (rest, throw, moves, and release under a tolerance), and PlayOptions.pacing, the README's engine-limits row and the touch.pacing.test.ts header now name the throw where the script rests.
b8b0a85 to
9304656
Compare
There was a problem hiding this comment.
Changes requested: the still-run cap and the throw docs from the last round are in, and the 82 ms stall now fails under settledWithinSteps, but two things remain.
- The pull request is now 2,022 authored lines (not counting the two fixtures' recorded data rows), over the 2,000-line review threshold, and its touch-pacing half (
touch.ts,webInput.ts, their unit tests and browser self-tests) and its cast-step half (frames.ts,busy.ts, their tests and fixtures) share no code, so please split it into two pull requests. README.md(lines 228-230) and thebusyWindowProofdoc inbusy.ts(lines 541-544) still give the still-run allowance as ceil(step ÷ travel) frames without the new cap, so the PR's own must-fail case, which that rule would pass, fails with no documented reason; one added clause such as "and at most one frame more than the pace into the run allows" fixes both.
[Automated review]
| Under `settledWithinSteps`, a run under one cast step a frame may instead last | ||
| ceil(step ÷ travel) frames, whatever the run before it. The travel is the | ||
| slower moving pace around the run, never less than its own catch-up per frame. A |
There was a problem hiding this comment.
This states the allowance without the new cap in busy.ts (one frame past what the pace into the run allows). The PR's own must-fail case, 212 px held five frames after a 2.3 px-a-frame coast, passes by this rule (ceil(2 ÷ 0.46) = 5) and fails in code (cap 2). Suggest adding: "and at most one frame more than the pace into the run allows, ceil(step ÷ max(pace before, catch-up per frame)) + 1".
There was a problem hiding this comment.
Done in fe42259. The README adds: "The run also lasts at most one frame more than the pace into it allows: ceil(step ÷ max(pace before, catch-up per frame)) + 1."
| * Under `settledWithinSteps`, a run under one cast step a frame may instead | ||
| * last ceil(step ÷ travel) frames, whatever the run before it. The travel is | ||
| * the slower moving pace around the run, never less than its own catch-up | ||
| * per frame. A |
There was a problem hiding this comment.
Same as the README: this doc gives ceil(step ÷ travel) as the bound, but the code below also caps the run at Math.ceil(castStepPx / into - 1e-6) + 1. Please name the cap here too.
There was a problem hiding this comment.
Done in fe42259. The busyWindowProof doc names the cap the same way.
…ast's own position steps busyWindowProof takes settledWithinSteps, a settle bound in the cast's own position steps. trackMarker gives each frame its step, read from the cast itself: one block of the frame's color resolution (colorBlockPx: a JPEG's chroma subsampling from its frame header, 1 for a PNG) over the bitmap's scale, so 2 px in WebKit's 4:2:0 JPEG cast and 1 px in Chromium's PNG cast, and never a step inferred from the motion. A held frame's local pace is also read across the run itself where a curve slows faster than the frames before it show.
…d frame's run pace The held-frame run pace applies only to a proof that bounds its settle in the cast's steps; every other busy proof judges as before.
…y proof reads a still run against the cast's own step Near a coast's rest, WebKit's 2 px cast shows a new position about every step / pace frames, so a frame delivered early can leave a two-frame plateau right after two changes in a row. The rule "a run more than one frame longer than the one before it is a stall" read that as a stall (2 of 20 runs of meta#8190's Slider coast proof at four CPUs). Where the pace around the run is under the cast's own step, a proof with settledWithinSteps now allows a run of up to ceil(step / pace) frames plus one early frame; a longer run is still a stall. The pace around the run is the smaller of the motion over up to four frames before it and after the change that ends it, never across it, and the step is read from the cast, never the motion. These still fail: a three-frame freeze at 1.2 px a frame that resumes from where it held, a 40 ms freeze in a WebKit or a Chromium tail, a one-frame hold at two steps a frame or more, and a three-frame hold at 2 px a frame on a 1 px cast. A proof with settledWithin judges as before.
…ounted busy proof The still-run bound reads the travel at the run from the moving paces around the run, never less than the run's own catch-up over its frames. A pace of 0 after the change is the settled plateau, not motion, so it no longer makes the bound unlimited: on the WebKit tail, holding 214 px for 82 or 139 ms before the change to 218 px fails again. The run also lasts at most one frame more than the pace into it allows, so a slow tail after a stall cannot excuse it. Holding 212 px for 82 ms and replaying the coast four frames late fails, as a 4 px pixel bound does.
9304656 to
fe42259
Compare
|
Split as asked. This pull request keeps the cast-step half: |
There was a problem hiding this comment.
Changes requested: the split and the still-run cap docs from the last round are in, and the step-counted bound behaves as described, but one documentation point remains.
README.md(lines 189-192) still says WebKit's 2 px position step comes from capturing the page at half resolution, while the new text at lines 149-151 and thecastStepandTrackedFrame.stepdocs say it comes from the JPEG's 4:2:0 chroma blocks, the only thingcastStepreads, so please reword the older paragraph to the frame-header cause.
[Automated review]
| `step` `trackMarker` gives each frame): 1 px in Chromium's PNG cast and 2 px in | ||
| WebKit's JPEG cast, whose 4:2:0 chroma holds the marker's color in 2 by 2 | ||
| pixel blocks and so makes the tail's plateaus longer. Two steps stop at the |
There was a problem hiding this comment.
This explains WebKit's 2 px step by 4:2:0 chroma blocks, but the frame-evidence paragraph further down (lines 189-192) still says the page is captured at half resolution. castStep reads only the header's chroma subsampling, so please reword that paragraph to this cause, for example: "WebKit's casts are 2 px quantized: the frames are 4:2:0 JPEGs at the viewport's size, whose chroma holds colour in 2 by 2 pixel blocks, so a marker found by its colour moves in 2 CSS px steps (Chromium's PNG cast in 1)."
…hroma blocks CodeBunny on meta#9545: the older paragraph still said half-resolution capture; the step castStep reads is the frame header's 4:2:0 chroma block. Co-authored-by: Cindy Zhang <cindyxz@meta.com>
There was a problem hiding this comment.
Changes requested: the README now gives one cause for WebKit's 2 px step, and the step-counted bound behaves as described on the recorded WebKit coast, but one point about the step itself remains.
trackMarkergives every frame the JPEG's chroma block as itsstepwhatever colour it tracks, yet in both recorded WebKit fixtures the cyan control, tracked from the same frames, moves by odd amounts (96, 105, 112, 121 px inbusy.webkittail.fixture.ts), socastStepwould double thesettledWithinStepsbound for any marker other than the default magenta; please setsteponly for the colour it was measured for (or havecastSteprefuse other tracks) and scope theTrackedFrame.step,castStepand README wording to the default marker.
[Automated review]
| t: batch[k].t, | ||
| box: result.box, | ||
| pixels: result.pixels, | ||
| step: {x: block.x / result.scale, y: block.y / result.scale}, |
There was a problem hiding this comment.
This labels every track with the frame's chroma block, whatever colour was tracked. In both recorded WebKit fixtures the cyan control, tracked from the same frames with CONTROL_RGB, moves by odd amounts (96, 105, 112, 121 px in busy.webkittail.fixture.ts), so its frames would carry step: {x: 2, y: 2} while it resolves 1 px, and castStep would double the settledWithinSteps bound for any marker other than magenta. Could step be set only when tracking MARKER_RGB (so castStep refuses other tracks), with the TrackedFrame.step doc saying the 2 px step is the default marker's?
…cast's step CodeBunny on meta#9545: a cyan control in the same WebKit frames moves by odd px, so a step measured for the magenta marker would double the settle bound for another color. trackMarker sets step only for MARKER_RGB; castStep refuses tracks without it. Co-authored-by: Cindy Zhang <cindyxz@meta.com>
There was a problem hiding this comment.
No blocking issues at this head: the step-counted settle bound behaves as described on the recorded WebKit coasts, and the last point is fixed, since only the default marker's frames now carry step and castStep refuses other tracks. CI's test jobs have not run on this draft yet.
- Optional: the description's Change bullet still says
trackMarkergives each frame its position step; "each frame of the default marker" would match the code. - For awareness: the cap's extra frame lets
settledWithinSteps: 2pass a 202 px hold two frames long onbusy.webkittail.fixture.tsthatsettledWithin: 4fails; that follows the README's rule, and the three-frame plateau case depends on it.
[Automated review]
Maintainer impact
WebKit's busy-main-thread proofs failed on a loaded runner with nothing wrong in the component. WebKit's JPEG cast moves in 2 px steps, so an easing tail's plateaus read as stalls under
settledWithin: 2(3 of 20 runs of the Slider coast proof). This lets a proof bound its settle in the cast's own steps. #9725 is the touch-pacing half.Evidence
internal/interaction-harnesschanges.Intended invariant
A busy proof fails only for a stall. A proof without
settledWithinStepsjudges as before.Change
trackMarkergives each frame its position step, one block of the frame's color resolution (colorBlockPx, from the frame header) over the bitmap's scale.castStepreads it and never infers one from motion.settledWithinSteps:busyWindowProofbounds the settle in those steps. Only such a proof reads a held frame's pace across the run itself.#9694 moves the Slider's WebKit busy proof onto this and #9725.
Scope
Testing
Local gates at fe42259, on
main1d04d40: typecheck,ASTRYX_STRICT_LINT=1 pnpm lint(no warnings in changed files), Prettier,check:knowledge, changeset coverage (nothing ships), the harness's and Core's gesture unit tests (773), and the harness's browser self-tests in Chromium on a 2-thread pin (one physical core) at four workers (88 passed, 1 skipped) and in WebKit in the CI job's container profile (76 passed, 13 skipped). The node suite passed 16,062 tests. 3 CLI files timed out under box load; they pass alone 469/469; this PR touches no CLI file; cause fix: #9496/#9517/#9722. Every commit passes the harness's typecheck and unit tests on its own. The comment-provenance baseline's--checkdiffers only inmain's own four stale entries (ChatComposer, Item, layerSurfacePress, Token).