Repository navigation
Conversation
…ter a stalled dispatch, and a proof can check the pacing the page received WebKit's touch command takes no timestamp, so the page times each event by its arrival. Under box contention a dispatch stalls 27 to 31 ms, and the driver, which aimed every event at the gesture's start, then sent the overdue moves together, 1 to 2 ms apart: C17 measured BottomSheet's flick at 2000 px/s reaching the page as 3,627 to 13,200 px/s. playSteps now sends each event at its scripted time and never sooner after the one before it than the script spaces them, so a stall stretches one gap instead of bunching the rest. With `checkPacing`, WebKit's play reads the timeStamps the page received (a passive capture listener on touch events) and throws TouchPacingError, naming both gap lists, when an event is missing or a gap is more than TOUCH_PACING_TOLERANCE_MS off the script. retakeOnTouchPacing(testInfo, attempt) runs a proof's attempt once more on a TouchPacingError only: two attempts in all, a product failure is never re-taken, and each re-take is listed as a `touch-retake` annotation. The tolerance is measured: on one worker of a loaded machine, 440 gaps over 40 WebKit flicks moved at most 4 ms (99th percentile 3 ms; the page's stamps are whole ms); under eight workers 4% of flicks had a gap further off, up to 21 ms. Red first: the stalled-dispatch case fails on the previous scheduling; checkTouchPacing and retakeOnTouchPacing are new, with the three re-take cases (both attempts off pace stays red, a product failure after good input is not re-taken, off pace then good passes and is listed). In the browser a 40 ms driver stall makes WebKit's play throw TouchPacingError, and Chromium, whose page sees the scripted stamps, plays on.
…a click, and SCRIPTED_CLICK_SKIP names the skip where it does not Playwright's WebKit synthesises a click only from its tap gesture (`driver.tap`), never from touches sent one by one, so a proof that needs the click of a scripted press and lift has to know. `driver.synthesizesClicks` says it (Chromium true, WebKit false), and such a proof skips with `test.skip(!driver.synthesizesClicks, SCRIPTED_CLICK_SKIP)`, as PARTIAL_LIFT_SKIP does for a partial lift. The self-test plays a scripted tap and then driver.tap on one button and reads the order of touchstart, touchend and click: driver.tap's click comes last and a scripted tap's click, if any, before driver.tap's touchstart, so it waits on that order rather than on the clock. This came from the web-input drivers helper line, which no longer carries it.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
…se reads it, its last two move intervals and its lift C17 adopted checkPacing on BottomSheet's flick detent proofs and every WebKit attempt failed, even on an idle worker: WebKit delivers a gesture's first moves with the page's own touchstart handling time (gaps of 4, 11, 3 ms against 8, 8, 8) and its lift about a frame late (8 to 11 ms against 2.1). Neither feeds a release velocity: Core's tracker reads the last three moved samples, two intervals, and a release reads as still only 40 ms after the last move. Before the check the same flicks reached the page at 2,000 px/s. checkPacing now judges what decides a release: an event missing, either of the last two move intervals more than TOUCH_PACING_TOLERANCE_MS (4 ms) off the script, or the lift TOUCH_LIFT_STILLNESS_MS (40 ms, UIKit's measured stillness, spec:AST-100 FR2) or more after the last move. checkTouchPacing takes the played steps, so it knows which events are moves. Red first: C17's received gaps fail the previous check and pass this one; a stall and its burst in the last two intervals still fails, and so does a lift 45 ms late, while the same stall earlier in the gesture passes. In the browser a 40 ms driver stall before the last move throws in WebKit, and Chromium plays on.
There was a problem hiding this comment.
Changes requested: keeping the scripted gaps after a stall is sound, but the new pacing check misfires on gestures that rest before lifting and one new assertion is timing-sensitive.
checkTouchPacingcompares the lift with a fixed 40 ms, so a script that rests before lifting (for exampledrag(..., {restMs: 120})) throwsTouchPacingErroron both attempts even when WebKit delivers it on script; please judge the lift against the script's own lift gap (touch.ts, lines 220-225).expect(sentGaps[2]).toBeGreaterThanOrEqual(40)fails when Node's 40 ms timer resolves slightly early, and the same 1 ms slack the later gaps get would fix it (touch.pacing.test.ts, line 59).- The
TOUCH_LIFT_STILLNESS_MScomment calls 40 ms UIKit's stillness, while AST-100 FR2 and Core's constant say the stillness reset is where Astryx departs from iOS, and the README's engine-limits table still promises about 1 ms pacing.
[Automated review]
| } | ||
| } | ||
| const liftAfter = received[lift] - received[lastMove]; | ||
| if (liftAfter >= TOUCH_LIFT_STILLNESS_MS) { |
There was a problem hiding this comment.
This compares the lift with a fixed 40 ms rather than with the script, so a gesture that rests 40 ms or more before lifting throws even when the page receives it exactly on script. In WebKit, the self-test's own drag(..., {restMs: 120}) played with checkPacing arrives with every gap on pace (16 to 17 ms against 16.7, the lift at 120 ms) and still throws on both attempts of retakeOnTouchPacing. Failing only when the scripted and received lift gaps fall on different sides of the interval keeps the check's purpose:
const scriptedLift = steps[lift].t - steps[lastMove].t;
if (
(scriptedLift > TOUCH_LIFT_STILLNESS_MS) !==
(liftAfter > TOUCH_LIFT_STILLNESS_MS)
) {Using > also matches Core's tracker, which still releases a velocity at exactly 40 ms.
| scriptedGaps[i] - 1, | ||
| ); | ||
| } | ||
| expect(sentGaps[2]).toBeGreaterThanOrEqual(40); |
There was a problem hiding this comment.
Node can resolve setTimeout(…, 40) up to about 1 ms early by performance.now(): repeating this case 120 times gave a stalled gap under 40 ms three times (minimum 39.31 ms), which would turn the required test check red with no cause in the code. The same 1 ms slack the later gaps get keeps the case red on the old scheduling, whose later gaps measure about 0 ms:
expect(sentGaps[2]).toBeGreaterThanOrEqual(39);|
|
||
| /** | ||
| * How long after its last move a lift may reach the page before the release | ||
| * reads as still, in ms: UIKit's measured stillness (`spec:AST-100` FR2). |
There was a problem hiding this comment.
AST-100 FR2 says the stillness reset is "the one case where it differs from iOS", and Core's constant in packages/core/src/motion/uikit/gestures.ts reads "Not a UIKit value" (a guess taken from Android and Flutter). Citing it as Core's internal stillness interval, with a SYNC note naming that file, keeps the two from drifting.
…ample elements in one call, and read a watch's whole record
A hold the test takes after an action reaches the page as late as the runner
lets it: on a loaded machine a 1 s settle can end before the test's next call
arrives, and a proof that reads it then fails on the runner, not the product.
- `armHold(scope, {property, to})`, taken before the action: when an animation
of the scope's own element whose last keyframe sets `property` to `to` is
recorded, everything running in the scope is held, into the same held set
as `holdAnimations`, in a microtask of the task that created it. Until the
page's next frame, an animation the same task starts in the scope later is
held too. A script animation is held before its first frame (a call from
rest at 0 ms, one on an explicit start time where it stands); a CSS
animation or transition at its first frame. `held()` returns the held
animations and the one that fired, or rejects after three of the page's
frames naming what the element started instead, and stands the hold down.
- `holdAnimations` and `armHold` share one page-side hold, and
`waitForAnimation` and a watch one page-side description.
- `sampleCurves(locators, times)`: several elements in one page call, so
nothing the page runs between two calls lands between one element's
samples and another's.
- `watch.recorded()`: every animation recorded on the element since the
watch, running or finished, with no wait. `started()` answers with the
running ones whenever one has progressed, so a short fade that ended beside
a part still running is only in the record.
The Item flick proof, the first user, also needs the touch driver's script
stamps (meta#8910, 8ca17da) for its result under load: with the engine's own
stamps a loaded runner puts the throw off its scripted pacing, and the proof
fails by name with a TouchPacingError.
Review only this PR's change: vjeux/astryx@90da4a7...cec68ad
Why
WebKit's touch command takes no timestamp, so the page times each touch by its arrival. Under load a dispatch stalls 27 to 31 ms and the moves behind it land 1 to 2 ms apart: BottomSheet's 2,000 px/s flick reached the page at 3,627 to 13,200 px/s.
What led to it
The driver aimed every event at the gesture's start, so after a stall it sent the overdue moves together, and nothing told a spec its input was off script.
Options and tradeoffs
Why this choice
playStepssends each event at its scripted time and never sooner after the one before it than the script spaces them: a stall stretches one gap instead of bunching the rest.play(script, {checkPacing: true})throwsTouchPacingError, with both gap lists, where the page'stimeStamps decide a release: an event missing, either of the last two move intervals (what a release velocity reads) more thanTOUCH_PACING_TOLERANCE_MS(4 ms) off, or the lift 40 ms or more after the last move (UIKit's stillness). The first moves and the lift carry the page's own handling time and are not judged. Opt-in.retakeOnTouchPacing(testInfo, attempt)runs a proof's attempt once more, only on aTouchPacingError, as atouch-retakeannotation; a second fails with that cause; a product failure is never re-taken.driver.synthesizesClicksandSCRIPTED_CLICK_SKIP: a proof that needs a scripted tap's click skips by name where WebKit synthesises none.How to check it
pnpm exec vitest run --project node internal/interaction-harness/src/touch.pacing.test.ts: 9 pass.pnpm test:interaction internal/interaction-harness/src/harness.interaction.browser.spec.ts --grep "scripted tap clicks|off its pacing": both pass in both engines.Evidence
pnpm check:repo, node project pass.