Skip to content

test(Slider): the busy-main-thread coast proof runs in WebKit on the paced touch play - #9694

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:slider/webkit-busy-proof
Nov 5, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:slider/webkit-busy-proof

Conversation

@vjeux

@vjeux vjeux commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

#9545, #9725 and #9956 are merged, so this pull request is one commit on main.

Maintainer impact

#8190 shipped the Slider's busy-main-thread coast proof in Chromium only. Before #9725 and #9956, WebKit's injected touches could bunch behind a stalled dispatch, and its JPEG cast moves in 2 px steps, so the proof could not judge the coast there. This runs it in WebKit too, on #9725's paced touch play, which #9956 paces from WebKit's answers, and #9545's cast-step settle.

Evidence

Intended invariant

The busy proof judges the Slider's coast through a blocked main thread in both engines. It fails only for the coast, or names the runner's fault.

Change

  • The proof plays its 900 px/s flick with pacing: 'spacing', so a stalled dispatch stretches one gap. A page that read a resting finger, or no throw, fails the play and names whose fault it was.
  • It bounds the settle at settledWithinSteps: 2: 2 px in Chromium's cast, 4 px in WebKit's.
  • Its verdict, annotation and evidence JSON carry the release the page read beside the script's, so a throw that a slow runner stretched names the runner.
  • The engine guard that kept it to Chromium is gone.

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 7fa7b33, on main 70d6b46, on a Storybook build of that main: typecheck, CI=true pnpm lint (no errors, and no warnings in the changed spec), Prettier, changeset coverage (nothing ships), pnpm review:pre (no findings), the busy proof ×40 in WebKit in the CI job's container profile (40 passed) and ×20 in Chromium on a 2-thread pin (one physical core; 20 passed), the Slider's interaction specs in WebKit in the same profile (19 passed, 4 behind engine guards) and in Chromium (23 passed), the Slider's Chromium a11y contracts (30 passed), and the full unit suites (test:ui 20,435 passed; test:node 16,529 passed).

@vercel

vercel Bot commented Nov 3, 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 5, 2026 1:53am UTC

Request Review

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

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 5, 2026, 2:04 AM UTC

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 7fa7b33

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
Slider · Storybook 92.3 kB (0) · 8.3 kB CSS (0) 2,333 (0) ✓ · ✓ · ✓

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

API changes: none in this pull request

Committed API report behind this build: cli 9 rows, core 3 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


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 busy proof's new step-counted rule needs one fix, and the rest of the change reads well.

  • Under settledWithinSteps, the still-run allowance in internal/interaction-harness/src/busy.ts (lines 987-1000) takes the slower of the paces around a run, so busy.webkittail.fixture.ts held at 212 px for 82 ms (four 2 px steps from rest) and then resumed passes, while settledWithin: 4 fails it as an 82.2 ms stall; capping the allowance at ceil(step / max(paceBefore, catchUp / runFrames)) + 1 fails that hold, keeps every existing busy.test.ts and busy.control.test.ts case passing, and the hold makes a good must-fail case.

The paced play's release reading matches Core's VelocityTracker, and running the Slider proof in WebKit is what AST-094 asks for. Two smaller points: the touchPacingFault and PlayOptions.pacing JSDoc and the README's engine-limits row leave out the 'throw' fault, and a harness test that compares readRelease with VelocityTracker would stop the copied filter from drifting. CI skipped this draft, so the WebKit interaction lane still needs to pass once it is marked ready.

