Shopify / Shopify/react-native-skia

Android: `external` memory accounting climbs to ~3.4 GB and aborts Hermes after ~30s, while real memory stays flat (regression after 2.1.1)

Open
#3,999 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
8.6k
Forks
647
Avg merge
1d 17h
Merged PRs (30d)
35

Description

Summary

On Android, our app aborts deterministically ~30 seconds after launch with a Hermes
Max heap size was exceeded OOM. The JS heap is nearly empty at that moment (17.6 MB
allocated) and process RSS/PSS is flat, but Hermes reports external = 3,433,173,207
(~3.4 GB) of native memory registered against the heap.

This looks like an over-reporting bug in the external memory pressure accounting rather
than an actual leak: no real memory is being consumed. TOTAL PSS is flat and even
falls over the sampling window right up to the abort.

The app is stable on 2.1.1 and dies on every version we have tried from 2.2.12
onward, including 2.6.2 — which already contains #3495, #3709 and #3803.

Environment

@shopify/react-native-skia 2.6.2 (also reproduced on 2.2.12; 2.1.1 unaffected)
react-native 0.85.3
Expo SDK 56
Hermes hermes-engine 250829098.0.10 (Hermes V1)
New Architecture enabled (bridgeless — note [RNBridgeless] in the log)
Build type release (minified JS, Hermes bytecode)
Devices Android emulator API 36; originally found on a physical Pixel 9a (Android 17)
iOS not affected — same commit builds, renders and stays alive on the iOS simulator

What we see

Abort, from logcat:

I/HermesGC: OOM: [RNBridgeless] reason = Max heap size was exceeded
            (1 from category: vm_allocate_category),
            numCollections = 51,
            heapSize   =         20971520,   //  20.0 MB
            allocated  =         18436064,   //  17.6 MB
            va         =         20971520,   //  20.0 MB
            external   =       3433173207.   //   3.4 GB   <-- this
F/libc   : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
           in tid 4368 (mqt_v_js)

Process memory sampled every 10s via dumpsys meminfo over the same run — note it is
flat/declining, i.e. there is no real growth to match the 3.4 GB figure:

t(s)  TOTAL_PSS(kB)  JAVA_HEAP  NATIVE_HEAP  alive
   0         379187      36420       195220  yes
  10         360398      14880       194800  yes
  20         358858      12192       193988  yes
  30              ?          ?            ?  NO -- PROCESS GONE

Version matrix

Verified which fixes each tag contains using the compare API
(compare/<merge_commit>...<tag>ahead = contains):

version #3495 recorder pictures #3709 update pressure after dispose #3803 canvas texture pressure result
2.1.1 stable, ran >160s, PSS flat
2.2.12 OOM at ~31s, 2/2 launches
2.6.2 OOM at ~30s

Intermediate 2.4.x releases were also affected in our testing.

We checked 2.9.1 as well before filing: the only change to JsiSkHostObjects.h in
2.6.2...2.9.1 is an include-path refactor, and JsiSkImage.h only gains a Graphite
readback path — so there is no reason to expect the current release to behave
differently, and we did not test it.

What we ruled out

  • Not an actual leak. PSS, Java heap and native heap are all flat or falling
    (see trace above). Only the external counter grows.
  • Not the SurfaceTexture log noise. We see
    updateAndRelease: EGLConsumer is not attached to an OpenGL ES context (42
    occurrences in the 30s window), but this is present on the working 2.1.1 build too,
    and in fact more frequently there (1595 in 60s vs 243 in 40s when we measured it).
    It does not correlate with the abort.
  • Not the three prior memory-pressure fixes. All of #3495, #3709 and #3803 are
    ancestors of the 2.6.2 tag we tested.

Our usage

The screen that triggers this is our app's logged-out welcome screen, whose only Skia
content is a continuously animating Skottie canvas (a looping bot animation).
Elsewhere the app uses Canvas, Image, ImageSVG, Mask, Group, Paint,
shaders/gradients and useClock.

Because the animation runs continuously from launch, the abort time is very consistent
(~30s), which suggests the counter grows at a fixed rate per frame rather than in
response to any user interaction.

Reproduction

We do not yet have a minimal reproducer — this is currently only reproduced in our
application, which we cannot share. Given that the trigger appears to be a
continuously-animating Skottie canvas in a release build under Hermes V1 + bridgeless on
Android, we would expect a bare RN app looping a Skottie animation to show the same
external growth, and we are happy to attempt that reproducer if it would help.

We are also happy to run instrumented builds or patches against our app, since the
repro here is deterministic and takes 30 seconds.

Impact

We are pinned to 2.1.1 and cannot take Expo SDK 56's bundled 2.6.2, so we are holding
back roughly a year of Skia releases.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the 2.1.1 and 2.2.12 changes around JsiSkHostObjects.h and the Skottie canvas path, focusing on where Android external-memory pressure is reported. Reproduce with a release Hermes V1, bridgeless Android app looping a Skottie animation, then verify that external stays bounded and the app no longer aborts; no minimal reproducer or test is provided yet.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, react-native, typescript
Domain
mobile-dev, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.