Skip to content

Android: snapshots take 2–22 s, and wait times out, while a Text ticks once a second inside a React Native sheet's dialog window #3558

Closed

Description

@nickcernera

Summary

On one screen of our React Native app, Android snapshots slow from about 0.3 s to between 2 and 22 s, and a wait
for an element that is plainly on screen times out. The screen is a bottom sheet hosted in its own dialog window
(@lodev09/react-native-true-sheet) that shows a clock ticking once a second. Pause the clock, leaving everything else
on screen unchanged, and snapshots are fast again on the same session.

A stock Android screen that ticks faster (the Clock app's stopwatch, with or without its popup menu open) does not
reproduce it. So this is not simply "the UI never goes idle"; something about this window or tree makes captures slow
while it updates. Details, measurements and the narrowing we did are below. We can build a minimal repro app if that
would help.

Environment

  • agent-device 0.21.18 (installed) and 0.21.27 (latest; same result)
  • Android emulator: sdk_gphone64_arm64, Android 16 (API 36), google/sdk_gphone64_arm64/emu64a:16/BE2A.250530.026.F3,
    1080×2400
  • Host: Apple M5 Max, macOS. Host load average was 20–45 during most runs, because other emulators and builds were
    running.
  • App: React Native 0.86.3 (New Architecture / Fabric), Expo SDK 57, react-native-reanimated 4.5.4,
    @lodev09/react-native-true-sheet 4.0.0-beta.24, release build (no Metro, no dev tools)

The screen

A full-height bottom sheet hosted by TrueSheet, so on Android its content sits in a separate dialog window above the
activity. In its header, a pill shows elapsed time (0:04, 0:05, …). A Text updated every second through React
state, inside an accessible View whose accessibilityLabel changes only once a minute.

Measurements

1. The ticking clock is the trigger. Same session, same screen, agent-device snapshot --json five times while
the clock ticks, then five times after pausing the clock (one tap; nothing else on screen changes):

Clock Snapshot wall time (5 runs) Result
Ticking 11.1 s, 2.4 s, 4.5 s, 2.2 s, 21.6 s all ok, 53 app identifiers
Paused 0.7 s, 0.3 s, 0.3 s, 0.2 s, 0.3 s all ok, 55 app identifiers

Inside a replay, the slow captures turn into failures. wait "id=\"live-walk-sheet\"" 15000 (the sheet's own root,
which is on screen) times out:

wait timed out for selector: id="live-walk-sheet"
hint: No readable snapshot capture completed before the wait timeout. Retry, or use screenshot to inspect the current surface.

with this sequence in the request log, every time:

android_snapshot_helper_session_fallback   reason: "Android snapshot helper session request timed out"   (≈5 s each, repeated)
android_snapshot_helper_session_capture    error: "Wait deadline exceeded"
android_snapshot_helper_content_invalid    reason: "content-poor-app-window"
    helperForegroundAppPackage: <our package>
    helperWindowRootCount: 1, helperApplicationWindowRootCount: 0
    helperNodeCount: 28, helperSystemUiNodeCount: 28
    helperApplicationMeaningfulNodeCount: 0, helperWindowTypes: [3]
    helperCaptureMode: "interactive-windows"
snapshot_capture                           error: "Android snapshot helper returned insufficient foreground app content"

So when the session request times out, the fallback capture sees only system UI and no application window: neither
the activity nor the sheet's dialog.

2. What the device emits. With the clock ticking, adb shell uiautomator events shows a
TYPE_WINDOW_CONTENT_CHANGED from a TextView (CONTENT_CHANGE_TYPE_TEXT) about every 1000 ms, then about 100 ms
later a CONTENT_CHANGE_TYPE_SUBTREE from a parent FrameLayout. That's 12 events in 6 s. adb shell uiautomator dump
fails on this screen every time (ERROR: could not get idle state., ~11 s, 4 of 4). On the app's other screens it
completes in ~2 s.

3. What does NOT reproduce it (agent-device 0.21.18 and 0.21.27, same emulator, same host load):

Screen Accessibility events uiautomator dump agent-device snapshot wait for a visible label
Clock app stopwatch running (main window) 54 in 6 s (subtree + text) fails, "could not get idle" 5/5 ok, 0.2–0.6 s ok, 0.2 s
Clock stopwatch running + overflow popup window open — — 5/5 ok, 0.2–0.3 s, popup items read ok, 0.2 s
Our app, a static TrueSheet sheet (walk setup) — — ok; sheet content read in full ok
Our app, live sheet with the clock paused — — 5/5 ok, 0.2–0.7 s —

So a never-idle screen alone is handled well, and so are a separate popup window and a static sheet dialog. The
combination that fails is per-second text updates inside the React Native sheet's dialog window.

4. Things we tried that did not help:

  • Setting window_animation_scale, transition_animation_scale and animator_duration_scale to 0.
  • Marking the ticking Text importantForAccessibility="no-hide-descendants". The text-change events keep coming;
    Android still emits them while accessibility is enabled.
  • agent-device 0.21.27, which includes fix(android): back off a timed-out snapshot helper session and bound content re-captures #3160 ("back off a timed-out snapshot helper session and bound content
    re-captures"). Flow results: 1 flake and 2 reds in 3 runs.
  • Splitting a replay into several shorter replays in the same session made it worse: 4/4 red at this screen. One
    replay inlined: 3/3 green on a quieter host. That suggests a freshly started helper session is the most exposed.

Impact

Any flow that waits on, presses, or reads an element of a screen with a live clock or counter inside a sheet is red or
flaky, depending on host load. In our release-build smoke suite it is the only flaky flow, and it keeps the suite from
reaching the 10 consecutive green runs we need before it can gate releases. Timers, live activity screens and workout
sheets are common, so we expect others to hit this.

Questions and possible directions

  1. Is the helper's session capture waiting for an idle or quiet window, and could it skip or bound that wait (like
    Appium UiAutomator2's waitForIdleTimeout, commonly set to 0 for live content)? A per-session or env setting
    would be enough for us.
  2. When the session request times out, the fallback (interactive-windows) returned no application window at all,
    only system UI, so the screen read as empty. Should the fallback include the foreground app's own windows,
    including a dialog-hosted sheet, before declaring content-poor?
  3. Is there anything about a React Native Fabric dialog window (TrueSheet) that the helper treats differently from a
    native PopupWindow? The stock popup case is fast.

Repro

We can share:

  • the exact .ad replay and its full request logs (requests/*.ndjson) for a failing wait;
  • a minimal React Native app on request: one TrueSheet sheet with a Text updated by setInterval every 1000 ms,
    then agent-device wait "id=\"<sheet root testID>\"" 15000 in a loop.

Stock control, which passes, as a baseline:

adb shell am start -n com.google.android.deskclock/com.android.deskclock.DeskClock
# Stopwatch tab → Start, then:
agent-device open com.google.android.deskclock --platform android --session clk
agent-device snapshot --json --session clk          # fast while the stopwatch runs
agent-device press 'id="com.google.android.deskclock:id/overflow_action_button"' --session clk
agent-device wait 'label="Settings"' 15000 --session clk   # ok, ~0.2 s

Activity

  1. nickcernera commented on Oct 13, 2026

    @nickcernera
    Author

    Update: narrowed it down. The trigger is a looping animation, not the ticking clock.

    We tested each candidate with probe builds of the same app, on the same emulator, at similar host load (load average
    31–46), timing 8 agent-device snapshot --json calls with the sheet open:

    Build Snapshot times Sheet root in snapshot
    Unchanged 5.2–15.0 s 5 / 8
    Walking-wide per-second re-render slowed to once a minute (clock still ticking) 5.4–22.4 s 5 / 8
    Breathing dot held still (clock still ticking) 0.4–1.1 s 8 / 8
    Unchanged build, device animation scales set to 0 before launch 0.2–0.4 s 8 / 8

    The dot is an 8×8 Animated.View in the sheet's header. Its opacity loops forever with Reanimated 4.5.4:
    withRepeat(withSequence(withTiming(0.4, 800 ms), withTiming(1, 800 ms)), -1). The sheet is a
    @lodev09/react-native-true-sheet sheet, so on Android the dot sits in a separate dialog window. With the dot still, the
    once-a-second text update is harmless. That matches the stock Clock stopwatch reading fine.

    So the minimal repro is probably: an infinite Reanimated opacity loop inside a dialog window. We haven't confirmed
    whether the dialog window matters or whether a looping animation in the main activity window does the same.

    For our suite we now set window_animation_scale, transition_animation_scale and animator_duration_scale to 0 on
    the test emulator, as Espresso suites do. Reanimated treats that as Reduce Motion and holds the loop still, so we're
    unblocked. Snapshots that slow down this sharply behind one small looping animation still seem worth a look, since many
    apps have a pulsing "live" indicator, and the fallback capture returning no application window at all stands either
    way. Happy to test a build.

  2. thymikee commented on Oct 14, 2026

    @thymikee
    Member

    Thanks! want to send a PR or I should cover it?

  3. nickcernera commented on Oct 14, 2026

    @nickcernera
    Author

    Happy for you to cover it — you know the helper's idle/fallback paths far better than I would. I'm unblocked on my side (animation scales at 0 on the test emulator), but I'll test any build against the original screen, and can put together a minimal repro (a TrueSheet sheet with a withRepeat opacity loop) if that helps.

  4. thymikee commented on Oct 16, 2026

    @thymikee
    Member

    Fixed by #3611, released in 0.21.28. Could you confirm on 0.21.28 against the original screen with animations on?

  5. nickcernera commented on Oct 17, 2026

    @nickcernera
    Author

    Thanks for the quick turnaround! Tested 0.21.28 against the original screen with animations on.

    The capture failure is fixed. The sheet's root is in every snapshot now (12/12, vs 5/8 on 0.21.18), and wait "id=\"live-walk-sheet\"" succeeds.

    Snapshots are still slow while the dot loops, though. Same session, same sheet, same emulator. The host was heavily loaded (load average 170–240, other emulators and builds running), but the animations-off round ran at that same load, so the gap is the animation, not the machine:

    Device animations Snapshot wall time Sheet root in snapshot
    On (2 rounds) 4.3–15.1 s 12/12
    Off (Reanimated holds the dot still) 0.3–0.5 s 6/6

    The daemon's own durationMs on the slow ones matches the wall time (4.1–5.3 s on the ones I checked), so the time is in the capture, not the CLI. That's at least 10× slower with one 8×8 opacity loop on screen, which adds up quickly inside a replay.

    Would you like me to open a separate issue for the speed so this one can stay closed, or would you rather track it here? Nothing's blocking us: we keep animation scales at 0 on the test emulator for now.

  6. thymikee commented on Oct 17, 2026

    @thymikee
    Member

    Decision: residual snapshot latency: let's track the remaining speed issue here; no need to open another issue. Thanks for checking with animations back on. The previous fix addressed the missing-window failure, but left a traversal bottleneck.

    I reproduced that bottleneck with an animated dialog and the helper's idle wait explicitly disabled. Android serves accessibility cache misses on the app's UI thread. Our traversal waited for each child before requesting the next, so it could pay another animation-frame delay repeatedly. Prefetch helps, but Android caps each prefetch at 50 nodes.

    #3790 queues up to eight sibling reads together while preserving output order, capture limits, and fresh captures. On a controlled API 35 fixture with 400 text nodes in a dialog, a pulsing dot, and UI-thread frame work, alternating base/head installs reduced mean capture time from 2.79 s to 0.74 s. The static control was 0.11 s vs 0.08 s, and the captured app trees matched.

    This confirms a remaining helper-side bottleneck, but doesn't yet prove that it accounts for all the latency on your original screen under that host load. A test of the PR build with animations on would be useful, ideally with the request log so we can compare the helper's elapsedMs with total request time. No animation or device settings need to change for this fix.

  7. thymikee commented on Oct 18, 2026

    @thymikee
    Member

    Thanks for the numbers. Yes, please open a separate issue for the speed and include that table: 4.3–15.1 s with the opacity loop on, against 0.3–0.5 s with it held still, under the same load. This one stays closed for the capture fix.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions