Skip to content

test(ProgressBar, CircularProgress): motion traces measure what they claim on a loaded runner - #9413

Merged
cixzhang merged 3 commits into
meta:mainfrom
vjeux:test/motion-traces-loaded-runner
Nov 2, 2026
Merged

cixzhang merged 3 commits into
meta:mainfrom
vjeux:test/motion-traces-loaded-runner

Conversation

@vjeux

@vjeux vjeux commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Follows #9378, which reads these specs' screencast frames by timestamp.

Maintainer impact

Checks in the progress motion specs fail on a loaded runner without anything wrong in the product. Each reads a property of the motion from which screencast frames the runner happened to capture. Pinned to two cores with 4 workers:

  • CircularProgress's block test required at least 10 distinct arc reaches inside a main-thread block. It read as few as 4, and was under 10 in 14 of 80 runs.
  • ProgressBar's storyboard check "no jump bigger than a quarter of the travel between frames" failed 3 of 20 runs.
  • ProgressBar's block test failed on "the block started before 80% of the trip" (287 px against 273.8). Under heavier load it also failed its count of at least 5 distinct positions.

Intended invariant

Each check judges what it claims, whatever the screencast's capture rate:

  • the motion's per-frame step;
  • that the arc keeps moving through a block;
  • where the fill was when the block began.

Change

  • ProgressBar storyboard, on the change's own clock.
    • The page holds the fill's transition as it starts (armHold), and the harness's animationStoryboard films that held set: the transition seeked one 60 Hz interval at a time and photographed, through its end.
    • The forward-only, endpoint and jump checks, the JSON trace and the contact sheet read the storyboard's frames. They are the same 74, 123, 164, 199… px steps a full-rate capture shows, on every run.
    • Before, the checks differenced consecutive captured frames: one missed capture doubled a step (122 then 199 px against 55.5).
    • CircularProgress's storyboard films the same way.
  • CircularProgress block test, against the harness's busy-window control.
    • Why the count failed: the arc's visible motion inside the block lasts only ten to twelve 60 Hz frames. The theme's easing makes most of the trip in the first ~60 ms, and the block starts 20 to 50 ms into the change. The count needed nearly every one of those frames captured.

    • Now: a box the compositor moves along the top edge (startBusyControl) is in the same screencast. busyWindowProof judges the arc against it. The arc's reach must change on every frame the control moved and never stall longer than the control, until it settles. A capture gap thins both alike.

    • The control runs only through the block. Its first turn comes after the judged part, and the page is still again when the screencast stops.

    • Each frame is decoded once. The arc's reach is read at the ring in the viewport frame, the same pixels a crop to the ring holds: 8 runs of 48 frames each read alike both ways, so no crop pass is needed.

    • It settles 3 degrees short of its end (settledWithin: 3), from the reading's own mechanics. Held and seeked every 2 ms, the change reads:

      Reach (degrees) …322 323 324 325 326 327 328
      ms of the change it lasts 2–6 each 8 4 12 12 18 rests
      • From 325 on, each 1-degree step of the reading takes about a frame or more, so two frames can read the same degree while the arc still moves. Before 325 the reading changes every frame.
      • With the usual 2, the proof judges 325 and fails correct runs on that plateau. It did so in 3 of 3 unloaded runs, moving on 9 of the control's 10 frames.
    • One piece, never backwards and landing at 90% stay.

  • ProgressBar block test: judged against the control, its block start read on the page.
    • The test sets the value, and once the transition starts and one more frame is presented, blocks. That start falls 18 to 42 ms into the change (30 runs at the same load).
    • The check read the fill's position from the first frame the screencast captured after the block began, and a capture gap made that frame late.
    • The block's own task now reads where the fill's leading edge is as it starts.
    • The distinct-positions count (at least 5) is replaced by the same busy proof as CircularProgress's block test. The fill's edge must change on every frame the control moved and stall no longer than the control, until it is within half a pixel of its end.
      • Held and seeked every 2 ms, each half-pixel step of the reading takes 10 ms or less until the last, which takes 20 ms, longer than a frame.
      • The control runs only through the block, above the track.
    • Arrival before the block ends and never running backwards stay.
    • What the test cannot arrange is the main thread reaching the block before the fill arrives. The page can block only after the frame that hands the transition to the compositor. A loaded machine can then keep the main thread from the block for most of the move; on a loaded box the block began anywhere from 22% to 95% of the trip.
      • Trying the block from a task queued in the start frame, one frame earlier, moved nothing.
      • So that is the test's one precondition: the fill still had a step of the cast (half a pixel) to go when the block began, read on the page. Its margin is the motion's own 300 ms, not a timing tuned to load.

