Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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>& uiManager) {
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<View>
{Array.from({length: 100}, (_, i) => (
<View key={i} />
))}
</View>,
);
});

// 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,
);
});
});
Loading