Skip to content

Commit 1161fb4

Browse files
Add cloneMultiple method to the ShadowNode class (facebook#50624)
Summary: This PR adds a new cloning method, allowing for updating multiple nodes in a single transaction. It works in two phases: 1. Find which nodes have to be cloned (i.e. nodes given on input and all their ancestors) 2. Clone nodes in the bottom up order - so that every node is cloned exactly once So the idea is that when we want to update all the red nodes in this picture, we first find the nodes in the green area and the clone only them in the correct order (children are cloned before parents): ![Screenshot 2025-04-10 at 14 31 14](https://github.com/user-attachments/assets/f397eeff-8279-4d0c-bf87-083bfb44b86a) Adapting this method [brought a huge performance gain to reanimated](software-mansion/react-native-reanimated#6214). I want to upstream it, so that: 1. we can optimize it further, because making it a part of the `ShadowNode` class gives us access to the parent field in `ShadowNodeFamily` so we can traverse the tree upwards, allowing for a optimal implementation of the first phase (in reanimated we repeatedly call `getAncestors`, which revisits some nodes multiple times) 2. the community can use it A naive approach that calls `cloneTree` for every node is much slower, as it has to repeat many operations. ## Changelog: [GENERAL] [ADDED] - Added `cloneMultiple` to `ShadowNode` class. Pull Request resolved: facebook#50624 Test Plan: I tested it with the following reanimated implementation and everything works fine: <details> ```c++ const auto callback = [&](const ShadowNode &shadowNode, const std::optional<ShadowNode::ListOfShared> &newChildren) { return shadowNode.clone( {mergeProps(shadowNode, propsMap, shadowNode.getFamily()), newChildren ? std::make_shared<ShadowNode::ListOfShared>(*newChildren) : ShadowNodeFragment::childrenPlaceholder(), shadowNode.getState()}); }; return std::static_pointer_cast<RootShadowNode>( oldRootNode.cloneMultiple(families, callback)); ``` </details> I would like to add tests for it, but I'm not sure what's the best approach for that in the repo. Reviewed By: mdvacca Differential Revision: D75284060 Pulled By: javache fbshipit-source-id: 0704c4386c3041eb368adf6950d46de197479058
1 parent 56c1ab3 commit 1161fb4

File tree

3 files changed

+118
-2
lines changed

3 files changed

+118
-2
lines changed

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,79 @@ std::shared_ptr<ShadowNode> ShadowNode::cloneTree(
401401
return std::const_pointer_cast<ShadowNode>(childNode);
402402
}
403403

404+
namespace {
405+
406+
std::shared_ptr<ShadowNode> cloneMultipleRecursive(
407+
const ShadowNode& shadowNode,
408+
const std::unordered_set<const ShadowNodeFamily*>& familiesToUpdate,
409+
const std::unordered_map<const ShadowNodeFamily*, int>& childrenCount,
410+
const std::function<std::shared_ptr<
411+
ShadowNode>(const ShadowNode&, const ShadowNodeFragment&)>& callback) {
412+
const auto* family = &shadowNode.getFamily();
413+
auto& children = shadowNode.getChildren();
414+
std::shared_ptr<ShadowNode::ListOfShared> newChildren;
415+
auto count = childrenCount.at(family);
416+
417+
for (int i = 0; count > 0 && i < children.size(); i++) {
418+
const auto childFamily = &children[i]->getFamily();
419+
if (childrenCount.contains(childFamily)) {
420+
count--;
421+
if (!newChildren) {
422+
newChildren = std::make_shared<ShadowNode::ListOfShared>(children);
423+
}
424+
(*newChildren)[i] = cloneMultipleRecursive(
425+
*children[i], familiesToUpdate, childrenCount, callback);
426+
}
427+
}
428+
429+
ShadowNodeFragment fragment{.children = newChildren};
430+
if (familiesToUpdate.contains(family)) {
431+
return callback(shadowNode, fragment);
432+
}
433+
return shadowNode.clone(fragment);
434+
}
435+
436+
} // namespace
437+
438+
std::shared_ptr<ShadowNode> ShadowNode::cloneMultiple(
439+
const std::unordered_set<const ShadowNodeFamily*>& familiesToUpdate,
440+
const std::function<std::shared_ptr<ShadowNode>(
441+
const ShadowNode& oldShadowNode,
442+
const ShadowNodeFragment& fragment)>& callback) const {
443+
std::unordered_map<const ShadowNodeFamily*, int> childrenCount;
444+
445+
for (const auto& family : familiesToUpdate) {
446+
if (childrenCount.contains(family)) {
447+
continue;
448+
}
449+
450+
childrenCount[family] = 0;
451+
452+
auto ancestor = family->parent_.lock();
453+
while ((ancestor != nullptr) && ancestor != family_) {
454+
auto ancestorIt = childrenCount.find(ancestor.get());
455+
if (ancestorIt != childrenCount.end()) {
456+
ancestorIt->second++;
457+
break;
458+
}
459+
childrenCount[ancestor.get()] = 1;
460+
461+
ancestor = ancestor->parent_.lock();
462+
}
463+
464+
if (ancestor == family_) {
465+
childrenCount[ancestor.get()]++;
466+
}
467+
}
468+
469+
if (childrenCount.empty()) {
470+
return nullptr;
471+
}
472+
473+
return cloneMultipleRecursive(
474+
*this, familiesToUpdate, childrenCount, callback);
475+
}
476+
404477
#pragma mark - DebugStringConvertible
405478

406479
#if RN_DEBUG_STRING_CONVERTIBLE

packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class ShadowNode : public Sealable,
3737
using Weak [[deprecated("Use std::weak_ptr<const ShadowNode> instead")]] =
3838
std::weak_ptr<const ShadowNode>;
3939
// TODO(T223558094): delete this in the next version.
40-
using Unshared [[deprecated("Use std::weak_ptr<const ShadowNode> instead")]] =
40+
using Unshared [[deprecated("Use std::shared_ptr<ShadowNode> instead")]] =
4141
std::shared_ptr<ShadowNode>;
4242
using ListOfShared = std::vector<Shared>;
4343
using ListOfWeak = std::vector<std::weak_ptr<const ShadowNode>>;
@@ -111,6 +111,19 @@ class ShadowNode : public Sealable,
111111
const std::function<std::shared_ptr<ShadowNode>(
112112
const ShadowNode& oldShadowNode)>& callback) const;
113113

114+
/*
115+
* Clones the nodes (and the subtree containing all the nodes) by
116+
* replacing the `oldShadowNode` for every `shadowNodeFamily` from
117+
* `familiesToUpdate` with a node that `callback` returns.
118+
*
119+
* Returns `nullptr` if the operation cannot be performed successfully.
120+
*/
121+
std::shared_ptr<ShadowNode> cloneMultiple(
122+
const std::unordered_set<const ShadowNodeFamily*>& familiesToUpdate,
123+
const std::function<std::shared_ptr<ShadowNode>(
124+
const ShadowNode& oldShadowNode,
125+
const ShadowNodeFragment& fragment)>& callback) const;
126+
114127
#pragma mark - Getters
115128

116129
ComponentName getComponentName() const;

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class ShadowNodeTest : public testing::Test {
3232
* </AB>
3333
* <AC/>
3434
* </A>
35-
* </Z>
35+
* <Z/>
3636
*/
3737

3838
auto props = std::make_shared<const TestProps>();
@@ -295,3 +295,33 @@ TEST_F(ShadowNodeTest, handleRuntimeReferenceTransferOnClone) {
295295
// The wrappedShadowNode should still reference nodeABRev2
296296
EXPECT_EQ(wrappedShadowNode->shadowNode, nodeABRev2);
297297
}
298+
299+
TEST_F(ShadowNodeTest, cloneMultiple) {
300+
auto newProps = std::make_shared<const TestProps>();
301+
auto newRoot = nodeA_->cloneMultiple(
302+
{&nodeA_->getFamily(), &nodeAB_->getFamily()},
303+
[&](const ShadowNode& oldShadowNode, const ShadowNodeFragment& fragment) {
304+
return oldShadowNode.clone({
305+
.props = newProps,
306+
.children = fragment.children,
307+
.state = fragment.state,
308+
});
309+
});
310+
311+
EXPECT_EQ(newRoot->getTag(), nodeA_->getTag());
312+
EXPECT_EQ(newRoot->getProps(), newProps);
313+
314+
auto newNodeAA = newRoot->getChildren()[0];
315+
EXPECT_EQ(newNodeAA->getTag(), nodeAA_->getTag());
316+
EXPECT_EQ(newNodeAA->getProps(), nodeAA_->getProps());
317+
// AA was cloned when its parent was cloned as it was shared
318+
EXPECT_NE(newNodeAA.get(), nodeAA_.get());
319+
320+
auto newNodeAB = newRoot->getChildren()[1];
321+
EXPECT_EQ(newNodeAB->getTag(), nodeAB_->getTag());
322+
EXPECT_EQ(newNodeAB->getProps(), newProps);
323+
324+
auto newNodeABA = newNodeAB->getChildren()[0];
325+
EXPECT_EQ(newNodeABA->getTag(), nodeABA_->getTag());
326+
EXPECT_EQ(newNodeABA.get(), nodeABA_.get());
327+
}

0 commit comments

Comments
 (0)