Skip to content

Commit d0078c2

Browse files
shubh73meta-codesync[bot]
authored andcommitted
Avoid copying the children list twice when cloning a path to the root (#58690)
Summary: `ShadowNode::cloneTree` copies the parent's children into a local vector, swaps in the new child, then copies that local again into the `std::make_shared` passed to `clone()`. The second copy bumps and drops the refcount of every sibling at each level on the path to the root. The local is dead after the call, so this moves it instead. `UIManager::updateShadowTree` has the same double copy, and `addAncestorsToUpdateList` copied the whole children vector just to read one element. Output is unchanged. This is a small cleanup rather than a meaningful perf win, since the parent clone itself does O(N) Yoga work per child. Host benchmark (clang -O3, Apple M4 Pro), `cloneTree` on one leaf of a View with N View siblings, median of 7 runs: | Children | Before | After | Delta | |---|---|---|---| | 10 | 1.68 µs | 1.65 µs | -1.7% | | 100 | 16.02 µs | 15.76 µs | -1.6% | | 1000 | 164.23 µs | 160.84 µs | -2.1% | At 10–100 children the difference is within noise. <details><summary>Benchmark</summary> ```cpp #include <benchmark/benchmark.h> #include <react/renderer/components/view/ViewComponentDescriptor.h> #include <react/renderer/core/EventDispatcher.h> #include <react/renderer/core/ShadowNode.h> #include <react/utils/ContextContainer.h> #include <memory> #include <vector> namespace facebook::react { auto contextContainer = std::make_shared<const ContextContainer>(); auto eventDispatcher = std::shared_ptr<EventDispatcher>{nullptr}; auto viewComponentDescriptor = ViewComponentDescriptor{ComponentDescriptorParameters{ .eventDispatcher = eventDispatcher, .contextContainer = contextContainer}}; std::shared_ptr<ShadowNode> createViewShadowNode( Tag tag, std::shared_ptr<const std::vector<std::shared_ptr<const ShadowNode>>> children) { auto family = viewComponentDescriptor.createFamily( {.tag = tag, .surfaceId = 1, .instanceHandle = nullptr}); return viewComponentDescriptor.createShadowNode( {.props = ViewShadowNode::defaultSharedProps(), .children = std::move(children)}, family); } // Clones the path from the root to a single leaf of a parent with // `state.range(0)` children, as a state update on that leaf would. static void cloneTreeWithSiblings(benchmark::State& state) { auto childCount = static_cast<Tag>(state.range(0)); auto children = std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(); for (Tag tag = 0; tag < childCount; tag++) { children->push_back(createViewShadowNode( tag + 2, ShadowNode::emptySharedShadowNodeSharedList())); } auto leaf = children->at(childCount / 2); auto root = createViewShadowNode(1, children); for (auto _ : state) { auto newRoot = root->cloneTree( leaf->getFamily(), [](const ShadowNode& oldShadowNode) { return oldShadowNode.clone({}); }); benchmark::DoNotOptimize(newRoot); } } BENCHMARK(cloneTreeWithSiblings)->Arg(10)->Arg(100)->Arg(1000); } // namespace facebook::react BENCHMARK_MAIN(); ``` </details> ## Changelog: [GENERAL] [CHANGED] - Avoid a redundant copy of the children list in `ShadowNode::cloneTree` and `UIManager::updateShadowTree` Pull Request resolved: #58690 Test Plan: Added `cloneTree` coverage to `ShadowNodeTest`. Built the renderer gtests on the host with CMake (Debug): - `core/tests`: 65/65 pass (the 4 Hermes-dependent files were not built) - `mounting/tests` StateReconciliationTest, StackingContextTest, OrderIndexTest: 21/21 pass Not built for Android/iOS and not measured on a device. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: javache Differential Revision: D122059304 Pulled By: fabriziocucci fbshipit-source-id: 699574af73217ad569ba840c834a55de9c5dd019
1 parent 25041be commit d0078c2

3 files changed

Lines changed: 38 additions & 3 deletions

File tree

‎packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ std::shared_ptr<ShadowNode> ShadowNode::cloneTree(
411411
childNode = parentNode.clone(
412412
{.children =
413413
std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
414-
children)});
414+
std::move(children))});
415415
}
416416

