From 01eafef8b43b64a2a4eb4a693603e4b516bc8c56 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 14 Mar 2025 08:42:23 -0700 Subject: [PATCH] Add `updateShadowTree` to UIManager (#50020) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50020 Changelog: [Internal] [General][Added] - Add `updateShadowTree` to UIManager We have an internal method `updateShadowTree` that does similar thing as `ReanimatedModuleProxy::cloneShadowTreeWithNewProps` in reanimated (https://github.com/software-mansion/react-native-reanimated/blob/6516b2e4e12c0186cdf00e7f85b8fef820b97088/packages/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp#L67 it's logically similar but not copied over), with which we can efficiently commit props for multiple shadowNodes onto ShadowTree. it guarantees every ShadowNode that needs to be updated in this commit is only cloned once. Reviewed By: javache Differential Revision: D71129150 fbshipit-source-id: 92156114ec6c2694e1639cf2ecf153d243ade769 --- .../react/renderer/uimanager/UIManager.h | 3 + .../uimanager/UIManagerUpdateShadowTree.cpp | 223 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerUpdateShadowTree.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index c7ce8a0f0bb..674b812510c 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -201,6 +201,9 @@ class UIManager final : public ShadowTreeDelegate { void reportMount(SurfaceId surfaceId) const; + void updateShadowTree( + const std::unordered_map& tagToProps); + private: friend class UIManagerBinding; friend class Scheduler; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerUpdateShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerUpdateShadowTree.cpp new file mode 100644 index 00000000000..c031c2acc68 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerUpdateShadowTree.cpp @@ -0,0 +1,223 @@ +/* + * 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. + */ + +#include "UIManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +struct ShadowNodeUpdateInfo { + Tag tag; + int depth; + std::weak_ptr node; + // populate for node that needs props update + std::optional changedProps; + // populate for node that's also a ancestor node for updated nodes + std::vector updatedChildrenIndices{}; +}; + +void addAncestorsToUpdateList( + std::shared_ptr& shadowNode, + std::shared_ptr& rootShadowNode, + std::vector& shadowNodesToUpdate) { + // list of ancestors from root ShadowNode to current ShadowNode's parent + auto ancestors = shadowNode->getFamily().getAncestors(*rootShadowNode); + + std::vector> ancestorShadowNodesShared{}; + ancestorShadowNodesShared.resize(ancestors.size()); + std::shared_ptr currentShadowNode = rootShadowNode; + auto ancestorIndex = 0; + while (!currentShadowNode->getChildren().empty() && + currentShadowNode->getTag() != shadowNode->getTag()) { + ancestorShadowNodesShared[ancestorIndex] = currentShadowNode; + auto children = currentShadowNode->getChildren(); + auto childIndex = ancestors[ancestorIndex].second; + currentShadowNode = children[childIndex]; + ancestorIndex++; + } + + int ancestorDepth = static_cast(ancestors.size() - 1); + // iterate from current ShadowNode's parent to root ShadowNode + for (auto iter = ancestors.rbegin(); iter != ancestors.rend(); ++iter) { + auto& ancestorShadowNode = iter->first.get(); + auto ancestorTag = ancestorShadowNode.getTag(); + auto ancestorAddedToUpdateList = std::find_if( + shadowNodesToUpdate.begin(), + shadowNodesToUpdate.end(), + [ancestorTag](const auto& elem) { return elem.tag == ancestorTag; }); + if (ancestorAddedToUpdateList == shadowNodesToUpdate.end()) { + react_native_assert( + ancestorShadowNodesShared[ancestorDepth]->getTag() == + ancestorShadowNode.getTag()); + shadowNodesToUpdate.push_back({ + .tag = ancestorShadowNode.getTag(), + .depth = ancestorDepth, + .node = ancestorShadowNodesShared[ancestorDepth], + .updatedChildrenIndices = {iter->second}, + }); + } else { + ancestorAddedToUpdateList->updatedChildrenIndices.push_back(iter->second); + } + ancestorDepth--; + } +} + +} // namespace + +/* Commit a map of ShadowNode props to ShadowTree, with guarantee that each + * ShadowNode is cloned only once in the commit. + * The tree cloning algorithm is inspired by `cloneShadowTreeWithNewProps` in + * https://github.com/software-mansion/react-native-reanimated/ (under MIT + * license). + */ +void UIManager::updateShadowTree( + const std::unordered_map& tagToProps) { + const auto& contextContainer = *contextContainer_; + + std::unordered_map remainingTagToProps = tagToProps; + getShadowTreeRegistry().enumerate([&](const ShadowTree& shadowTree, + bool& stop) { + if (remainingTagToProps.empty()) { + stop = true; + return; + } + + auto rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + auto surfaceId = rootShadowNode->getSurfaceId(); + std::vector shadowNodesToUpdate; + + // Step 1: Create a list of shadow nodes to update + // which includes all the ShadowNodes of tags in input map, plus all + // their ancestors + + std::deque> deque{rootShadowNode}; + + int depth = 0; + while (!deque.empty()) { + auto size = deque.size(); + for (auto i = 0; i < size; i++) { + auto shadowNode = std::move(deque.front()); + deque.pop_front(); + + if (auto nodesPropsIt = remainingTagToProps.find(shadowNode->getTag()); + nodesPropsIt != remainingTagToProps.end()) { + auto shadowNodeTag = shadowNode->getTag(); + auto shadowNodeAddedToUpdateList = std::find_if( + shadowNodesToUpdate.begin(), + shadowNodesToUpdate.end(), + [shadowNodeTag](const auto& elem) { + return elem.tag == shadowNodeTag; + }); + if (shadowNodeAddedToUpdateList == shadowNodesToUpdate.end()) { + shadowNodesToUpdate.push_back( + {.tag = shadowNodeTag, + .depth = depth, + .node = shadowNode, + .changedProps = nodesPropsIt->second}); + } else { + shadowNodeAddedToUpdateList->changedProps = nodesPropsIt->second; + } + remainingTagToProps.erase(nodesPropsIt->first); + + // Add all its ancestors to shadowNodesToUpdate list + addAncestorsToUpdateList( + shadowNode, rootShadowNode, shadowNodesToUpdate); + } + for (const auto& child : shadowNode->getChildren()) { + deque.push_back(child); + } + } + depth++; + } + + if (shadowNodesToUpdate.empty()) { + return; + } + + // Step 2: Clone nodes from children to ancestors + + std::sort( + shadowNodesToUpdate.begin(), + shadowNodesToUpdate.end(), + [](const auto& a, const auto& b) { return a.depth > b.depth; }); + + std::unordered_map> clonedShadowNodes; + + for (auto& nodeUpdateInfo : shadowNodesToUpdate) { + if (auto oldShadowNode = nodeUpdateInfo.node.lock()) { + Props::Shared newProps; + if (nodeUpdateInfo.changedProps) { + PropsParserContext propsParserContext{surfaceId, contextContainer}; + auto componentDescriptor = + componentDescriptorRegistry_ + ->findComponentDescriptorByHandle_DO_NOT_USE_THIS_IS_BROKEN( + oldShadowNode->getComponentHandle()); + newProps = componentDescriptor->cloneProps( + propsParserContext, + oldShadowNode->getProps(), + RawProps(nodeUpdateInfo.changedProps.value())); + } else { + newProps = oldShadowNode->getProps(); + } + + ShadowNodeFragment fragment; + auto children = oldShadowNode->getChildren(); + + // If children are previously updated (children should be cloned and + // updated before parents), add it to the children list of ShadowNode + for (auto& updatedChildIndex : nodeUpdateInfo.updatedChildrenIndices) { + if (updatedChildIndex < children.size()) { + auto childTag = children[updatedChildIndex]->getTag(); + auto clonedShadowNodesIt = clonedShadowNodes.find(childTag); + if (clonedShadowNodesIt != clonedShadowNodes.end()) { + children[updatedChildIndex] = clonedShadowNodesIt->second; + } else { + LOG(ERROR) << "Child ShadowNode has not been cloned"; + } + } else { + LOG(WARNING) << "Child no longer exits"; + } + } + + auto cloned = oldShadowNode->clone( + {.props = newProps, + .children = std::make_shared(children)}); + clonedShadowNodes.insert({oldShadowNode->getTag(), std::move(cloned)}); + } else { + LOG(ERROR) << "oldShadowNode is null"; + continue; + } + } + + // Step 3: Commit ShadowTree + + if (auto it = clonedShadowNodes.find(rootShadowNode->getTag()); + it != clonedShadowNodes.end()) { + shadowTree.commit( + [rootNode = + it->second](const RootShadowNode&) -> RootShadowNode::Unshared { + return std::static_pointer_cast(rootNode); + }, + {}); + } else { + LOG(ERROR) << "Root ShadowNode has not been cloned"; + } + }); +} + +} // namespace facebook::react