Evidence

  • Same-time comparison, both specs, pinned to two cores with 4 workers ×40:
    • main 837 of 840: the storyboard's jump check twice, CircularProgress's distinct count once;
    • this branch 840 of 840.
  • Failure before the change (seams on main's specs, 5 runs each):
    • one capture missed right after the fill starts moving: the storyboard's jump check fails 5 of 5 ("Expected: < 55.5");
    • alternate frames dropped: CircularProgress's distinct count fails 5 of 5 ("Expected: >= 10");
    • the block's first 100 ms of frames dropped: ProgressBar's old block-start check fails 5 of 5 (287 and 302 px against 273.8).
  • Success after the change:
    • the new storyboard never reads the screencast;
    • the block-start seam passes 5 of 5;
    • a block started 110 ms late passes too, its overlap judged against the control.
  • The checks still catch what they claim (mutants):
    • a fill moving in four held steps (steps(4, jump-none), endpoints right) fails the storyboard's jump check 5 of 5 (74 px);
    • the arc paused (and the pause committed) through the block fails the busy proof 10 of 10: it moved on 0 of 39 frames;
    • a fill paused through the block fails its busy proof 5 of 5: it moved on 0 of 39 control frames;
    • a block that starts after the fill arrived (400 ms late) fails the precondition 5 of 5.
  • Too little to judge: the proof refuses to judge fewer than 4 control frames.
    • Pinned to two cores with 4 workers, no run came out too little to judge: the control moved on 8 to 11 frames of the judged part.
    • With alternate frames dropped (a 30 fps capture), it still judged 5 or 6 control frames and passed 5 of 5.
  • The specs with 1 worker: both specs ×10 at 08c57fa, 210 of 210; ×5 at this head, 104 of 105.
    • The one failure is CircularProgress's tracing test ("lays out and paints nothing per frame"), which this change does not touch.
    • Its change had ended by the time it checked that the change was still running, after a slow start of tracing.
  • Residual, stated plainly: a run fails when the main thread does not reach the block before the fill arrives, or when the capture stalls longer than the fill's remaining move.
    • Neither occurred in the 120 runs above at this head.
    • At the heaviest load today (box load ~80), the old block-start check failed 2 of 80 runs and the old count 2 of 80.

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 074bb65:

  • Prettier and ESLint on both specs;
  • the core and lab typechecks;
  • pnpm lint (0 errors);
  • the interaction harness's unit tests (104);
  • pnpm check:repo and pnpm check:knowledge;
  • changeset coverage (nothing ships).

vjeux added 2 commits November 1, 2026 13:35
…claim on a loaded runner

Three checks read a property of the motion from which screencast frames a
loaded runner happened to capture.

- ProgressBar's storyboard judged "no jump bigger than a quarter of the
  travel between frames" between consecutive captured frames: one frame the
  capture misses doubles the step between the two around it (a missed frame
  read 122 then 199 px against a 55.5 px bound). The storyboard now runs on
  the change's own clock: the page holds the fill's transition as it starts
  (`armHold`), and each frame is the transition seeked one 60 Hz interval on
  (`seekAnimations`) and photographed, through its end. The checks, the JSON
  trace and the contact sheet read those frames.
- CircularProgress's block test required at least ten distinct reaches of
  the arc inside the main-thread block. The arc's visible motion inside the
  block lasts about ten to twelve 60 Hz frames, so the count needed nearly
  every one captured; under load it read as few as 4. The test now judges
  the arc against the harness's busy-window control, a box the compositor
  moves in the same screencast (`startBusyControl`, `busyWindowProof`): the
  arc's reach must change on every frame the control moved and stall no
  longer than the control, until it settles within 3 degrees of its end.
  On the change's own clock each 1-degree step of the reading takes 2 to
  8 ms up to 324 degrees, then 12, 12 and 18 ms for 325, 326 and 327 before
  it rests at 328, so from 325 two frames can read the same degree while
  the arc still moves. The control runs only through the block: its first
  turn comes after the judged part, and the page is still again when the
  screencast stops. The arc's reach is read at the ring in each viewport
  frame, the same pixels a crop to the ring holds, so each frame is
  decoded once. One piece, never backwards and landing at 90% stay.
- ProgressBar's block test checked that the block started before the fill
  had made 80% of its trip from the first frame the screencast captured after
  the block began; a capture gap there read the fill at 287 px against a
  273.8 px bound, though the block had started 18 to 42 ms into the change.
  The block's own task now reads where the fill's leading edge is as it
  starts, and the check judges that.

Seams that thin the capture turn the old checks red: one missed frame
fails the storyboard's jump check 5 of 5, dropping alternate frames fails
the distinct count 5 of 5, and dropping the block's first 100 ms of frames
fails the block-start check 5 of 5 (287 and 302 px). Mutants show the new
checks still catch what they claim:
- a fill moving in four held steps fails the jump check 5 of 5;
- the arc paused through the block fails the busy proof 10 of 10 (it moved
  on none of the frames the control moved);
- a fill paused through the block fails its block test 5 of 5;
- a block that really starts 110 ms late fails the block-start check 5 of 5.
…ndow control

The test required at least five distinct leading-edge positions among the
screencast frames inside the main-thread block, a count a loaded capture
thins. It now judges the fill as CircularProgress's block test judges its
arc: against the harness's busy-window control in the same screencast,
the fill's edge must change on every frame the control moved and stall no
longer than the control, until it is within half a pixel of its end. On
the change's own clock each half-pixel step of the reading takes 10 ms or
less until the last, which takes 20 ms, longer than a frame.

What the test cannot arrange is that the main thread reaches the block
before the fill arrives: the page can block only after the frame that
hands the transition to the compositor, and a loaded machine can keep
the main thread from the block for most of the move. The check that the
block began before 80% of the trip becomes that precondition: the fill
still had a step of the cast to go, read on the page as the block began.
@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 2, 2026 5:08am UTC

Request Review

@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 2, 2026, 5:16 AM UTC

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

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 074bb65

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
ProgressBar · Storybook 7.9 kB (0) · 6.9 kB CSS (0) 413 (0) ✗ · ✓ · ✓
lab/CircularProgress · Storybook 8.5 kB (0) · 1.1 kB CSS (0) 597 (0) — · ✓ · —
Visual changes: 2 snapshots in 1 component

A change is a question for the reviewer, not a failure.

Component · story Before After Diff
ProgressBar · Default (neutral, dark) before frame after frame diff frame
ProgressBar · Default (neutral, light) before frame after frame diff frame

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

API changes: none in this pull request

Committed API report behind this build: core 1 row; 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.

Requesting changes: the reworked motion checks look sound and keep the ProgressBar FR4 evidence, but the new ProgressBar storyboard loop re-implements the interaction harness's animationStoryboard instead of calling it. Please pass the armed hold's held to animationStoryboard and read storyboard.frames for the edge checks, trace and contact sheet; if the half-pixel edge reading needs device-scale frames, add a capture-scale option to Storyboard rather than keeping a local copy. The changed Chromium specs have not run in CI yet because the pull request is a draft.

[Automated review]

Comment on lines +811 to +823
const held = await hold.held();
const end = Math.max(...held.map(a => a.offsetMs + a.curveEndMs));
const times: number[] = [];
for (let t = 0; t < end; t += FRAME_MS) {
times.push(t);
}
times.push(end);
const frames: Frame[] = [];
for (const t of times) {
const shown = await seekAnimations(page, t);
frames.push({t: shown, data: (await page.screenshot()).toString('base64')});
}
await releaseAnimations(page, {finish: true});

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 grid-seek-photograph loop is what animationStoryboard in internal/interaction-harness/src/storyboard.ts already does, and it takes an armed hold's held for exactly this case. Calling it keeps one path: the shared loop also retries Chromium's empty surface copies and skips held animations that never end (here end would be Infinity and this loop would not terminate). If the edge reader needs device-scale frames, a capture-scale option on Storyboard covers it.

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 2, 2026
@cixzhang
cixzhang marked this pull request as ready for review November 2, 2026 01:02
… animationStoryboard

The storyboard held the fill's transition with armHold and filmed it with
its own seek-and-photograph loop. It passes the armed hold's set to the
harness's animationStoryboard instead, which seeks the same 16.7 ms grid
to the curve's end, and reads the storyboard's frames for the edge checks,
the JSON trace and the contact sheet. The checks read whole CSS pixels
from those frames, the precision they assert.

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

Recommending approval: the ProgressBar storyboard now films the held transition with the interaction harness's animationStoryboard, which resolves the earlier request, and both block tests judge the motion against the compositor control instead of a capture count. The storyboard's whole-pixel frames are enough for its checks, and no product behavior changes.

[Automated review]

github-actions Bot added a commit that referenced this pull request Nov 2, 2026
@cixzhang
cixzhang merged commit 30c3fdf into meta:main Nov 2, 2026
61 checks passed
@vjeux
vjeux deleted the test/motion-traces-loaded-runner branch November 2, 2026 05:41

This branch was successfully deployed

1 active deployment
Preview — 074bb65a Deployed Nov 2, 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