fix(runtime): bound promise tracker to prevent OOM#143
Merged
V3RON merged 4 commits intoJul 3, 2026
Conversation
The promise tracker retained a record (with a captured stack) for every promise created via the global constructor and only released it on settle. Records were never drained between tests/files and had no size bound, so an app that continuously creates promises that are abandoned before they settle (animations, requestAnimationFrame loops, polling, async data-binding) grew the JS heap linearly until 'JavaScript heap out of memory'. Release a promise's record as soon as the promise is garbage-collected (via FinalizationRegistry) and cap the number of retained records as a synchronous backstop, keeping memory bounded regardless of app bridge/render traffic. Fixes callstackincubator#142
|
@mfazekas is attempting to deploy a commit to the Callstack Team on Vercel. A member of the Team first needs to authorize it. |
Replace runtime reproduction tests with a playground harness regression that verifies abandoned promises are reclaimed after GC.
The runtime unit tests continue to cover promise tracker behavior, while the playground harness GC assertion depends on non-deterministic FinalizationRegistry scheduling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes an unbounded memory leak in the runtime promise tracker (
@react-native-harness/runtime) that could exhaust the JS heap (JavaScript heap out of memory) during long runs.The tracker recorded every promise created via the global constructor — plus a captured stack — and only released it when the promise settled. Records were never drained between tests/files and had no size bound. Any app that keeps creating promises that are abandoned before they settle (animations,
requestAnimationFrameloops, polling, async data-binding) therefore grew the heap linearly with elapsed render time until the process aborted.The fix:
FinalizationRegistry— releases a promise's tracking record as soon as the promise itself is garbage-collected. A collected promise can no longer settle or keep anything pending, so retaining its record (and stack) is pure waste. This is the root-cause fix.MAX_TRACKED_PROMISES = 10_000, evict-oldest)— a synchronous backstop for when GC can't keep up under heavy allocation pressure. Timeout diagnostics only ever display 10 pending promises, so this is invisible in normal use.Related Issue
Fixes #142
Context
The tracker only exists to report which promises are still pending when a test times out. It does not need the full session history — only currently-live, unsettled promises. Both changes preserve that behavior: live pending promises stay tracked, while collected/abandoned ones are released and the total is bounded.
No public API changes. The existing
clearTrackedPromises()/getPendingPromises()surface is unchanged.Testing
promise-tracker-leak.repro.test.ts— a regression guard that fails on the old code (30k records retained, never drained) and passes now (bounded).promise-tracker-oom.repro.test.ts— an opt-in (RN_HARNESS_OOM_REPRO=1) reproduction that drives the real tracker under a low heap cap. Before: RSS climbs linearly toFATAL ERROR: Reached heap limit ... JavaScript heap out of memory. After: 50M abandoned promises under--max-old-space-size=512complete cleanly with RSS pinned at the cap.packages/runtimesuite compared against a pristine baseline: the leak repro flips fail→pass and there are no new failures (the pre-existing environment-specific failures are unchanged by this PR).