Skip to content

perf(scroll): reuse the settled capture as the next directional baseline #2826

Description

@thymikee

Purpose

A directional scroll that follows an unchanged scrollable should not pay for a second surface read to learn what its previous scroll already measured. Each directional scroll currently captures its own pre-gesture baseline, so the cost of consecutive scrolls on the same surface grows by one capture each time.

Measured cost today

The mechanism is capture speed, not staleness. On Android the helper's interactive capture returns in roughly 190 ms, which is faster than the fling lands, so the first post-gesture read still matches the pre-gesture tree and the observation loop must take the change on a second read:

  • Android, tester /catalog: scroll_movement_observed {"direction":"down","movement":"moved","attempts":2,"durationMs":441,"requestedPixels":1800}
  • iOS, tester /automation: scroll_movement_observed {"direction":"down","movement":"moved","attempts":1,"durationMs":370,"requestedPixels":502}

Same code, different platform timing: the iOS runner capture itself costs ~350 ms, so the single read completes after the content has already settled. The Android path therefore performs two captures per scroll where iOS performs one.

Proposed shape

Carry each scroll's post-gesture capture forward as the next scroll's baseline instead of re-capturing it, keyed on the session side-effect revision already read for ref frames (readSessionRuntimeRevision, src/daemon/ref-frame.ts). A revision match means nothing in the session could have altered the tree between the two scrolls, so the cached capture is the same tree a fresh baseline would return.

Required behavior

  • An eligible post-gesture capture may serve as the next directional scroll's baseline only while the session side-effect revision still matches the revision recorded with it.
  • Any operation able to change the observed tree invalidates the cached capture, and the next scroll recaptures rather than comparing against a stale surface.
  • A cache miss must cost exactly what a scroll costs today; the cache is never allowed to change a movement verdict, only to supply the comparison input.
  • The attempts and durationMs in scroll_movement_observed keep reporting what actually happened, so a reused baseline is visible in the diagnostic rather than flattering it.

Observable completion conditions

  • Coverage proving reuse when the revision is unchanged, and a recapture after an invalidating side effect.
  • Coverage proving that a reused baseline and a freshly captured baseline produce the same movement verdict for the same pair of trees.
  • A device measurement on the chained Android case showing the extra read is gone, with the iOS case unregressed.

Open boundaries before this is AFK-ready

  • Which captures are eligible: any post-gesture capture, or only one that answered moved (a unchanged or at-edge verdict may mean the surface is already the wrong comparison input).
  • Whether a capture taken during a scroll-until loop, which reads repeatedly without side effects, may seed the baseline for the next command in the same batch.
  • What the invalidation set is, exactly: which commands and which runtime facts count as tree-changing.

Deferred deliberately during #2776 review: the disclosure work shipped without it, and a first-read delay was rejected because it would slow every successful scroll on the fast platform to make a metric on the slow one match a prediction.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions