-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use commit hook to update props on Fabric (#4075)
## Summary This draft PR introduces a new approach towards updating layout props on Fabric that uses `UIManagerCommitHook`. Previously, we would keep a registry of most recent ShadowNodes. Each time RN or Reanimated cloned a ShadowNode, we would retrieve the most recent version of it from the registry as well as store the new copy. Obviously, this required synchronization at the level of each `cloneNode` call so rendering large trees was slow. The new approach stores all most recent props of animated components in `PropsRegistry` and applies them on each RN render using `ReanimatedCommitHook` all at once which improves the performance significantly. Changes: * Removed `ReanimatedUIManagerBinding` * Added `ReanimatedCommitHook` * Converted `NewestShadowNodesRegistry` into `PropsRegistry` * Converted `_removeShadowNodeFromRegistry` into `_removeFromPropsRegistry` * Updated initialization flow Requires: * facebook/react-native#36216 (merged, thanks @sammy-SC, released in 0.72.0-rc.0) Things to consider: * Commit an identical tree and let `ReanimatedCommitHook` handle all updates instead of calling `isThereAnyLayoutProp`, enqueuing update in `operationsInBatch_` and updating `lastReanimatedRoot` but modifying only necessary ShadowNodes? * Copy `propsRegistry.map_` in `ReanimatedCommitHook` to prevent locking the UI thread while the background thread runs the commit hook? Detected problems: * If calculating layout of RN tree takes more than one frame, Reanimated will skip the commit before RN can commit its own tree and the problem still persists * If calculating layout of RN tree takes more than one frame, Reanimated will run the animations in the meantime and finally RN will mount outdated tree * When there's just a few layout props changes and many non-layout props changes (e.g. emoji shower and progress bar), we still apply all changes via commit (this is to enforce UI consistency). * On each RN render (even if only one component is changed) we need to apply all changes from the whole PropsRegistry (which contains all currently mounted animated components). ## Review Since this PR affects 31 files, it is recommended to review the commits one-by-one. ## Test plan Launch FabricExample and open the following examples: chessboard, width, ref
- Loading branch information
Showing
31 changed files
with
527 additions
and
539 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#ifdef RCT_NEW_ARCH_ENABLED | ||
|
||
#include "PropsRegistry.h" | ||
|
||
namespace reanimated { | ||
|
||
std::lock_guard<std::mutex> PropsRegistry::createLock() const { | ||
return std::lock_guard<std::mutex>(mutex_); | ||
} | ||
|
||
void PropsRegistry::update( | ||
const ShadowNode::Shared &shadowNode, | ||
folly::dynamic &&props) { | ||
const auto tag = shadowNode->getTag(); | ||
const auto it = map_.find(tag); | ||
if (it == map_.cend()) { | ||
// we need to store ShadowNode because `ShadowNode::getFamily` | ||
// returns `ShadowNodeFamily const &` which is non-owning | ||
map_[tag] = std::make_pair(shadowNode, props); | ||
} else { | ||
// no need to update `.first` because ShadowNodeFamily doesn't change | ||
// merge new props with old props | ||
it->second.second.update(props); | ||
} | ||
} | ||
|
||
void PropsRegistry::for_each(std::function<void( | ||
const ShadowNodeFamily &family, | ||
const folly::dynamic &props)> callback) const { | ||
for (const auto &[_, value] : map_) { | ||
callback(value.first->getFamily(), value.second); | ||
} | ||
} | ||
|
||
void PropsRegistry::remove(const Tag tag) { | ||
map_.erase(tag); | ||
} | ||
|
||
} // namespace reanimated | ||
|
||
#endif // RCT_NEW_ARCH_ENABLED |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
#pragma once | ||
#ifdef RCT_NEW_ARCH_ENABLED | ||
|
||
#include <react/renderer/components/root/RootShadowNode.h> | ||
#include <react/renderer/core/ShadowNode.h> | ||
|
||
#include <unordered_map> | ||
#include <utility> | ||
|
||
using namespace facebook; | ||
using namespace react; | ||
|
||
namespace reanimated { | ||
|
||
class PropsRegistry { | ||
public: | ||
std::lock_guard<std::mutex> createLock() const; | ||
// returns a lock you need to hold when calling any of the methods below | ||
|
||
void update(const ShadowNode::Shared &shadowNode, folly::dynamic &&props); | ||
|
||
void for_each(std::function<void( | ||
const ShadowNodeFamily &family, | ||
const folly::dynamic &props)> callback) const; | ||
|
||
void remove(const Tag tag); | ||
|
||
void setLastReanimatedRoot( | ||
RootShadowNode::Shared const &lastReanimatedRoot) noexcept { | ||
// TODO: synchronize with mutex? | ||
lastReanimatedRoot_ = lastReanimatedRoot; | ||
} | ||
|
||
bool isLastReanimatedRoot( | ||
RootShadowNode::Shared const &newRootShadowNode) const noexcept { | ||
// TODO: synchronize with mutex? | ||
return newRootShadowNode == lastReanimatedRoot_; | ||
} | ||
|
||
void pleaseSkipCommit() { | ||
letMeIn_ = true; | ||
} | ||
|
||
bool shouldSkipCommit() { | ||
return letMeIn_.exchange(false); | ||
} | ||
|
||
private: | ||
std::unordered_map<Tag, std::pair<ShadowNode::Shared, folly::dynamic>> map_; | ||
|
||
mutable std::mutex mutex_; // Protects `map_`. | ||
|
||
RootShadowNode::Shared lastReanimatedRoot_; | ||
|
||
std::atomic<bool> letMeIn_; | ||
}; | ||
|
||
} // namespace reanimated | ||
|
||
#endif // RCT_NEW_ARCH_ENABLED |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
#ifdef RCT_NEW_ARCH_ENABLED | ||
|
||
#include <react/renderer/core/ComponentDescriptor.h> | ||
|
||
#include "ReanimatedCommitHook.h" | ||
#include "ShadowTreeCloner.h" | ||
|
||
using namespace facebook::react; | ||
|
||
namespace reanimated { | ||
|
||
RootShadowNode::Unshared ReanimatedCommitHook::shadowTreeWillCommit( | ||
ShadowTree const &shadowTree, | ||
RootShadowNode::Shared const &oldRootShadowNode, | ||
RootShadowNode::Unshared const &newRootShadowNode) const noexcept { | ||
if (propsRegistry_->isLastReanimatedRoot(newRootShadowNode)) { | ||
// ShadowTree commited by Reanimated, no need to apply updates from | ||
// PropsRegistry | ||
return newRootShadowNode; | ||
} | ||
|
||
// ShadowTree not commited by Reanimated, apply updates from PropsRegistry | ||
|
||
auto surfaceId = newRootShadowNode->getSurfaceId(); | ||
|
||
auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{}); | ||
|
||
ShadowTreeCloner shadowTreeCloner{uiManager_, surfaceId}; | ||
|
||
{ | ||
auto lock = propsRegistry_->createLock(); | ||
|
||
propsRegistry_->for_each([&](const ShadowNodeFamily &family, | ||
const folly::dynamic &props) { | ||
auto newRootNode = | ||
shadowTreeCloner.cloneWithNewProps(rootNode, family, RawProps(props)); | ||
|
||
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 | ||
return; | ||
} | ||
rootNode = newRootNode; | ||
}); | ||
} | ||
|
||
shadowTreeCloner.updateYogaChildren(); | ||
|
||
// request Reanimated to skip one commit so that React Native can mount the | ||
// changes instead of failing 1024 times and crashing the app | ||
propsRegistry_->pleaseSkipCommit(); | ||
|
||
return std::static_pointer_cast<RootShadowNode>(rootNode); | ||
} | ||
|
||
} // namespace reanimated | ||
|
||
#endif // RCT_NEW_ARCH_ENABLED |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
#pragma once | ||
#ifdef RCT_NEW_ARCH_ENABLED | ||
|
||
#include <react/renderer/uimanager/UIManagerCommitHook.h> | ||
|
||
#include <memory> | ||
|
||
#include "PropsRegistry.h" | ||
|
||
using namespace facebook::react; | ||
|
||
namespace reanimated { | ||
|
||
class ReanimatedCommitHook : public UIManagerCommitHook { | ||
public: | ||
ReanimatedCommitHook( | ||
const std::shared_ptr<PropsRegistry> &propsRegistry, | ||
const std::shared_ptr<UIManager> &uiManager) | ||
: propsRegistry_(propsRegistry), uiManager_(uiManager) {} | ||
|
||
void commitHookWasRegistered( | ||
UIManager const &uiManager) const noexcept override {} | ||
|
||
void commitHookWasUnregistered( | ||
UIManager const &uiManager) const noexcept override {} | ||
|
||
RootShadowNode::Unshared shadowTreeWillCommit( | ||
ShadowTree const &shadowTree, | ||
RootShadowNode::Shared const &oldRootShadowNode, | ||
RootShadowNode::Unshared const &newRootShadowNode) | ||
const noexcept override; | ||
|
||
virtual ~ReanimatedCommitHook() noexcept = default; | ||
|
||
private: | ||
std::shared_ptr<PropsRegistry> propsRegistry_; | ||
|
||
std::shared_ptr<UIManager> uiManager_; | ||
}; | ||
|
||
} // namespace reanimated | ||
|
||
#endif // RCT_NEW_ARCH_ENABLED |
Oops, something went wrong.