Skip to content

Android: snapshots ~10× slower while a looping Reanimated opacity animation is on screen (follow-up to #3558) #3832

Open

Description

@nickcernera

Follow-up to #3558, split out as asked. #3611 (0.21.28) fixed the missing-window capture failure. This issue tracks the remaining latency: Android snapshots are about 10× slower while one small looping animation is on screen.

Setup

  • Screen: a full-height @lodev09/react-native-true-sheet 4.0.0-beta.24 sheet (on Android, a separate dialog window). Its header holds an 8×8 Animated.View whose opacity loops forever with Reanimated 4.5.4: withRepeat(withSequence(withTiming(0.4, 800 ms), withTiming(1, 800 ms)), -1).
  • React Native 0.86.3 (New Architecture), Expo SDK 57, release build.
  • Emulator sdk_gphone64_arm64, Android 16 (API 36), 1080×2400. Host: Apple M5 Max, macOS, shared with other emulators and builds.
  • "Animations off" means window_animation_scale, transition_animation_scale and animator_duration_scale set to 0 before launch. Reanimated reads that as Reduce Motion and holds the dot still. Nothing else on screen changes.

0.21.28, 2026-10-17 (the table requested in #3558)

Same session, same sheet. Host load average was 170–240 for both rounds.

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

On the slow captures, the daemon's durationMs matched the wall time (4.1–5.3 s on the ones checked).

0.21.28 vs 0.21.29-nightly.20261019.75 (includes #3790), 2026-10-19

Same app build, same emulator, one session. Each block is 7 snapshot --json calls; the first call is dropped because it reinstalls the shared helper APK. Blocks run in ABAB order. The numbers are the helper's data.androidSnapshot.elapsedMs. Every capture succeeded and contained the sheet root.

Device animations Host load 0.21.28 median (range) nightly median (range)
On 29–41 676 ms (535–1246) 536 ms (301–681)
On 16–27 443 ms (263–1140) 439 ms (302–6664, 1 outlier)
Off 17–24 164 ms (151–232) 40 ms (39–46)

What this shows on our screen:

  • perf(android): avoid animation stalls in capture and viewport reads #3790 makes the static capture about 4× faster (164 → 40 ms).
  • With the loop running, it changes little at moderate load: 676 → 536 ms at load 29–41, and 443 → 439 ms at load 16–27.
  • The loop's cost on the nightly is about 11× (40 ms → 440–540 ms), with an occasional multi-second capture (6.7 s once, as load rose).
  • Host load scales the slowdown. The same 0.21.28 measured about 0.5–1.2 s here and 4–15 s at load 170–240.

So the remaining cost seems to come from the animation itself rather than traversal order. That fits your note that Android serves cache misses on the UI thread, which a running animation keeps busy.

Impact and workaround

Anything that snapshots repeatedly (wait, press after a settle, replays) pays this on every capture while a pulsing "live" indicator is on screen. That's common in timers, workouts and live-status UIs. Our workaround is animation scales at 0 on the test emulator, as Espresso suites do. That isn't available when the animation is the thing under test.

Happy to run a build against this screen. I can attach the request logs (requests/*.ndjson) for any of the runs above, or put together a minimal repro app (one TrueSheet sheet with the withRepeat opacity loop).

Activity

  1. thymikee commented on Oct 20, 2026

    @thymikee
    Member

    Triage: bug (performance). Thanks, the ABAB numbers isolate it. The nightly median with the loop running (440–540 ms) is the static capture (40 ms) plus the snapshot helper's idle wait: waitForIdle waits up to 500 ms (AccessibilityTreeCapture.java:29), and a looping animation never goes idle, so every capture pays the full wait. The multi-second outliers under load look like a separate cost, from node-cache misses served on the busy UI thread. How the idle wait should behave when the UI never settles is a maintainer decision; it stays parked until then.

  2. thymikee commented on Oct 22, 2026

    @thymikee
    Member

    #3853 shipped in 0.21.30. When the stored tree is at least 500 ms old and nothing needs a fresh capture, the helper's idle wait is now capped at 100 ms, so a looping animation no longer costs the full 500 ms on every capture. Please rerun your ABAB blocks on 0.21.30 with the loop on and off, and report the data.androidSnapshot.elapsedMs medians. The multi-second outliers under heavy host load are a separate cost; please note whether they still appear.

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

    bugSomething isn't workingneeds-infoWaiting on reporter or external input

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions