Skip to content

FirstDrawDoneListener still leaks one react-native-screens fragment per navigation after #5567 #6357

Closed

Description

@correalm

Integration

sentry-android (through @sentry/react-native)

Version

sentry-android 8.61.0 (@sentry/react-native 8.31.0)

Steps to Reproduce

  1. React Native app (RN 0.81.5, new architecture) with a native stack (react-native-screens 4.17.1) and reactNavigationIntegration({ enableTimeToInitialDisplay: true }).
  2. Push a screen onto the stack, wait for it to render, then go back. Repeat 11 times.
  3. Force a GC and take a heap dump (am dumpheap -g).

Expected Result

The popped ScreenStackFragments are collected.

Actual Result

Each push leaves one ScreenStackFragment and its whole native view tree in memory. On a 256 MB heap device the app ends in OutOfMemoryError after long sessions.

Counts after a forced GC, 11 push/pop cycles:

Class Before After
com.swmansion.rnscreens.ScreenStackFragment 3 14
io.sentry.android.core.internal.util.FirstDrawDoneListener 1 12
com.facebook.react.views.view.ReactViewGroup 143 638

Retention path:

ViewRootImpl
  .mAttachInfo -> View$AttachInfo
  .mTreeObserver -> ViewTreeObserver
  .mOnGlobalLayoutListeners -> ViewTreeObserver$CopyOnWriteArray
  .[n] -> FirstDrawDoneListener$2
  .val$view -> com.swmansion.rnscreens.stack.views.ScreensCoordinatorLayout
  .fragment -> com.swmansion.rnscreens.ScreenStackFragment

Cause

#5567 (fix for #5495) makes the OnGlobalLayoutListener call view.getViewTreeObserver().removeOnGlobalLayoutListener(this). That does not work for the views RNSentryReactFragmentLifecycleTracer registers:

  1. onDraw() adds the layout listener to the window's ViewTreeObserver.
  2. A static screen has no layout pass after its first draw, so the listener does not run yet.
  3. The next layout pass comes when the screen is popped. The fragment view is already detached, so view.getViewTreeObserver() returns the view's own floating observer, not the window's.
  4. The remove call does nothing. The listener stays on the window's observer and keeps the detached view alive.

Fix

#6152 fixes it: it posts the OnDrawListener removal to the main thread right after the draw and drops the layout listener. We applied the same change with patch-package (a copy of the listener in @sentry/react-native used by RNSentryReactFragmentLifecycleTracer) and ran the same test:

Class Before After 11 cycles
ScreenStackFragment 3 4
FirstDrawDoneListener 1 1
ReactViewGroup 143 190

This matches enableTimeToInitialDisplay: false, so the leak is gone with TTID still on. The #6152 description says the old behaviour "isn't a memory leak" because everything dies with the activity. In a single-Activity React Native app the activity lives for the whole session, so it is an unbounded leak. We would be glad to see #6152 merged.

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions