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)
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 theexternalcounter grows. - Not the
SurfaceTexturelog 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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