Repository navigation
fix(interaction-harness): a run held while its resume is pending keeps its place at any playback rate - #9109
Conversation
…s its place at any playback rate holdAnimations took the start of a run whose play is still pending (no start time yet) as now less its current time, which holds only at rate 1. A run at rate 2 covers its curve in half the timeline, so a run resumed 400 ms into its curve and held before its start time resolved was given a start 200 ms too early, and a seek to the moment of the hold put it 400 ms further along its curve than the hold found it, in Chromium and WebKit. Its start now reaches back through its playback rate, as seeking and sampling read it. A new case resumes a rate-2 run inside the hold's own read of the page, so its resume is pending when the hold takes it, and seeks to the moment of the hold: both held runs stay where the hold read them, to a millisecond. Before this change the faster run lands 400 ms off in both engines.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
No new or modified components detected. Bundle Size SummaryNo component packages changed. Accessibility AuditStatus: The accessibility audit did not run on this pull request. It runs after merges to Generated by PR Enrichment workflow | View full report |
There was a problem hiding this comment.
The holdAnimations fix is correct: the new case fails against the old formula and passes with the new one, and the existing hold cases still pass. One change is needed, in the new self-test. Its resume fires in the recorder's first getAnimations() read during installRecorder, not in the hold's own read, so the run is still pending at the hold only if two page round trips finish before the browser resolves the play; when they are slower, heldAtMs reads about 467 and the exact toBe(400) fails on correct code. Triggering the resume from a read only the hold makes, such as document.timeline.currentTime (which holdAnimations reads first and the recorder never does), keeps the run pending whatever the round trips cost; please update the test comment and the description to match.
[Automated review]
| await new Promise(resolve => | ||
| requestAnimationFrame(() => requestAnimationFrame(resolve)), | ||
| ); | ||
| // Resumed inside the hold's own read of the page's animations. |
There was a problem hiding this comment.
This resume does not fire inside the hold's read. On this fresh page, the first getAnimations() call after the override is the recorder's initial record(), which holdAnimations reaches through installCurveEnd → installRecorder, two page round trips before its own read. When those round trips are slow, the run already has a start time at the hold, and heldAtMs is no longer 400 (line 2139). A trigger on document.timeline.currentTime, which holdAnimations reads first and the recorder never reads, resumes the run in the hold's own task:
const {get} = Object.getOwnPropertyDescriptor(AnimationTimeline.prototype, 'currentTime')!;
Object.defineProperty(document.timeline, 'currentTime', {
configurable: true,
get() {
delete (document.timeline as {currentTime?: unknown}).currentTime;
faster.play();
return get!.call(document.timeline);
},
});…its run inside the hold, and runs long enough The self-test resumed its rate-2 run from a one-shot patch of document.getAnimations, but the harness's animation recorder, which the hold installs, lists the page's animations itself, at install and on every frame: it took the patch, and the run resumed a frame or more before the hold, no longer pending (2 cores: 16 of 24 runs failed, held at 433 to 500 ms). The run now resumes from the hold's own first read of the timeline's time, caught once on the prototype that defines it; the recorder never reads it, and a property on the timeline object itself can be lost with its wrapper. Both runs also last 60 s: on a loaded WebKit runner the hold came after #box's 2 s run had ended (20 of 20 failed). Pinned to two cores it passes 24 of 24, and 40 of 40 in a WebKit container capped at 3 CPUs at 16 workers; with main's hold it fails every run in both, the resumed run 400 ms off.
|
Fixed in da89ae6, two causes in the self-test:
Pinned to two cores it passes 24 of 24, and 40 of 40 in WebKit capped at 3 CPUs at 16 workers; with the old formula it fails every run in both, 400 ms off. |
There was a problem hiding this comment.
The holdAnimations fix and its self-test are ready: the new case fails against the old formula, passes with the new one, and the existing hold cases still pass. The earlier concern is resolved, since the resume now fires from the hold's own document.timeline.currentTime read, so the run is still pending when the hold lists it however slow the page round trips are. Nothing further is needed for this change.
[Automated review]
Maintainer impact
holdAnimationsplaced a run whose play was still pending (resumed, or just made, with no start time yet) as if it ran at rate 1. A run at another playback rate, held before its start time resolved, was then seeked and sampled at the wrong place: at rate 2, a run resumed 400 ms into its curve landed 400 ms further along it than the hold found it, in Chromium and WebKit.Intended invariant
Held runs keep their timing relative to one another: a seek to t moves each held run to its own t −
offsetMsthrough its playback rate (HeldAnimation.offsetMs,seekAnimationsininternal/interaction-harness/src/curves.ts), so a seek to the moment of the hold leaves every held run where the hold read it.Change
now − currentTime / playbackRate(a rate of 0 counts as 1, as seeking reads it), notnow − currentTime. A run at rate 2 covers its curve in half the timeline, so it started half as long ago.waitForAnimation's dates and progress,seekAnimationsandsampleCurve.Prior art: open pull requests searched for
holdAnimations,seekAnimationsandcurveEndMs. #8535, merged, dated finished runs; #8446 changes the recorder while a trace runs. Neither changes how a hold reads a run's start.Evidence
Scope
Testing
pnpm exec playwright test --config playwright.interaction.config.ts --project chromium internal/interaction-harness/src/harness.interaction.browser.spec.ts --grep "resume is still pending"node internal/interaction-harness/scripts/webkit-container.mjs internal/interaction-harness/src/harness.interaction.browser.spec.ts --grep "resume is still pending"Local gates at da89ae6: Prettier, ESLint and the harness typecheck pass on the changed files; the root build passes; the core UI suite passes; the node project passes except two process-timing cases in
packages/cli/foundation/release/cache.test.mjs, which pass when that file runs alone (34 of 34); the harness browser spec passes in Chromium and WebKit; the new case passes 24 of 24 in Chromium pinned to two cores, 40 of 40 in WebKit capped at 3 CPUs at 16 workers, and 10 of 10 uncapped in each engine.