Skip to content

Android: orphaned 60fps render loop after recents-swipe with singleTask + foreground service — CPU accumulates per warm restart until force-stop #3971

Description

@jejeje-dot

Description

With android:launchMode="singleTask", swiping the app away from recents destroys the Activity but keeps the process alive (a foreground service holds it). On the next warm start, the previous Canvas's render loop is left attached to the now-detached SurfaceTexture: logcat bursts at 60fps with

RNSkia: updateAndRelease() failed
EGLConsumer: [SurfaceTexture-...] updateAndRelease: EGLConsumer is not attached to an OpenGL ES context

(~480 lines per restart), and the orphaned loop keeps consuming CPU cumulatively per swipe→restart cycle until the process is force-stopped.

Measured steady-state CPU with the app foregrounded (Galaxy S25, one full-screen animated Canvas):

State CPU
clean cold start 7–8%
after 1 × (recents swipe → warm restart) ~28%
after 4 cycles ~37%

After enough cycles the main thread saturates and the app appears to hang on startup (gray screen with spinner), which users experience as "the app won't open". A force-stop fully resets it.

Ordinary backgrounding (HOME key) does not leak — CPU drops to 0%. The leak is specific to the destroy-Activity-while-process-survives path.

Steps to reproduce

  1. App with a full-screen animated Canvas, android:launchMode="singleTask", and a foreground service so the process survives Activity destroy (a plain FGS is enough).
  2. Launch, open recents, swipe the app away.
  3. Relaunch from the launcher (warm start, same process).
  4. Observe the 60fps updateAndRelease() failed burst in logcat and the permanently elevated CPU. Repeat 2–3 to stack further.

Why upgrading doesn't help (checked against latest)

I compared the Android teardown path between 2.4.18 and 2.10.0 — it is unchanged:

  • SkiaTextureView.onSurfaceTextureDestroyedmApi.onSurfaceDestroyed(); return true;
  • RNSkOpenGLCanvasProvider::surfaceDestroyed() only nulls _surfaceHolder and releases the JNI ref — nothing stops the render/redraw loop that keeps requesting frames for that view.

#2054 reported the same log in 2023 and was closed the same day as an expected/harmless log. This report is the harm case: the log is the visible symptom of an orphaned 60fps loop that accumulates real CPU and eventually makes the app unusable. PR #3950 (v2.9.0, "fail gracefully when the OpenGL context or surface can't be created") does not cover it either — in this scenario the producer-side EGL surface stays "valid"; only the consumer is detached, so present() keeps going.

Workaround

Unmounting the Canvas whenever AppState !== 'active' prevents the orphaned loop (the Canvas is already unmounted by the time the swipe destroys the Activity) and survives the destroy path cleanly. That works, but it means every singleTask + FGS app has to know about this failure mode.

Ask: stop/park the view's render loop in surfaceDestroyed (or on onSurfaceTextureDestroyed) so a destroyed surface can't keep a 60fps loop alive on a dead consumer.

Environment

  • @shopify/react-native-skia: 2.4.18 (teardown path verified identical through 2.10.0)
  • react-native: 0.83.6 (new architecture / bridgeless), react: 19.2.0, Expo SDK 55
  • react-native-reanimated: 4.2.1
  • Android 16 (SDK 36), Samsung Galaxy S25 (SM-S931Q)
  • android:launchMode="singleTask", foreground service keeps the process alive across Activity destroy

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