[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 a freeze that resumes a slow tail gets the tail's long allowance. On busy.webkittail.fixture.ts, holding 212 px from +115.4 ms to +197.6 ms (rest is 220 px) and then replaying the rest of the coast passes under settledWithinSteps: 2, while settledWithin: 4 (the same 4 px bound on this cast) fails it with "longest stall 82.2 ms". Capping the allowance at one frame past what the pace before the run allows fixes that hold and keeps all 50 busy.test.ts and 24 busy.control.test.ts cases passing:

const beforeCap =
  Math.ceil(
    castStepPx /
      Math.max(paceBefore ?? 0, runFrames > 0 ? catchUp / runFrames : 0) -
      1e-6,
  ) + 1;
return (
  unchanged <= Math.min(Math.ceil(castStepPx / travel - 1e-6), beforeCap)
);

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 #9545's 9304656, which this pull request now sits on: the still-run allowance is capped at one frame past what the pace into the run allows, as you suggest. Your hold (212 px from +115.4 to +197.6 ms, the coast replayed four frames late) is a must-fail case there; it passed before and fails with "longest stall 82.2 ms", as settledWithin: 4 does, and the other busy cases still pass. The Slider proof here re-ran against the cap: 10 of 10 in WebKit in the CI job's profile, 20 of 20 in Chromium.

@vjeux
vjeux force-pushed the slider/webkit-busy-proof branch from cf530ed to de951a8 Compare November 3, 2026 05:38
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

The two smaller points: the touchPacingFault and PlayOptions.pacing docs, the README's engine-limits row and the pacing test's header now name the throw fault (#9545 at 9304656). A harness test that calls VelocityTracker is not possible here: the repository's @astryx/no-core-internal-import rule reports any import of core/src/gestures from outside Core and Lab, the harness included. So coreRelease stays a copy, with a SYNC note naming VelocityTracker.ts, and the harness's unit cases pin the same rules (moved samples only, a same-time sample replacing the last, an interval across no time reading 0, fewer than two moves or a rest reading 0).

@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 allowance fix works, and one scope item remains.

  • This branch targets main but carries #9545's seven harness commits, so it is 2,042 authored lines (over the 2,000-line limit) where its own Slider change is 20; land #9545 first, then rebase this branch on main so it carries only the Slider spec commit.

The capped still-run allowance now fails a WebKit coast that stalls and then resumes a slow tail, and the fault docs name throw. Two small follow-ups: the README's still-run paragraph and the busyWindowProof JSDoc still describe the allowance without the new cap, and a Core-side unit test comparing readRelease with VelocityTracker would guard the copy from both sides, since Core may import the harness. CI skipped this draft, so the WebKit interaction lane still needs to pass once it is marked ready.

[Automated review]

* nearest step. The run also has to read as a slowing
* curve, at most one frame longer than the component's run before it.
* 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

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.

Non-blocking: this sentence, and README.md lines 228-230, still describe the allowance as ceil(step ÷ travel) frames "whatever the run before it". Since the cap at lines 1001-1011, a run may also last at most one frame more than the pace into it allows; worth saying here and in the README.

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 #9545 at fe42259: the README and the busyWindowProof doc now name the cap.

@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

This pull request is stacked on #9545. A fork's pull request can't target #9545's branch, so it targets main and carries #9545's commits until that merges. Its own change is one commit of 20 lines in the Slider spec, which the body's compare link (9304656...de951a8) shows alone. When #9545 merges, this rebases onto main as that one commit.

@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

#9545 is now split, so this pull request depends on #9545 and #9725, and its compare link shows only its own commit. The Core-side cross-check you suggested is in #9725: VelocityTracker.harness.test.ts reads seven gestures with VelocityTracker and with the harness's readRelease, and expects them equal. A copy with another weight fails four of them.

@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: one scope item remains, and the earlier documentation and drift-guard points are addressed.

  • This branch targets main but carries the harness commits of #9545 and #9725, so it is 2,181 authored lines (over the 2,000-line limit) where its own Slider change is 20; land #9545 and #9725 first, then rebase this branch on main so it carries only the Slider spec commit.

The README and the busyWindowProof doc now state the still-run cap, and VelocityTracker.harness.test.ts catches a change to the copied filter's weight and sample rules. One small follow-up: its only rest case lifts 88 ms after the last move, so a harness stillness interval anywhere from 40 to 87 ms still passes, and a rest 41 ms after the last move (or a direct comparison of TOUCH_STILLNESS_MS with UIKIT_VELOCITY.stillness) would pin it. CI skipped this draft, so the WebKit interaction lane still needs to pass once it is marked ready.

[Automated review]

[116, 50, 16],
[132, 50, 32],
],
lift: 120,

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.

Non-blocking: this is the only rest case, and it lifts 88 ms after the last move, so a harness TOUCH_STILLNESS_MS anywhere from 40 to 87 ms still passes all seven cases (the harness's own boundary tests read that constant, so they move with it). A rest that lifts 41 ms after the last move, or a direct expect(TOUCH_STILLNESS_MS).toBe(UIKIT_VELOCITY.stillness.value) beside the weight, would pin 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 #9939 on main: two gestures lift exactly at UIKIT_VELOCITY.stillness and 1 ms past it, and a check confirms that the tracker reads a speed at the first and none at the second. A harness copy with a 39 or 41 ms interval, or a >= bound, now fails one case.

@vjeux
vjeux force-pushed the slider/webkit-busy-proof branch from 57b4be0 to 464ea7f Compare November 4, 2026 11:00
@vjeux

vjeux commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

#9545 and #9725 are merged, so this branch is now one commit on main: the Slider spec change (+11 −9). The stillness-interval pin you suggested goes in its own pull request on main, since it changes the Core cross-check, not this spec.

@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 WebKit run of the Slider busy proof can fail on a loaded runner without naming the runner.

  • Miss: the flick plays with pacing: 'spacing' but no releaseTolerance (SliderTouch.interaction.browser.spec.ts:1330), so when each WebKit dispatch outlasts the script's 8.3 ms interval the page reads about 245 px/s instead of 840 px/s, the coast is too short to judge, and the proof fails with "too little motion in this window to judge" instead of the named runner fault the description promises; pass a releaseTolerance (0.5 turned every such run into TouchPacingError: … a dispatch stalled before the page) or narrow the stated invariant.

The earlier size item is resolved: the branch is now one 20-line commit on main. Note that CI's WebKit lane does not run Slider specs, so this proof's WebKit runs stay local; once the branch is ready, a maintainer adds the ci:run label so CI runs the Chromium proof.

[Automated review]

// stalled dispatch. A page that read a resting finger, or no throw,
// fails the play and names whose fault it was.
await driver.play(flick(at, {velocity: {x: 900, y: 0}, durationMs: 60}), {
pacing: 'spacing',

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 no releaseTolerance, a stretched WebKit flick (each dispatch slower than the 8.3 ms interval) reads about 245 px/s instead of 840 px/s and still passes the play. The short coast then fails the proof with "too little motion in this window to judge", which names neither the coast nor the runner. A tolerance turns it into a named TouchPacingError:

await driver.play(flick(at, {velocity: {x: 900, y: 0}, durationMs: 60}), {
  pacing: 'spacing',
  releaseTolerance: 0.5,
});

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.

I left the tolerance out because a 0.5 bound fails healthy runs. In WebKit in the CI job's profile (4 CPUs, ten runs per arm, alternating), the play with your snippet failed 4 and then 5 of 10, each a TouchPacingError reading 269 to 2,227 px/s against the script's 910. Without it, 9 of 10 passed.

The one red there was a BusyWindowTooLateError: the coast was 63 ms in when the page first saw it, past its curve's end at 58 ms. The one red in 100 runs of a local probe gave the same numbers, and there the page had read a bunched 6,493 px/s release. The paced play sent each touch its interval after the previous one left, so touches WebKit took late reached the page 1 to 3 ms apart, and the coast hit the track's end. #9956 fixed that: each touch now also waits its interval, less 2 ms, after WebKit answers the previous one. In the same probe, the 25 slowest releases (271 to 449 px/s) coasted 478 to 519 ms and passed, so "too little motion" did not occur.

At c544be2, on main with #9956, the busy proof passed 40 of 40 in WebKit in the CI job's profile.

@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 Slider's new WebKit busy proof can still fail on a slow runner without naming the runner.

  • Miss: with pacing: 'spacing' alone (SliderTouch.interaction.browser.spec.ts:1330), a runner whose WebKit dispatch takes about 22 ms delivers the flick at about 250 px/s instead of 840 px/s, and 2 of 22 runs failed with "too little motion in this window to judge", which names neither the coast nor the runner as the description's invariant promises.

Thanks for the tolerance data: it rules out a release tolerance, so either keep driver.play's result and put the lift's release (page and script px/s) in the final expect message and the evidence JSON, or narrow the invariant to say a stretched throw can end in "too little motion to judge". The rest reads well: removing the skip, the paced play and the step-counted settle give the WebKit busy proof that AST-094 asks for, and the Chromium proof still passes.

[Automated review]

// when it arrives, so the play keeps the script's spacing after a
// stalled dispatch. A page that read a resting finger, or no throw,
// fails the play and names whose fault it was.
await driver.play(flick(at, {velocity: {x: 900, y: 0}, durationMs: 60}), {

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.

On a runner whose WebKit dispatch takes about 22 ms, this flick reaches the page at about 250 px/s instead of 840 px/s, and 2 of 22 runs failed "too little motion in this window to judge", which names neither the coast nor the runner. Since a tolerance fails healthy runs, keeping the play's result and showing its release in the failure would name the slow throw:

const played = await driver.play(
  flick(at, {velocity: {x: 900, y: 0}, durationMs: 60}),
  {pacing: 'spacing'},
);
// ...
const release = played.at(-1)?.release;
expect(
  proof.verdict.failures,
  release == null
    ? proof.summary
    : `${proof.summary}; the page read ${release.pagePxPerS.toFixed(0)} px/s of the script's ${release.scriptPxPerS.toFixed(0)} px/s`,
).toEqual([]);

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 7fa7b33, with your snippet's message. The play's result is kept, and the verdict, the annotation and the evidence JSON carry the lift's release.

A local probe that added 20 ms after each WebKit answer reproduced your case. 1 of 22 runs failed "too little motion in this window to judge" on both heads, and on this one the failure also reads "the page read 184 px/s of the script's 910 px/s". On a healthy runner, the busy proof passed 40 of 40 in WebKit in the CI job's profile.

…paced touch play

The proof plays its flick with `pacing: 'spacing'`, so a stalled dispatch
stretches one gap instead of bunching the throw. A page that read a
resting finger, or no throw, fails the play and names whose fault it was.
It bounds the settle in two of the cast's own steps, so WebKit's 2 px
plateaus near rest are not stalls. Its verdict names the release the
page read beside the script's, so a throw that a slow runner stretched
names the runner. The engine guard that kept it to Chromium is gone.

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

Approved pending CI: CI has not run on this head yet.

The Slider's busy-main-thread coast proof now runs in WebKit, and the earlier concern is resolved: a red caused by a slow runner's stretched throw now ends with the release the page read beside the script's, in the failure message, the annotation and the evidence file.

Next step: once the pull request leaves draft, a maintainer adds the ci:run label so CI runs. CI's WebKit lane does not plan Slider specs, so the WebKit run of this proof stays local.

[Automated review]

@vjeux
vjeux marked this pull request as ready for review November 5, 2026 02:11
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 5, 2026
@cixzhang
cixzhang marked this pull request as draft November 5, 2026 02:15
@cixzhang
cixzhang marked this pull request as ready for review November 5, 2026 02:16
@cixzhang
cixzhang merged commit 195fcfd into meta:main Nov 5, 2026
165 of 170 checks passed
@vjeux
vjeux deleted the slider/webkit-busy-proof branch November 5, 2026 05:14

This branch was successfully deployed

1 active deployment
Preview — 7fa7b33f Deployed Nov 5, 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