417417
return std::const_pointer_cast<ShadowNode>(childNode);

‎packages/react-native/ReactCommon/react/renderer/core/tests/ShadowNodeTest.cpp‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,37 @@ TEST_F(ShadowNodeTest, cloneMultipleWithEmptyFamilySet) {
429429

430430
EXPECT_EQ(result, nullptr);
431431
}
432+
433+
TEST_F(ShadowNodeTest, cloneTree) {
434+
auto newProps = std::make_shared<const TestProps>();
435+
auto newRoot = nodeA_->cloneTree(
436+
nodeABB_->getFamily(), [&](const ShadowNode& oldShadowNode) {
437+
return oldShadowNode.clone({.props = newProps});
438+
});
439+
440+
EXPECT_EQ(newRoot->getTag(), nodeA_->getTag());
441+
EXPECT_NE(newRoot.get(), nodeA_.get());
442+
EXPECT_EQ(newRoot->getChildren().size(), 3);
443+
EXPECT_EQ(newRoot->getChildren()[0]->getTag(), nodeAA_->getTag());
444+
EXPECT_EQ(newRoot->getChildren()[0]->getProps(), nodeAA_->getProps());
445+
EXPECT_EQ(newRoot->getChildren()[2]->getTag(), nodeAC_->getTag());
446+
EXPECT_EQ(newRoot->getChildren()[2]->getProps(), nodeAC_->getProps());
447+
448+
auto newNodeAB = newRoot->getChildren()[1];
449+
EXPECT_EQ(newNodeAB->getTag(), nodeAB_->getTag());
450+
EXPECT_NE(newNodeAB.get(), nodeAB_.get());
451+
EXPECT_EQ(newNodeAB->getChildren().size(), 2);
452+
EXPECT_EQ(newNodeAB->getChildren()[0]->getTag(), nodeABA_->getTag());
453+
EXPECT_EQ(newNodeAB->getChildren()[0]->getProps(), nodeABA_->getProps());
454+
455+
auto newNodeABB = newNodeAB->getChildren()[1];
456+
EXPECT_EQ(newNodeABB->getTag(), nodeABB_->getTag());
457+
EXPECT_NE(newNodeABB.get(), nodeABB_.get());
458+
EXPECT_EQ(newNodeABB->getProps(), newProps);
459+
460+
// The original tree is left untouched.
461+
EXPECT_EQ(nodeA_->getChildren().size(), 3);
462+
EXPECT_EQ(nodeA_->getChildren()[1].get(), nodeAB_.get());
463+
EXPECT_EQ(nodeAB_->getChildren().size(), 2);
464+
EXPECT_EQ(nodeAB_->getChildren()[1].get(), nodeABB_.get());
465+
}

‎packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerUpdateShadowTree.cpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ void addAncestorsToUpdateList(
4444
while (!currentShadowNode->getChildren().empty() &&
4545
currentShadowNode->getTag() != shadowNode->getTag()) {
4646
ancestorShadowNodesShared[ancestorIndex] = currentShadowNode;
47-
auto children = currentShadowNode->getChildren();
47+
const auto& children = currentShadowNode->getChildren();
4848
auto childIndex = ancestors[ancestorIndex].second;
4949
currentShadowNode = children[childIndex];
5050
ancestorIndex++;
@@ -201,7 +201,8 @@ void UIManager::updateShadowTree(
201201
auto cloned = oldShadowNode->clone(
202202
{.props = newProps,
203203
.children = std::make_shared<
204-
std::vector<std::shared_ptr<const ShadowNode>>>(children)});
204+
std::vector<std::shared_ptr<const ShadowNode>>>(
205+
std::move(children))});
205206
clonedShadowNodes.insert({oldShadowNode->getTag(), std::move(cloned)});
206207
} else {
207208
LOG(ERROR) << "oldShadowNode is null";

0 commit comments

Comments
 (0)