Files
react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp
T
Joshua Gross 1e4d8d902d Core/Differ: detect and optimize reparenting
Summary:
# Summary

In previous diffs earlier in 2020, we made changes to detect and optimize reordering of views when the order of views changed underneath the same parent.

However, until now we have ignored reparenting and there's evidence of issues because of that. Because Fabric flattens views more aggressively, reparenting is also marginally more likely to happen.

This diff introduces a very general Reparenting detection. It will work with view flattening/unflattening, as well as tree grafting - subtrees moved to entirely different parts of the tree, not just a single
parent disappearing or reappearing because of flattening/unflattening.

There is also another consideration: previously, we were generating strictly too many Create+Delete operations that were redundant and could cause consistency issues, crashes, or bugs on platforms that do not handle that gracefully -
especially since the ordering of the Create+Delete is not guaranteed (a reparented view could be created "first" and then the differ could later issue a "delete" for the same view).

Intuition behind how it works: we know the cases where we can detect reparenting: it's when nodes are *not* matched up with another node from the other tree, and we're either trying to delete an entire subtree, or create an entire subtree. For perf reasons, we generate whatever set of operations comes first (say, we generate all the Delete and Remove instructions) and take note in the `ReparentingMetadata` data-structure that Delete and/or Remove have been performed for each tag (if ordering is different, we do the same for Create+Insert if those come first). Then if we later detect a corresponding subtree creation/deletion, we don't generate those mutations and we mark the previous mutations for deletion. This incurs some map lookup cost, but this is only wasteful for commits where a large tree is deleted and a large tree is created, without reparenting.

We may be able to improve perf further for certain edge-cases in the future.

# Why can't we solve this in JS?

Two things:

1. We certainly can avoid reparenting situations in JS, but it's trickier than before because of Fabric's view flattening logic - product engineers would have to think much harder about how to prevent reparenting in the general case.
2. In the case of specific views like BottomSheet that may crash if they're reparented, the solution is to make sure that the BottomSheet and the first child of the BottomSheet is never memoized, so that lifecycle functions and render are called more often; and that in every render, the BottomSheet manually clones its child, so that when the Views are recreated, the child of the BottomSheet has a tag and is an entirely different instance. This is certainly possible to do but feels like an onerous requirement for product teams, and it could be challenging to track down every specific BottomSheet that is memoized and/or hoist them higher in the view hierarchy so they're not reparented as often.

Reviewed By: shergin

Differential Revision: D23123575

fbshipit-source-id: 2fa7e1f026f87b6f0c60cad469a3ba85cdc234de
2020-08-15 19:20:33 -07:00

