From dcf9caed81990ad922d7a3fd896e7ab48a725571 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 16 Aug 2022 10:43:36 +0200 Subject: [PATCH] [Fabric] Optimize cloning ShadowTree when animating layout props (#3369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR improves the implementation of ShadowTree cloning algorithm in `NativeReanimatedModule::performOperations`. Previously, `ShadowNode::cloneTree` was called for each update, so effectively the root node was cloned $n$ times. In this PR I've proposed an algorithm for solving this task more optimally. The main idea is that unsealed ShadowNodes can be still updated. In particular, their children can be replaced using `ShadowNode::replaceChild`. Also, freshly cloned ShadowNodes are unsealed, so each ShadowNode can be cloned only once and then updated. One important thing to mention is that `replaceChild` does not update `yogaChild_` field in ShadowNode, so we need to invoke `ShadowNode::updateYogaChildren` manually. This function has $O(n)$ time complexity where $n$ denotes the number of children. In order to avoid calling `updateYogaChildren` multiple number of times for the same ShadowNode, the calls are batched in `std::set`. Since in most use-cases there is no more than ~50 props updates, I've decided to use ordered set implementation with $O(\log{n})$ time complexity of insertion in order to save memory as well as avoid the overhead of calculating hash function. | Before | After | |:-:|:-:| | Before | After |
Execution time measurements | nested view levels | chessboard size | old algorithm [μs] | new algorithm [μs] | speedup [x] | |:-:|:-:|:-:|:-:|:-:| | 0 | 10x10 | 4008 | 1326 | 3,0 | | 0 | 20x20 | 15962 | 3620 | 4,4 | | 0 | 30x30 | 50287 | 5939 | 8,5 | | 0 | 40x40 | 131410 | 10669 | 12,3 | | 10 | 10x10 | 105792 | 4058 | 26,1 | | 10 | 20x20 | 388404 | 5935 | 65,4 | | 10 | 30x30 | 879948 | 9334 | 94,3 | | 10 | 40x40 | 1598547 | 15477 | 103,3 | | 50 | 10x10 | 524744 | 11750 | 44,7 | | 50 | 20x20 | 2056805 | 17257 | 119,2 | | 50 | 30x30 | 4657397 | 24588 | 189,4 | | 50 | 40x40 | 8305191 | 35413 | 234,5 |
## Changes - Implemented `ShadowTreeCloner` for optimal cloning ShadowTree when animating layout props on Fabric - Used `ShadowTreeCloner` in `NativeReanimatedModule::performOperations` - Moved creation of `PropsParserContext` to `ShadowTreeCloner` ## Test code and steps to reproduce The following examples can be used to test the algorithm: - `WidthExample` - `RefExample` - `ChessboardExample` - `NewestShadowNodesRegistryRemoveExample` ## Checklist - [ ] Included code example that can be used to test this change - [ ] Updated TS types - [ ] Added TS types tests - [ ] Added unit / integration tests - [ ] Updated documentation - [ ] Ensured that CI passes --- Common/cpp/Fabric/ShadowTreeCloner.cpp | 92 +++++++++++++++++++ .../NativeModules/NativeReanimatedModule.cpp | 57 +++++------- Common/cpp/headers/Fabric/ShadowTreeCloner.h | 41 +++++++++ android/CMakeLists.txt | 7 ++ 4 files changed, 162 insertions(+), 35 deletions(-) create mode 100644 Common/cpp/Fabric/ShadowTreeCloner.cpp create mode 100644 Common/cpp/headers/Fabric/ShadowTreeCloner.h diff --git a/Common/cpp/Fabric/ShadowTreeCloner.cpp b/Common/cpp/Fabric/ShadowTreeCloner.cpp new file mode 100644 index 00000000000..c6fd600d26d --- /dev/null +++ b/Common/cpp/Fabric/ShadowTreeCloner.cpp @@ -0,0 +1,92 @@ +#ifdef RCT_NEW_ARCH_ENABLED + +#include "ShadowTreeCloner.h" +#include "FabricUtils.h" + +namespace reanimated { + +ShadowTreeCloner::ShadowTreeCloner( + std::shared_ptr newestShadowNodesRegistry, + std::shared_ptr uiManager, + SurfaceId surfaceId) + : newestShadowNodesRegistry_{newestShadowNodesRegistry}, + propsParserContext_{ + surfaceId, + *getContextContainerFromUIManager(&*uiManager)} {} + +ShadowTreeCloner::~ShadowTreeCloner() { +#ifdef DEBUG + react_native_assert( + yogaChildrenUpdates_.empty() && + "Deallocating `ShadowTreeCloner` without calling `updateYogaChildren`."); +#endif +} + +ShadowNode::Unshared ShadowTreeCloner::cloneWithNewProps( + const ShadowNode::Shared &oldRootNode, + const ShadowNodeFamily &family, + RawProps &&rawProps) { + // adapted from ShadowNode::cloneTree + + auto ancestors = family.getAncestors(*oldRootNode); + + if (ancestors.empty()) { + return ShadowNode::Unshared{nullptr}; + } + + auto &parent = ancestors.back(); + auto &oldShadowNode = parent.first.get().getChildren().at(parent.second); + + const auto newest = newestShadowNodesRegistry_->get(oldShadowNode->getTag()); + + const auto &source = newest == nullptr ? oldShadowNode : newest; + + const auto props = source->getComponentDescriptor().cloneProps( + propsParserContext_, source->getProps(), rawProps); + + auto newChildNode = source->clone({/* .props = */ props}); + + for (auto it = ancestors.rbegin(); it != ancestors.rend(); ++it) { + auto &parentNode = it->first.get(); + auto childIndex = it->second; + + auto children = parentNode.getChildren(); + const auto &oldChildNode = *children.at(childIndex); + react_native_assert(ShadowNode::sameFamily(oldChildNode, *newChildNode)); + + newestShadowNodesRegistry_->set(newChildNode, parentNode.getTag()); + + if (!parentNode.getSealed()) { + // Optimization: if a ShadowNode is unsealed, we can directly update its + // children instead of cloning the whole path to the root node. + auto &parentNodeNonConst = const_cast(parentNode); + parentNodeNonConst.replaceChild(oldChildNode, newChildNode, childIndex); + yogaChildrenUpdates_.insert(&parentNodeNonConst); + return std::const_pointer_cast(oldRootNode); + } + + children[childIndex] = newChildNode; + + newChildNode = parentNode.clone({ + ShadowNodeFragment::propsPlaceholder(), + std::make_shared(children), + }); + } + + return std::const_pointer_cast(newChildNode); +} + +void ShadowTreeCloner::updateYogaChildren() { + // Unfortunately, `replaceChild` does not update Yoga nodes, so we need to + // update them manually here. + for (ShadowNode *shadowNode : yogaChildrenUpdates_) { + static_cast(shadowNode)->updateYogaChildren(); + } +#ifdef DEBUG + yogaChildrenUpdates_.clear(); +#endif +} + +} // namespace reanimated + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/Common/cpp/NativeModules/NativeReanimatedModule.cpp b/Common/cpp/NativeModules/NativeReanimatedModule.cpp index 2d8a41d959e..3e1d8a1c397 100644 --- a/Common/cpp/NativeModules/NativeReanimatedModule.cpp +++ b/Common/cpp/NativeModules/NativeReanimatedModule.cpp @@ -13,6 +13,7 @@ #include "FabricUtils.h" #include "NewestShadowNodesRegistry.h" #include "ReanimatedUIManagerBinding.h" +#include "ShadowTreeCloner.h" #endif #include "EventHandlerRegistry.h" @@ -507,56 +508,42 @@ void NativeReanimatedModule::performOperations() { react_native_assert(uiManager_ != nullptr); const auto &shadowTreeRegistry = uiManager_->getShadowTreeRegistry(); - auto contextContainer = getContextContainerFromUIManager( - &*uiManager_); // TODO: use Scheduler::getContextContainer - PropsParserContext propsParserContext{surfaceId_, *contextContainer}; jsi::Runtime &rt = *runtime.get(); shadowTreeRegistry.visit(surfaceId_, [&](ShadowTree const &shadowTree) { shadowTree.commit([&](RootShadowNode const &oldRootShadowNode) { - // lock once due to performance reasons - auto lock = newestShadowNodesRegistry_->createLock(); - auto rootNode = oldRootShadowNode.ShadowNode::clone(ShadowNodeFragment{}); - for (const auto &pair : copiedOperationsQueue) { - const ShadowNodeFamily &family = pair.first->getFamily(); - react_native_assert(family.getSurfaceId() == surfaceId_); - - auto newRootNode = - rootNode->cloneTree(family, [&](ShadowNode const &oldShadowNode) { - const auto newest = - newestShadowNodesRegistry_->get(oldShadowNode.getTag()); + ShadowTreeCloner shadowTreeCloner{ + newestShadowNodesRegistry_, uiManager_, surfaceId_}; - const auto &source = newest == nullptr ? oldShadowNode : *newest; + { + // lock once due to performance reasons + auto lock = newestShadowNodesRegistry_->createLock(); - const auto newProps = source.getComponentDescriptor().cloneProps( - propsParserContext, - source.getProps(), - RawProps(rt, *pair.second)); + for (const auto &pair : copiedOperationsQueue) { + const ShadowNodeFamily &family = pair.first->getFamily(); + react_native_assert(family.getSurfaceId() == surfaceId_); - return source.clone({/* .props = */ newProps}); - }); + auto newRootNode = shadowTreeCloner.cloneWithNewProps( + rootNode, family, RawProps(rt, *pair.second)); - if (newRootNode == nullptr) { - // this happens when React removed the component but Reanimated still - // tries to animate it, let's skip update for this specific component - continue; + if (newRootNode == nullptr) { + // this happens when React removed the component but Reanimated + // still tries to animate it, let's skip update for this specific + // component + continue; + } + rootNode = newRootNode; } - rootNode = newRootNode; - auto ancestors = family.getAncestors(*rootNode); - for (const auto &pair : ancestors) { - const auto &parent = pair.first.get(); - const auto &child = parent.getChildren().at(pair.second); - newestShadowNodesRegistry_->set(child, parent.getTag()); + // remove ShadowNodes and its ancestors from NewestShadowNodesRegistry + for (auto tag : copiedTagsToRemove) { + newestShadowNodesRegistry_->remove(tag); } } - // remove ShadowNodes and its ancestors from NewestShadowNodesRegistry - for (auto tag : copiedTagsToRemove) { - newestShadowNodesRegistry_->remove(tag); - } + shadowTreeCloner.updateYogaChildren(); return std::static_pointer_cast(rootNode); }); diff --git a/Common/cpp/headers/Fabric/ShadowTreeCloner.h b/Common/cpp/headers/Fabric/ShadowTreeCloner.h new file mode 100644 index 00000000000..ae7a9871d05 --- /dev/null +++ b/Common/cpp/headers/Fabric/ShadowTreeCloner.h @@ -0,0 +1,41 @@ +#pragma once +#ifdef RCT_NEW_ARCH_ENABLED + +#include +#include + +#include +#include + +#include "NewestShadowNodesRegistry.h" + +using namespace facebook; +using namespace react; + +namespace reanimated { + +class ShadowTreeCloner { + public: + ShadowTreeCloner( + std::shared_ptr newestShadowNodesRegistry, + std::shared_ptr uiManager, + SurfaceId surfaceId); + + ~ShadowTreeCloner(); + + ShadowNode::Unshared cloneWithNewProps( + const ShadowNode::Shared &oldRootNode, + const ShadowNodeFamily &family, + RawProps &&rawProps); + + void updateYogaChildren(); + + private: + PropsParserContext propsParserContext_; + std::shared_ptr newestShadowNodesRegistry_; + std::set yogaChildrenUpdates_; +}; + +} // namespace reanimated + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 948b8a79dcd..835ed09db19 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -212,6 +212,12 @@ if(${IS_NEW_ARCHITECTURE_ENABLED}) PATHS ${LIBRN_DIR} NO_CMAKE_FIND_ROOT_PATH ) + find_library( + RRC_VIEW + rrc_view + PATHS ${LIBRN_DIR} + NO_CMAKE_FIND_ROOT_PATH + ) find_library( REACT_RENDER_SCHEDULER react_render_scheduler @@ -307,6 +313,7 @@ if(${IS_NEW_ARCHITECTURE_ENABLED}) ${REACT_DEBUG} ${REACT_RENDER_DEBUG} ${RRC_ROOT} + ${RRC_VIEW} ${FABRICJNI} ${REACT_RENDER_SCHEDULER} )