Skip to content

Screenshot capture fires mid-scroll and janks 120Hz ARR panels #750

Description

@LuisOsta

Version

3.61.0 (via @posthog/react-native-plugin 2.5.1 / posthog-react-native 4.66.2)

Steps to Reproduce

  1. React Native 0.84 app (new architecture, release build) with enableSessionReplay: true, default throttleDelayMs (1000), on a physical Android 15+ device with an ARR panel (tested: Pixel 10 Pro XL, 120Hz LTPO, Display.hasArrSupport() == true). Emulators cannot reproduce this — the virtual HWC exposes one fixed 60Hz mode, so the frame-rate-override path never engages.
  2. Scroll a content screen continuously (drags + flings) for ~80s. For a deterministic repro we record a real finger session once with evemu-record and replay it byte-identically with adb shell uinput (the kernel-path mechanism AOSP CTS uses; adb shell input swipe under-triggers the ARR heuristics ~10× and won't reproduce).
  3. Capture with Perfetto (android.surfaceflinger.frametimeline data source) and count jank types for the app's layer:
select jank_type, count(*) from actual_frame_timeline_slice
where layer_name like '%<your.app>%' group by 1 order by 2 desc;

Expected Result

Scrolling stays smooth while replay records. At minimum, replay does not spend capture cost on frames it will discard: during a scroll, maskLegacyScreenshot's redraw guard rejects any capture that overlapped a non-animation redraw (unsafeRedraw), which a scroll redraw always is — so mid-gesture captures are pure cost with zero shipped frames.

Actual Result

Capture is scheduled off onDraw through the Throttler, which re-arms on hasPendingDraw — so during continuous scrolling, captures chain at exactly 1/throttleDelayMs for the entire gesture, by construction. Each capture is a full-window PixelCopy readback plus a ~16MB ARGB Bitmap + WebP encode. The burst stalls the app's frame delivery long enough that ViewRootImpl's intermittent-update detector (Android 15+) drops the frame-rate vote 120→60; when the cadence recovers it flips back. Every flip is a render-rate transition SurfaceFlinger must reschedule around, and the mis-scheduled frame stuffs the buffer queue — 200–950ms of late presentation per episode, felt as intermittent scroll stutter.

Measured (identical replayed input per arm, ~6,200 frames and ~106 render-rate switches per 80s run):

Arm Buffer Stuffing frames SurfaceFlinger Scheduling janks
Replay recording, defaults 651 33
Same app, replay not recording 0–45 (3 runs) 0–2
Replay recording + app-side "pause capture while a pointer is down" (5 runs) median 75 median 1

The third row is the proposed fix implemented app-side (stopSessionRecording() on pointer-down, startSessionRecording(true) on all-pointers-up + 1s fling debounce): jank returns to the no-replay baseline, and capture-thread activity confirms screenshots still fire between gestures — recordings stay intact because between-gesture frames are the only ones the redraw guard was keeping anyway.

Proposed fix (all hooks exist in PostHogReplayIntegration):

  1. Defer capture while a pointer is down (+ a short settle window for flings): onTouchEventListener already sees every DOWN/MOVE/UP; gate the throttled snapshot lambda on a volatile pointer-down flag and fire the deferred capture when quiet. Same instinct as 3.17.0's "do not capture screenshot during screen changes", wider.
  2. Micro-fix in the same listener: filter to DOWN/UP before MotionEvent.obtain + executor.submit — currently every MOVE of a 120Hz scroll is copied and submitted just to be discarded on the executor.
  3. The in-source TODO: PixelCopy.Request.Builder.ofWindow + Executor on API 34+.
  4. Reuse a persistent Bitmap across captures to remove the ~16MB/s allocation churn while scrolling.

Precedent: iOS has screenshotModeBackgroundCapture for exactly this symptom, and the RN docs recommend it verbatim for "janky or drops frames on high-refresh-rate (120Hz ProMotion) iPhones" (PostHog/posthog-js#3552) — Android has no equivalent. Related accepted capture-cost fixes: PostHog/posthog-ios#321, PostHog/posthog-ios#571, and this repo's 3.53.7/3.54.1/3.55.1 changelog entries. PostHog/posthog-js#3796 asks for RN wireframe mode partly for the same perf reason.

Happy to share full Perfetto traces, the replay harness, and our app-side pauser, or to re-run the A/B against a patched build.

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