Skip to content

Commit

Permalink
Use commit hook to update props on Fabric (#4075)
Browse files Browse the repository at this point in the history
## 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
tomekzaw authored Jun 21, 2023
1 parent ef02b1f commit 7b43e32
Show file tree
Hide file tree
Showing 31 changed files with 527 additions and 539 deletions.
41 changes: 41 additions & 0 deletions Common/cpp/Fabric/PropsRegistry.cpp
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
60 changes: 60 additions & 0 deletions Common/cpp/Fabric/PropsRegistry.h
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
59 changes: 59 additions & 0 deletions Common/cpp/Fabric/ReanimatedCommitHook.cpp
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
43 changes: 43 additions & 0 deletions Common/cpp/Fabric/ReanimatedCommitHook.h
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
Loading

0 comments on commit 7b43e32

Please sign in to comment.