diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index e84624567aad..3b82be921bee 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -23,6 +23,16 @@ namespace facebook::react { +namespace { + +// Native memory that the JS object returned by `createNode` keeps alive: the +// shadow node with its props, family and event emitter. A View with a few +// style props takes about 2.4 KB on Android (measured with heapprofd), a bit +// less on iOS, which doesn't keep the props as `folly::dynamic`. +constexpr size_t kCreatedShadowNodeNativeMemory = 2048; + +} // namespace + void UIManagerBinding::createAndInstallIfNeeded( jsi::Runtime& runtime, const std::shared_ptr& uiManager) { @@ -245,15 +255,23 @@ jsi::Value UIManagerBinding::get( return jsi::Value::undefined(); } - return valueFromShadowNode( - runtime, - uiManager->createNode( - tagFromValue(arguments[0]), - stringFromValue(runtime, arguments[1]), - surfaceIdFromValue(runtime, arguments[2]), - RawProps(runtime, arguments[3]), - std::move(instanceHandle)), - true); + auto node = valueFromShadowNode( + runtime, + uiManager->createNode( + tagFromValue(arguments[0]), + stringFromValue(runtime, arguments[1]), + surfaceIdFromValue(runtime, arguments[2]), + RawProps(runtime, arguments[3]), + std::move(instanceHandle)), + true) + .getObject(runtime); + // The shadow node is freed only when the GC collects this object, + // so tell the GC about the native memory it keeps alive. Without + // this, unmounted shadow trees can pile up until the JS heap + // itself grows enough for a full collection. + node.setExternalMemoryPressure( + runtime, kCreatedShadowNodeNativeMemory); + return node; } catch (const std::logic_error& ex) { LOG(FATAL) << "logic_error in createNode: " << ex.what(); } diff --git a/packages/react-native/src/private/__tests__/ShadowNodeExternalMemory-itest.js b/packages/react-native/src/private/__tests__/ShadowNodeExternalMemory-itest.js new file mode 100644 index 000000000000..7980597ee43d --- /dev/null +++ b/packages/react-native/src/private/__tests__/ShadowNodeExternalMemory-itest.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @fantom_mode * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; +import {View} from 'react-native'; + +// Must match `kCreatedShadowNodeNativeMemory` in UIManagerBinding.cpp. +const CREATED_SHADOW_NODE_NATIVE_MEMORY = 2048; + +function getExternalMemoryAfterGC(): number { + global.gc(); + const stats = nullthrows( + nullthrows(global.HermesInternal).getInstrumentedStats, + )(); + const externalBytes = stats.js_externalBytes; + if (typeof externalBytes !== 'number') { + throw new Error('Expected js_externalBytes to be a number'); + } + return externalBytes; +} + +describe('shadow node external memory', () => { + it('reports the native memory of created shadow nodes to the GC until they are collected', () => { + const before = getExternalMemoryAfterGC(); + + let root: ?Fantom.Root = Fantom.createRoot(); + Fantom.runTask(() => { + nullthrows(root).render( + + {Array.from({length: 100}, (_, i) => ( + + ))} + , + ); + }); + + // The 100 children and their parent are alive, so their native memory + // stays reported after a GC. + expect(getExternalMemoryAfterGC() - before).toBeGreaterThanOrEqual( + 101 * CREATED_SHADOW_NODE_NATIVE_MEMORY, + ); + + Fantom.runTask(() => { + nullthrows(root).destroy(); + }); + root = null; + + // Once the tree is unmounted and collected, its memory isn't reported + // anymore. + expect(getExternalMemoryAfterGC() - before).toBeLessThan( + CREATED_SHADOW_NODE_NATIVE_MEMORY, + ); + }); +});