Repository navigation
Report the native memory of created shadow nodes to the JS GC - #59702
Open
hryhoriiK97 wants to merge 1 commit into
Open
hryhoriiK97 wants to merge 1 commit into
hryhoriiK97 wants to merge 1 commit into
Conversation
The JS object that createNode returns owns the new shadow node (with its props, family and event emitter), so that native memory is only freed when the GC collects the object. The GC didn't know about it: it saw small JS objects and rarely ran a full collection, so the shadow nodes of unmounted trees piled up. Report a 2 KB estimate per created node with setExternalMemoryPressure, as the Blob module already does for blob data, so that the GC collects them in time. A View with a few style props takes about 2.4 KB on Android (measured with heapprofd), a bit less on iOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Summary:
The JS object that
createNodereturns owns the new shadow node, so the shadow node (with its props, family and event emitter) is only freed when the GC collects that object. The GC doesn't know about this native memory: it only sees a small JS object. With Hermes, it can go a long time without a full collection, and the shadow nodes of trees that were already unmounted pile up in native memory.This change reports an estimate of that native memory with
setExternalMemoryPressurewhencreateNodereturns, like the Blob module does for blob data (#44336). The GC then accounts for it and collects unmounted trees in time.The estimate is 2 KB per node. A
Viewwith a few style props takes about 2.4 KB on Android (shadow node, props, family and event emitter, measured with heapprofd), a bit less on iOS, which doesn't keep afolly::dynamiccopy of the props. Only nodes fromcreateNodeare reported, not clones: that's where most of the memory is, and reporting clones too made no difference in the measurements below while it would make the GC run more often during frequent re-renders. JSC'ssetExternalMemoryPressureis a no-op, so this only changes behavior with Hermes.Changelog:
[GENERAL] [FIXED] - With Hermes, the native memory of unmounted shadow trees is freed in time instead of piling up until the JS heap grows
Test Plan:
Fantom test.
ShadowNodeExternalMemory-itest.jsrenders 101 Views, checks that Hermes' external memory (HermesInternal.getInstrumentedStats().js_externalBytes) grows by at least 101 × 2 KB, then unmounts the tree and checks it goes back to where it started after a GC.It passes with this change and fails without it (
Expected 0 to be greater than or equal to 206848), with the Fantom tester rebuilt in each case.yarn flow, ESLint, Prettier andnode scripts/clang-format.js --checkpass on the changed files.Android devices. RNTester release build, a screen that mounts 1000
Views in aScrollViewand unmounts them again, 24 times. After each unmount, a Java GC is forced and the app's native heap is read fromdumpsys meminfowith nothing mounted. The same APK runs both arms, with the change switched by a system property; on the Pixel 9a the runs alternate (without, with, with, without), on the Galaxy A16 there is one run per arm.Without the change, native memory grows by about 1 MB per cycle on both phones. With it, Hermes runs a full collection after about 10 cycles and native memory then stays flat. Total memory (PSS) on the Pixel 9a, mean of cycles 13–24: 176.2 / 173.8 MB without, 164.2 / 161.1 MB with. Reporting clones as well gave the same result (38.8 / 39.2 MB).
Cost. Galaxy A16, a benchmark inside the same RNTester build: 33 rounds that mount and unmount 1000
Views and then 1000 cards (about 10,000 nodes), the first 3 rounds discarded. Mount time is measured fromsetStateto the layout effect of the commit, and Hermes' GC statistics are compared over the whole run. Two runs per arm:Views, median of 60Mount time doesn't change measurably. Hermes collects about twice as often, but the collections are smaller: total GC time grows by about 8%, and the longest one doesn't get longer. JS allocation grows by 1.2%, from the internal property Hermes adds to each node's object. With the 1000 cards mounted, the same JS allocation work took as long and ran as many GCs as with nothing mounted, in both arms, so the memory reported for a large mounted tree doesn't make collections more frequent.
iPhone. iPhone 15 Pro (iOS 26.6), RNTester release build, the same screen and 24 cycles. The app's memory footprint (
phys_footprint) is read every second and taken 7.5 s after each unmount, with nothing mounted. The same build runs both arms, with the change switched by a launch argument; the runs alternate (without, with, with, without).Without the change, the footprint grows by about 3 MB per cycle until Hermes runs a full collection on its own after 9 to 13 cycles. With it, the collection comes after about 6 cycles, and the footprint then goes down to 42–44 MB.
🤖 Generated with Claude Code