Describe the bug
A test suite with enough test files OOMs the Harness node runner (FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory). It is per-test-FILE, not per-render-time / per-bridge-traffic / Rive-specific / a single heavy test — 40 files that each only render(<View/>) + cleanup() reproduce it, while 20+ its in a single file stay bounded.
Root cause: each test file is fetched from Metro as its own bundle entry point (<file>.bundle?modulesOnly=true, see packages/runtime/src/bundler/bundle.ts). Metro keeps a full dependency graph in memory per distinct entry (in IncrementalBundler's revision cache, for delta updates) and only frees it when the server shuts down — so the run retains one graph (~tens of MB) per test file, climbing ~linearly with file count until the runner OOMs. Disabling HMR does not help (the caching is in the delta/revision cache, independent of watch/HMR).
Distinguishing experiments (iOS runner, --max-old-space-size=2048, sampling runner RSS):
| Test shape |
Files |
Result |
Peak RSS |
autoPlay RiveView + dataBind, 20 its |
1 |
✅ pass |
~600–850 MB |
Animated loop (JS-driven), 30 its |
1 |
✅ pass |
bounded |
Animated loop, 1 it each |
40 |
✋ OOM |
800 → 2103 MB |
render(<View/>) only, 1 it each |
40 |
✋ OOM |
865 → 2195 MB |
Heap snapshot at ~1 GB (streamed aggregate): 326 MB / 5.6M array, 180 MB / 5.8M object, 103 MB / 1.25M string; top containers are multiple Arrays of ~88K elements — consistent with a retained Metro module graph per processed file.
System Info
node 20 (CI) / 24 (local). React Native 0.79–0.80, Metro 0.82/0.83. iOS simulator, macOS.
Not version-specific to a recent change — reproduces on 1.3.0 and 1.4.0-rc.1.
React Native Harness Version
1.3.0 (npm latest) and 1.4.0-rc.1 (npm rc) — both reproduce.
Reproduction
Rive-free, self-contained repro (generator + jest config + expected RSS curve) on a branch:
https://github.com/mfazekas/react-native-harness/tree/repro/per-file-metro-graph-leak (apps/playground/LEAK_REPRO.md).
cd apps/playground
LEAK_FILES=40 node scripts/generate-leak-repro.mjs
NODE_OPTIONS="--max-old-space-size=2048" \
jest -c jest.harness.leak.config.mjs --selectProjects react-native-harness --harnessRunner ios
Steps to reproduce
- Generate N empty
render(<View/>) harness test files (above).
- Run them on the iOS runner under a heap cap; sample the runner node process RSS.
- RSS climbs ~30 MB/file and never releases →
Reached heap limit ... JavaScript heap out of memory. A correctly-bounded runner keeps RSS flat regardless of file count.
Real occurrence (CI)
Originally hit in rive-app/rive-nitro-react-native · run 28585932939 · job test-harness-ios (~50-file iOS Harness suite). The node runner (node/20.19.0) aborts ~256s in:
<--- Last few GCs --->
[8372:0x158008000] 256128 ms: Mark-Compact (reduce) 2054.2 (2093.3) -> 2044.2 (2087.5) MB ...
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
1: 0x104e75334 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [.../node/20.19.0/arm64/bin/node]
2: 0x105031e84 v8::internal::V8::FatalProcessOutOfMemory(...)
10: 0x105a0ddf0 Builtins_NewPromiseCapability
11: 0x105a0ea34 Builtins_PromiseAll
13: 0x1059dbee4 Builtins_ArrayMap
15: 0x1059b5844 Builtins_AsyncGeneratorResumeNext
20: 0x105956b94 Builtins_RunMicrotasks
(The PromiseAll/microtask frames are just the runner orchestrating files while the heap is already exhausted — the allocation that finally fails is incidental, not the leak site.)
Minimal reproduction in CI (chromium, no macOS/Xcode)
Reproduced on the cheap chromium runner (the leak is node-side, so it's platform-agnostic): generate 300 empty render(<View/>) files, disable per-file environment reset, and run under --max-old-space-size=1536.
🔴 Failing run: https://github.com/mfazekas/react-native-harness/actions/runs/28651060708
(fork mfazekas/react-native-harness, branch repro/per-file-metro-graph-leak, workflow .github/workflows/leak-repro.yml)
It processed 26 distinct empty test files, the runner heap climbed to the cap, then aborted:
<--- Last few GCs --->
[3651:0x3fe19000] 42976 ms: Mark-Compact (reduce) 1534.3 (1538.9) -> 1534.3 (1537.9) MB ...
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
3: 0x13bf47f v8::internal::V8::FatalProcessOutOfMemory(...) [node]
Aborted (core dumped) # exit code 134
~26 files × ~40 MB retained graph/file ≈ 1 GB on top of baseline ≈ the 1536 MB cap — one Metro graph retained per file, confirming the root cause. Proposed fix: #145.
Describe the bug
A test suite with enough test files OOMs the Harness node runner (
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory). It is per-test-FILE, not per-render-time / per-bridge-traffic / Rive-specific / a single heavy test — 40 files that each onlyrender(<View/>)+cleanup()reproduce it, while 20+its in a single file stay bounded.Root cause: each test file is fetched from Metro as its own bundle entry point (
<file>.bundle?modulesOnly=true, seepackages/runtime/src/bundler/bundle.ts). Metro keeps a full dependency graph in memory per distinct entry (inIncrementalBundler's revision cache, for delta updates) and only frees it when the server shuts down — so the run retains one graph (~tens of MB) per test file, climbing ~linearly with file count until the runner OOMs. Disabling HMR does not help (the caching is in the delta/revision cache, independent ofwatch/HMR).Distinguishing experiments (iOS runner,
--max-old-space-size=2048, sampling runner RSS):itsitsiteachrender(<View/>)only, 1iteachHeap snapshot at ~1 GB (streamed aggregate):
326 MB / 5.6M array,180 MB / 5.8M object,103 MB / 1.25M string; top containers are multipleArrays of ~88K elements — consistent with a retained Metro module graph per processed file.System Info
React Native Harness Version
1.3.0 (npm
latest) and 1.4.0-rc.1 (npmrc) — both reproduce.Reproduction
Rive-free, self-contained repro (generator + jest config + expected RSS curve) on a branch:
https://github.com/mfazekas/react-native-harness/tree/repro/per-file-metro-graph-leak (
apps/playground/LEAK_REPRO.md).Steps to reproduce
render(<View/>)harness test files (above).Reached heap limit ... JavaScript heap out of memory. A correctly-bounded runner keeps RSS flat regardless of file count.Real occurrence (CI)
Originally hit in rive-app/rive-nitro-react-native · run 28585932939 · job
test-harness-ios(~50-file iOS Harness suite). The node runner (node/20.19.0) aborts ~256s in:(The
PromiseAll/microtask frames are just the runner orchestrating files while the heap is already exhausted — the allocation that finally fails is incidental, not the leak site.)Minimal reproduction in CI (chromium, no macOS/Xcode)
Reproduced on the cheap
chromiumrunner (the leak is node-side, so it's platform-agnostic): generate 300 emptyrender(<View/>)files, disable per-file environment reset, and run under--max-old-space-size=1536.🔴 Failing run: https://github.com/mfazekas/react-native-harness/actions/runs/28651060708
(fork
mfazekas/react-native-harness, branchrepro/per-file-metro-graph-leak, workflow.github/workflows/leak-repro.yml)It processed 26 distinct empty test files, the runner heap climbed to the cap, then aborted:
~26 files × ~40 MB retained graph/file ≈ 1 GB on top of baseline ≈ the 1536 MB cap — one Metro graph retained per file, confirming the root cause. Proposed fix: #145.