413 lines
12 KiB
C++

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "ShadowTree.h"
#include <react/renderer/components/root/RootComponentDescriptor.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include <react/renderer/core/LayoutContext.h>
#include <react/renderer/core/LayoutPrimitives.h>
#include <react/renderer/debug/SystraceSection.h>
#include <react/renderer/mounting/MountingTelemetry.h>
#include <react/renderer/mounting/ShadowTreeRevision.h>
#include <react/renderer/mounting/ShadowViewMutation.h>
#include "ShadowTreeDelegate.h"
#include "TreeStateReconciliation.h"
namespace facebook {
namespace react {
/*
* Generates (possibly) a new tree where all nodes with non-obsolete `State`
* objects. If all `State` objects in the tree are not obsolete for the moment
* of calling, the function returns `nullptr` (as an indication that no
* additional work is required).
*/
static ShadowNode::Unshared progressState(ShadowNode const &shadowNode) {
auto isStateChanged = false;
auto areChildrenChanged = false;
auto newState = shadowNode.getState();
if (newState) {
newState = newState->getMostRecentStateIfObsolete();
if (newState) {
isStateChanged = true;
}
}
auto newChildren = ShadowNode::ListOfShared{};
if (!shadowNode.getChildren().empty()) {
auto index = size_t{0};
for (auto const &childNode : shadowNode.getChildren()) {
auto newChildNode = progressState(*childNode);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = shadowNode.getChildren();
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
index++;
}
}
if (!areChildrenChanged && !isStateChanged) {
return nullptr;
}
return shadowNode.clone({
ShadowNodeFragment::propsPlaceholder(),
areChildrenChanged ? std::make_shared<ShadowNode::ListOfShared const>(
std::move(newChildren))
: ShadowNodeFragment::childrenPlaceholder(),
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
});
}
/*
* An optimized version of the previous function (and relies on it).
* The function uses a given base tree to exclude unchanged (equal) parts
* of the three from the traversing.
*/
static ShadowNode::Unshared progressState(
ShadowNode const &shadowNode,
ShadowNode const &baseShadowNode) {
// The intuition behind the complexity:
// - A very few nodes have associated state, therefore it's mostly reading and
// it only writes when state objects were found obsolete;
// - Most before-after trees are aligned, therefore most tree branches will be
// skipped;
// - If trees are significantly different, any other algorithm will have
// close to linear complexity.
auto isStateChanged = false;
auto areChildrenChanged = false;
auto newState = shadowNode.getState();
if (newState) {
newState = newState->getMostRecentStateIfObsolete();
if (newState) {
isStateChanged = true;
}
}
auto &children = shadowNode.getChildren();
auto &baseChildren = baseShadowNode.getChildren();
auto newChildren = ShadowNode::ListOfShared{};
auto childrenSize = children.size();
auto baseChildrenSize = baseChildren.size();
auto index = size_t{0};
// Stage 1: Aligned part.
for (index = 0; index < childrenSize && index < baseChildrenSize; index++) {
const auto &childNode = *children.at(index);
const auto &baseChildNode = *baseChildren.at(index);
if (&childNode == &baseChildNode) {
// Nodes are identical, skipping.
continue;
}
if (!ShadowNode::sameFamily(childNode, baseChildNode)) {
// Totally different nodes, updating is impossible.
break;
}
auto newChildNode = progressState(childNode, baseChildNode);
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = children;
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
}
// Stage 2: Misaligned part.
for (; index < childrenSize; index++) {
auto newChildNode = progressState(*children.at(index));
if (newChildNode) {
if (!areChildrenChanged) {
// Making a copy before the first mutation.
newChildren = children;
}
newChildren[index] = newChildNode;
areChildrenChanged = true;
}
}
if (!areChildrenChanged && !isStateChanged) {
return nullptr;
}
return shadowNode.clone({
ShadowNodeFragment::propsPlaceholder(),
areChildrenChanged ? std::make_shared<ShadowNode::ListOfShared const>(
std::move(newChildren))
: ShadowNodeFragment::childrenPlaceholder(),
isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(),
});
}
static void updateMountedFlag(
const SharedShadowNodeList &oldChildren,
const SharedShadowNodeList &newChildren) {
// This is a simplified version of Diffing algorithm that only updates
// `mounted` flag on `ShadowNode`s. The algorithm sets "mounted" flag before
// "unmounted" to allow `ShadowNode` detect a situation where the node was
// remounted.
if (&oldChildren == &newChildren) {
// Lists are identical, nothing to do.
return;
}
if (oldChildren.empty() && newChildren.empty()) {
// Both lists are empty, nothing to do.
return;
}
int index;
// Stage 1: Mount and unmount "updated" children.
for (index = 0; index < oldChildren.size() && index < newChildren.size();
index++) {
const auto &oldChild = oldChildren[index];
const auto &newChild = newChildren[index];
if (oldChild == newChild) {
// Nodes are identical, skipping the subtree.
continue;
}
if (!ShadowNode::sameFamily(*oldChild, *newChild)) {
// Totally different nodes, updating is impossible.
break;
}
newChild->setMounted(true);
oldChild->setMounted(false);
updateMountedFlag(oldChild->getChildren(), newChild->getChildren());
}
int lastIndexAfterFirstStage = index;
// State 2: Mount new children.
for (index = lastIndexAfterFirstStage; index < newChildren.size(); index++) {
const auto &newChild = newChildren[index];
newChild->setMounted(true);
updateMountedFlag({}, newChild->getChildren());
}
// State 3: Unmount old children.
for (index = lastIndexAfterFirstStage; index < oldChildren.size(); index++) {
const auto &oldChild = oldChildren[index];
oldChild->setMounted(false);
updateMountedFlag(oldChild->getChildren(), {});
}
}
ShadowTree::ShadowTree(
SurfaceId surfaceId,
LayoutConstraints const &layoutConstraints,
LayoutContext const &layoutContext,
RootComponentDescriptor const &rootComponentDescriptor,
ShadowTreeDelegate const &delegate,
std::weak_ptr<MountingOverrideDelegate const> mountingOverrideDelegate,
bool enableReparentingDetection)
: surfaceId_(surfaceId),
delegate_(delegate),
enableReparentingDetection_(enableReparentingDetection) {
const auto noopEventEmitter = std::make_shared<const ViewEventEmitter>(
nullptr, -1, std::shared_ptr<const EventDispatcher>());
const auto props = std::make_shared<const RootProps>(
*RootShadowNode::defaultSharedProps(), layoutConstraints, layoutContext);
auto family = rootComponentDescriptor.createFamily(
ShadowNodeFamilyFragment{surfaceId, surfaceId, noopEventEmitter},
nullptr);
rootShadowNode_ = std::static_pointer_cast<const RootShadowNode>(
rootComponentDescriptor.createShadowNode(
ShadowNodeFragment{
/* .props = */ props,
},
family));
mountingCoordinator_ = std::make_shared<MountingCoordinator const>(
ShadowTreeRevision{rootShadowNode_, 0, {}},
mountingOverrideDelegate,
enableReparentingDetection);
}
ShadowTree::~ShadowTree() {
mountingCoordinator_->revoke();
}
Tag ShadowTree::getSurfaceId() const {
return surfaceId_;
}
MountingCoordinator::Shared ShadowTree::getMountingCoordinator() const {
return mountingCoordinator_;
}
void ShadowTree::commit(
ShadowTreeCommitTransaction transaction,
bool enableStateReconciliation) const {
SystraceSection s("ShadowTree::commit");
int attempts = 0;
while (true) {
attempts++;
if (tryCommit(transaction, enableStateReconciliation)) {
return;
}
// After multiple attempts, we failed to commit the transaction.
// Something internally went terribly wrong.
assert(attempts < 1024);
}
}
bool ShadowTree::tryCommit(
ShadowTreeCommitTransaction transaction,
bool enableStateReconciliation) const {
SystraceSection s("ShadowTree::tryCommit");
auto telemetry = MountingTelemetry{};
telemetry.willCommit();
RootShadowNode::Shared oldRootShadowNode;
{
// Reading `rootShadowNode_` in shared manner.
std::shared_lock<better::shared_mutex> lock(commitMutex_);
oldRootShadowNode = rootShadowNode_;
}
RootShadowNode::Unshared newRootShadowNode = transaction(oldRootShadowNode);
if (!newRootShadowNode) {
return false;
}
if (enableStateReconciliation) {
if (enableNewStateReconciliation_) {
auto updatedNewRootShadowNode =
progressState(*newRootShadowNode, *oldRootShadowNode);
if (updatedNewRootShadowNode) {
newRootShadowNode =
std::static_pointer_cast<RootShadowNode>(updatedNewRootShadowNode);
}
} else {
// Compare state revisions of old and new root
// Children of the root node may be mutated in-place
UnsharedShadowNode reconciledNode =
reconcileStateWithTree(newRootShadowNode.get(), oldRootShadowNode);
if (reconciledNode != nullptr) {
newRootShadowNode = std::make_shared<RootShadowNode>(
*reconciledNode, ShadowNodeFragment{});
}
}
}
// Layout nodes
std::vector<LayoutableShadowNode const *> affectedLayoutableNodes{};
affectedLayoutableNodes.reserve(1024);
telemetry.willLayout();
newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
telemetry.didLayout();
// Seal the shadow node so it can no longer be mutated
newRootShadowNode->sealRecursive();
auto revisionNumber = ShadowTreeRevision::Number{};
{
// Updating `rootShadowNode_` in unique manner if it hasn't changed.
std::unique_lock<better::shared_mutex> lock(commitMutex_);
if (rootShadowNode_ != oldRootShadowNode) {
return false;
}
rootShadowNode_ = newRootShadowNode;
{
std::lock_guard<std::mutex> dispatchLock(EventEmitter::DispatchMutex());
updateMountedFlag(
oldRootShadowNode->getChildren(), newRootShadowNode->getChildren());
}
revisionNumber_++;
revisionNumber = revisionNumber_;
}
emitLayoutEvents(affectedLayoutableNodes);
telemetry.didCommit();
mountingCoordinator_->push(
ShadowTreeRevision{newRootShadowNode, revisionNumber, telemetry});
notifyDelegatesOfUpdates();
return true;
}
void ShadowTree::commitEmptyTree() const {
commit(
[](RootShadowNode::Shared const &oldRootShadowNode)
-> RootShadowNode::Unshared {
return std::make_shared<RootShadowNode>(
*oldRootShadowNode,
ShadowNodeFragment{
/* .props = */ ShadowNodeFragment::propsPlaceholder(),
/* .children = */ ShadowNode::emptySharedShadowNodeSharedList(),
});
});
}
void ShadowTree::emitLayoutEvents(
std::vector<LayoutableShadowNode const *> &affectedLayoutableNodes) const {
SystraceSection s("ShadowTree::emitLayoutEvents");
for (auto const *layoutableNode : affectedLayoutableNodes) {
// Only instances of `ViewShadowNode` (and subclasses) are supported.
auto const &viewShadowNode =
static_cast<ViewShadowNode const &>(*layoutableNode);
auto const &viewEventEmitter = static_cast<ViewEventEmitter const &>(
*viewShadowNode.getEventEmitter());
// Checking if the `onLayout` event was requested for the particular Shadow
// Node.
auto const &viewProps =
static_cast<ViewProps const &>(*viewShadowNode.getProps());
if (!viewProps.onLayout) {
continue;
}
viewEventEmitter.onLayout(layoutableNode->getLayoutMetrics());
}
}
void ShadowTree::notifyDelegatesOfUpdates() const {
delegate_.shadowTreeDidFinishTransaction(*this, mountingCoordinator_);
}
} // namespace react
} // namespace facebook