Skip to content

test(interaction-harness): a busy proof can bound its settle in the cast's own steps - #9545

Merged
cixzhang merged 7 commits into
meta:mainfrom
vjeux:harness/paced-touch-cast-step
Nov 3, 2026
Merged

cixzhang merged 7 commits into
meta:mainfrom
vjeux:harness/paced-touch-cast-step

Conversation

@vjeux

@vjeux vjeux commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

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

  • A plateau that is the cast's step: with the settle counted in steps, a two-frame plateau right after two changes in a row still read as a stall in 2 of 20 Slider runs in WebKit (38.9 and 40.3 ms). A unit case on that pattern fails without the still-run bound and passes with it.
  • Stalls near rest fail: 214 px held 82 or 139 ms before the change to 218 px, and 212 px held 82 ms before a late slow tail. Each passed on an earlier version of this change.
  • Freezes still fail at either resolution: a 40 ms freeze more than two steps from rest, a one-frame hold at two steps a frame or more, and a three-frame freeze at 1.2 px a frame on a 2 px cast.
  • Product behavior unchanged: only internal/interaction-harness changes.

Intended invariant

A busy proof fails only for a stall. A proof without settledWithinSteps judges as before.

Change

  • The cast's step: trackMarker gives each frame its position step, one block of the frame's color resolution (colorBlockPx, from the frame header) over the bitmap's scale. castStep reads it and never infers one from motion.
  • settledWithinSteps: busyWindowProof bounds the settle in those steps. Only such a proof reads a held frame's pace across the run itself.
  • Still runs: under one step a frame, a run of up to ceil(step ÷ travel) frames is the cast's step. The travel is the slower moving pace around the run, never less than its catch-up per frame. The run also lasts at most one frame more than the pace into it allows.

#9694 moves the Slider's WebKit busy proof onto this and #9725.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

Local gates at fe42259, on main 1d04d40: 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 --check differs only in main's own four stale entries (ChatComposer, Item, layerSurfacePress, Token).

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 3, 2026 5:27pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 2, 2026
@astracat-bot

astracat-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 3, 2026, 5:36 PM UTC

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 7253545

Storybook · Sandbox · built from this PR's head

Package size: no published package changed; no new dependencies.

API changes: none in this pull request

Committed API report behind this build: core 2 rows; run pnpm api:report.

Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  • touchPacingStalls fails 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.ts applies to every busyWindowProof with a control: on busy.webkitdecel.fixture.ts with settledWithin: 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]

Comment on lines +209 to +210
const stalls = gapsMs.filter(
(gap, i) => gap >= maxGapMs && played[i + 1].t - played[i].t < maxGapMs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +921 to +928
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

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 TouchPageStallError when the page's listeners spent longer than the script's interval on the event before and that handling accounts for the gap; any other gap fails as TouchPacingError. Under 'spacing' the harness waits out a late answer, so the send spacing the wheel path uses cannot separate the two; the handling time can. A self-test page whose touchmove listener holds the thread 40 ms fails as TouchPageStallError, 10 of 10 in WebKit with 4 CPUs.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  • TouchPacingError says the runner failed after timing only touch listeners, while Core's gesture host samples a finger from pointermove: time touch pointers too, with a self-test whose pointermove listener holds the thread, or narrow the claim to touch listeners.
  • readRelease returns Infinity px/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.
  • touchArrivalProbe repeats armWheelRun'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'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in busy.ts unlimited: 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.
  • TouchPacingError still blames the runner after timing only touch listeners, while Core's gesture host samples a finger from pointermove.
  • readRelease still reads Infinity px/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).
  • touchArrivalProbe still repeats armWheelRun'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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested. The four points from the last review are fixed, but four remain.

  • When the script rests before the lift, readRelease takes 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's release reports 0 px/s; read the page from its own stamps and fail a play whose page does not rest where the script does.
  • Under settledWithinSteps a slow still run may last ceil(step / travel) frames whatever the run before it, but the README and the busyWindowProof JSDoc still say every run is at most one frame longer than the one before, and neither documents the new allowance.
  • armInputRun now times a step whose propagation the page stops, so such a wheel stall fails as WheelPageStallError without a retake, while the README and the wheelGapCauses JSDoc still say it counts as the dispatch's stall, and no test covers that mark.
  • coreRelease reimplements Core's VelocityTracker filter 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread internal/interaction-harness/README.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in busy.ts (lines 987-1000) takes the slower of the paces before and after the run, so on busy.webkittail.fixture.ts a stall that holds 212 px for four extra frames (82 ms) and then replays the rest of the coast passes, while settledWithin: 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 throw fault: touchPacingFault still says "null otherwise", PlayOptions.pacing lists only a resting finger and the tolerance, the README's engine-limits row lists a rest and too few moves, and the touch.pacing.test.ts header 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]

Comment on lines +987 to +1000
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +404 to +406
* 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the busyWindowProof doc in busy.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]

Comment thread internal/interaction-harness/README.md Outdated
Comment on lines +228 to +230
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Comment on lines +541 to +544
* 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fe42259. The busyWindowProof doc names the cap the same way.

vjeux added 4 commits November 2, 2026 22:02
…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.
@vjeux
vjeux force-pushed the harness/paced-touch-cast-step branch from 9304656 to fe42259 Compare November 3, 2026 08:08
@vjeux vjeux changed the title test(interaction-harness): a paced touch play keeps its spacing after a stalled dispatch, and settle bounds read the cast's own step test(interaction-harness): a busy proof can bound its settle in the cast's own steps Nov 3, 2026
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Split as asked. This pull request keeps the cast-step half: frames.ts, busy.ts, their tests and fixtures, and the README's busy-proof text. #9725 takes the touch-pacing half: touch.ts, webInput.ts, their unit tests and browser self-tests, and the README's engine-limits and wheel text. Both are on main and independent; #9694 needs both.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the castStep and TrackedFrame.step docs say it comes from the JPEG's 4:2:0 chroma blocks, the only thing castStep reads, so please reword the older paragraph to the frame-header cause.

[Automated review]

Comment thread internal/interaction-harness/README.md Outdated
Comment on lines +149 to +151
`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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  • trackMarker gives every frame the JPEG's chroma block as its step whatever 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 in busy.webkittail.fixture.ts), so castStep would double the settledWithinSteps bound for any marker other than the default magenta; please set step only for the colour it was measured for (or have castStep refuse other tracks) and scope the TrackedFrame.step, castStep and 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},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 trackMarker gives each frame its position step; "each frame of the default marker" would match the code.
  • For awareness: the cap's extra frame lets settledWithinSteps: 2 pass a 202 px hold two frames long on busy.webkittail.fixture.ts that settledWithin: 4 fails; that follows the README's rule, and the three-frame plateau case depends on it.

[Automated review]

@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 17:38
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 3, 2026
@cixzhang
cixzhang marked this pull request as draft November 3, 2026 17:49
@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 17:49
@cixzhang
cixzhang merged commit 547d90a into meta:main Nov 3, 2026
148 of 153 checks passed
@vjeux
vjeux deleted the harness/paced-touch-cast-step branch November 3, 2026 18:44

This branch was successfully deployed

1 active deployment
Preview — 72535456 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants