Skip to content

Reduce linear-navigation latency around list scrolling - #42

Open
devinprater wants to merge 5 commits into
google:masterfrom
devinprater:scroll-latency-fixes
Open

devinprater wants to merge 5 commits into
google:masterfrom
devinprater:scroll-latency-fixes

Conversation

@devinprater

Copy link
Copy Markdown

Problem

Swiping through scrollable lists (e.g. a social-media feed, but also Settings) can take ~250-560ms per item before speech starts. Measured on a Galaxy S25 (Android 16, TalkBack 17.0.1) using TalkBack's own debug logging with millisecond timestamps. Three distinct stalls, all in the swipe -> focus -> speak path:

  1. Dead auto-scrolls burn the full timeout. When an app never performs the requested scroll action and never emits scroll progress (observed: a Compose-based feed), every swipe waits out the full SCROLL_TIMEOUT_SHORT (500ms): gesture at 16:40:44.366, onAutoScrollFailed at 16:40:44.870, speech at 16:40:44.924. The timeout exists for slow lists, but a scroll with zero sign of life is never going to complete.
  2. Working scrolls serialize speech behind settle + re-search. A healthy Settings swipe costs ~242ms: ~58ms app response, ~113ms settle wait (TIMEOUT_MS_HANDLE_SCROLL_BY_GESTURE), ~48ms post-scroll focus re-search, ~23ms speech queue. Speech waits for all of it although the target is already known.
  3. Fast swiping drops items silently. A second swipe while a scroll is settling resets the scroll records before the delayed success handler fires, so the first item never speaks and the user perceives a stall (~450ms of silence in the trace).

Changes (4 commits)

  • Fail-fast on dead auto-scrolls (AutoScrollActor): arm a 150ms watchdog alongside the full scroll timeout. Fires only under complete scroll silence (see next commit); routes through the normal timeout path, so assume-success retry, focus and speech proceed immediately.
  • Fail-fast only under complete scroll silence (AutoScrollActor, AutoScrollInterpreter, TalkBackService wiring): the interpreter reports every scroll event to the actor. Any scroll activity since the action - even unmatched (e.g. the user's own finger on a settling list) - stands the watchdog down to stock timeout behavior. A fixed 150ms watchdog false-positived on rapid swipes during testing (focus escaped the list); the silence gate fixed it.
  • Speak-ahead (FocusProcessorForLogicalNavigation, behind SPEAK_AHEAD_OF_SCROLL): when a swipe triggers a scroll for an already-computed target, focus and speak it immediately instead of waiting out settle + re-search. Scroll bookkeeping is untouched (no record reset, no forced refocus); the post-scroll pass skips re-focus when focus is already on the target (no double speech) and corrects otherwise.
  • Flush pending scroll-success on new navigation (AutoScrollInterpreter, DirectionNavigationActor): each new directional navigation first completes any pending delayed scroll-success, so every swipe is heard (briefly, then interrupted as usual) instead of silently dropped.

Testing

  • Built with the repo's documented Gradle setup (assembleDebug, AGP 8.11 / Gradle 8.13 / JDK 17) and side-loaded alongside stock TalkBack.
  • Before: ~560ms/swipe in the problem feed, ~280ms/swipe in Settings with dropped items at fast cadence.
  • After: 17-50ms typical swipe-to-speech in Settings, ~130ms worst case, zero dropped items at fast cadence; the dead-scroll feed improved comparably.
  • Reverted one intermediate approach (retry-on-stale-focus) because its second search could hit wrap/edge paths on an unsettled tree and announce a false list boundary; the final set above has no wrap/edge interaction.

Happy to split these into separate PRs per fix if preferred, and to sign the Google CLA - just point me at it.

@google-cla

google-cla Bot commented Sep 20, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

…ogress

Linear navigation waits out the full SCROLL_TIMEOUT (500/1000ms)
whenever an app never performs the requested scroll action and never
reports progress (e.g. Reddit's feed). Arm a watchdog alongside the
full timeout; the first scroll-progress event disarms it via
cancelTimeout(). On fire, route through the normal timeout path so
assume-success retry, focus and speech proceed immediately.
The fixed 150ms watchdog false-positived on rapid swipes: overlapping
scrolls mean the newest record can see no matching events while the
list is still moving from a previous swipe, and focus then lands on a
moving tree (observed: focus escaping to search/edge panels).

The interpreter now reports every scroll event to the actor; the
watchdog stands down to stock timeout behavior whenever any scroll
activity postdates the action. Dead scrolls (total silence, e.g.
Reddit's feed) still fail fast.
When a swipe triggers a scroll, focus the already-computed target
immediately (~90ms) instead of waiting out settle + re-search (~160ms).
Scroll bookkeeping untouched; post-scroll pass skips re-focus when
focus is already on the target (no double speech), corrects otherwise.
Fast swiping discarded the in-flight item silently: the next swipe
reset the scroll records before the delayed success handler fired, so
the swiped-to item never spoke and the user perceived a stall. Each
new directional navigation now completes any pending scroll-success
first; it speaks immediately and is interrupted by the new item as
usual.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant