Skip to content

test(interaction-harness): a busy proof excuses a hold at the block's start only at the position the page set - #9467

Draft
vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:test/busy-hold-set-before-block
Draft

vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:test/busy-hold-set-before-block

Conversation

@vjeux

@vjeux vjeux commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Maintainer impact

Two things in the interaction harness's busy-main-thread proof.

  • The harness's own self-test fails on a loaded runner with nothing wrong. "a compositor animation advances every frame through a blocked main thread; a rAF one freezes" required at most one frame of motion once the block began.
    • A loaded runner presents more of the frames the page drew just before the block.
    • The test failed 1 to 5 runs in 24, pinned to two cores with 4 workers.
  • The proof excused too much at the block's start. busyWindowProof excuses a hold that starts before the block and ends at a change inside it: WebKit holds a just-started compositor animation, such as a sheet's release, before presenting it.
    • It excused any such hold, including an animation that was already moving and froze at the block's start.
    • A 35 to 50 ms freeze from just before the block, followed by motion, passed.

Intended invariant

  • A hold at the block's start is excused only when it holds the position the page itself last set before the block, the one a late hand-off holds.
  • The rAF control is judged frozen from what the screen showed: a stall for most of the block, and the proof failing it against the control and alone. The count of frames already in flight when the block began is not part of the claim.

Change

  • busyWindowProof takes setBeforeBlock: the tracked value the page last set before the block, in the tracked box's coordinates, such as a drag's last position at its release.
    • The hold excuse applies only to a hold at that value, within a pixel (half of the coarsest cast's 2 by 2 color block).
    • Without it, there is no hold excuse, and the hold is judged as a stall.
    • The at-rest start rule is unchanged: a just-started animation seen at rest before the block.
  • The self-test's rAF control no longer requires at most one frame of motion once the block began. It keeps the 600 ms stall, the proof failing it against the control, and the bound failing it alone.
  • The hold self-test passes its hold position as the page's.
  • The README describes setBeforeBlock.

Evidence

  • Failure before the change (the hold excuse): three new unit cases fail on main.
    • an animation moving up to the block, frozen from the frame before it until 50 ms in, then moving on, passes (excused);
    • the sheet's hold is excused with no position given;
    • the sheet's hold is excused when it holds a position the page did not set.
  • Success after the change: all three fail as they should, and the sheet's hold at its released position is still excused. Harness units 109 of 109.
  • The excuse's other two rules stay pinned:
    • the case for a hold that starts inside the block passes the last position before the judged part as the page's;
    • a new case holds from the frame presented as the block starts, at the position the page set, which only the start rule refuses;
    • another shows the position the page set on one frame before the block, then no marker until 150 ms in, which only the two-frame rule refuses.
    • Removing either rule fails its case.
  • The self-test, same time, pinned to two cores with 4 workers ×24, load 46 to 54: main 19 of 24 (5 failures at "moved on [01] of …"); this branch 24 of 24, and 24 of 24 again at f069d11.
  • The freeze check still bites: a mutant that also moves the rAF run on the compositor through the block fails 3 of 3 (a 17 ms longest stall against the 600 required).
  • The whole harness spec in Chromium: 1 worker, ×2 at 4ec2bca (170 of 170) and ×1 at f069d11 (85 of 85).

Callers in open pull requests

Only a proof whose animation starts at a release just before its block, and is held at the block's start, used the hold excuse. If one of these now fails with a stall at the block's start, pass setBeforeBlock (the release position in the tracked box's coordinates) when rebasing.

Specs that call busyWindowProof in open pull requests, with the first pull request that adds each:

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

internal/interaction-harness: its unit tests, and harness.interaction.browser.spec.ts in Chromium (playwright.interaction.config.ts).

Local gates at 81a2fae:

  • harness units (109);
  • the harness spec;
  • the harness typecheck;
  • pnpm lint (0 errors);
  • changeset coverage (nothing ships).

… start only at the position the page set

busyWindowProof excused a hold that starts before the block and ends at a
change inside it, as WebKit holds a just-started compositor animation (a
sheet's release) before presenting it. It excused any such hold, even an
animation that was already moving and froze at the block's start: a hold
of 35 to 50 ms from just before the block, then motion, passed. The excuse
now needs `setBeforeBlock`, the value the page itself last set before the
block, and applies only to a hold at that value (within a pixel, the
coarsest cast's half block); without it, the hold is judged as a stall.

The harness's own rAF control ("a rAF one freezes") required at most one
frame of motion after the block began. A loaded runner presents more of
the frames the page drew before the block (two in 1 to 5 runs of 24, pinned
to two cores), so the control failed with nothing wrong. It now checks the
freeze from the page's own writes: no position set while the block ran,
the screen stalled for most of it, and the proof failing it against the
control as before. The hold self-test passes its hold position as the
page's.
@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 6:46am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 2, 2026, 6:56 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

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: narrowing the hold excuse to setBeforeBlock works and fails closed, but it leaves the excuse's other two rules untested. is not excused when it starts inside the block in busy.control.test.ts still passes no setBeforeBlock. Since this change, that test passes whatever the start rule and the two-frame rule in busy.ts say, and removing both rules keeps every harness unit test green. Passing the last value before the judged part as setBeforeBlock in that test restores its coverage, for example frames.filter(f => f.t < BLOCK.from + 20).at(-1)!.box!.y.

[Automated review]

// cast holds color in 2 by 2 pixel blocks, so a position reads up to a pixel
// off.
const heldFromBeforeBlock =
setBeforeBlock != null &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With this gate, is not excused when it starts inside the block in busy.control.test.ts (which passes no setBeforeBlock) can no longer reach the start rule and the two-frame rule below. Removing both rules leaves every harness unit test passing. Passing frames.filter(f => f.t < BLOCK.from + 20).at(-1)!.box!.y as setBeforeBlock in that test restores the coverage.

// block still reach the screen as it starts (one or two, more on a loaded
// runner), and then the compositor sends nothing while nothing changes,
// so the bound's verdict fails it as the control's does.
expect(setDuringBlock).toBe(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: this assertion can't fail while blockMainThread without during spins inside a single page.evaluate, because no rAF callback runs during that loop. The stall and failure-message checks below are what prove the freeze.

… a freeze check that cannot fail

With the excuse gated on `setBeforeBlock`, the case for a hold that starts
inside the block passed no position and so no longer reached the excuse's
start rule or its two-frame rule: removing both left every unit green. It
now passes the last position before the judged part, and a new case holds
from the frame presented as the block starts, at the position the page
set, which only the start rule refuses. Removing the start rule fails one
case; removing both rules fails two.

The rAF control's count of positions the page set during the block could
not fail: the block is one loop in one task, so no frame callback runs in
it. The 600 ms stall and the verdict's failures carry the freeze.
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both addressed in f069d11.

  • Start and two-frame rules: is not excused when it starts inside the block now passes the last position before the judged part as setBeforeBlock, as you suggested, so it reaches the excuse's other rules again.
  • The start rule alone: a new case, is not excused when its first held frame is the one at the block start, holds from the frame presented as the block starts, at the position it is given. Only the start rule refuses it.
    • Removing the start rule fails that case.
    • Removing the start rule and the two-frame rule fails both cases.
  • The two-frame rule alone changes no verdict in these fixtures. A hold that starts before the block and reaches the judged part shows on every frame in between, so it has two frames whenever a frame lands there. When none does, judging starts at the next presented frame either way.
  • The non-blocking point: agreed, and the count of positions set during the block is gone. The 600 ms stall and the verdict's failures carry the freeze.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: the start rule is now pinned and the description matches the head, but the two-frame rule still has no test. Deleting heldFrames >= 2 in busy.ts alone leaves every harness unit passing; judges a gap at the edge of the judged part from the screencast alone in busy.test.ts caught that on the base, and passing setBeforeBlock: frames.filter(f => f.t < BLOCK.from).at(-1)!.box!.y in that case restores the coverage.

[Automated review]

holdStart > 0 &&
prior[holdStart].t < window.from &&
heldFrames >= 2;
heldFrames >= 2 &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deleting this line alone leaves every harness unit passing. judges a gap at the edge of the judged part from the screencast alone in busy.test.ts pinned it on the base, but it passes no setBeforeBlock, so since line 640 it never reaches this rule. Passing setBeforeBlock: frames.filter(f => f.t < BLOCK.from).at(-1)!.box!.y in that case restores the coverage.

A case where the position the page set shows on one frame before the
block, and the marker is then not found until 150 ms in: only the
two-frame rule refuses to excuse it, so removing that rule now fails it.
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Right, and I was wrong that it could not change a verdict. Fixed in 81a2fae.

  • The new case: is not excused when the position it holds shows on one frame only. The position the page set shows on one frame before the block, then the marker is not found on any frame until 150 ms in.
  • Why only that rule refuses it: a position shown on one frame is no hold, so the window is judged from the judged part's start. Without the two-frame rule, judging would start at the first position after the gap.
  • Mutants: removing heldFrames >= 2 now fails that case, and removing the start rule fails its own case.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No further changes needed: the hold excuse now requires setBeforeBlock, each of its three conditions (the 1 px match, the start rule and the two-frame rule) fails its own unit case when removed, and the changed self-tests pass in Chromium and WebKit. Open pull requests whose proofs rely on the hold excuse will need to pass setBeforeBlock with the release position when they rebase.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 81a2fae6 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

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.

1 participant