472 lines
14 KiB
C++
472 lines
14 KiB
C++
/*
|
|
* 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 "ShadowTree.h"
|
|
|
|
#include <react/debug/react_native_assert.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/ShadowTreeRevision.h>
|
|
#include <react/renderer/mounting/ShadowViewMutation.h>
|
|
#include <react/renderer/telemetry/TransactionTelemetry.h>
|
|
|
|
#include "ShadowTreeDelegate.h"
|
|
|
|
namespace facebook::react {
|
|
|
|
using CommitStatus = ShadowTree::CommitStatus;
|
|
using CommitMode = ShadowTree::CommitMode;
|
|
|
|
/*
|
|
* 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++) {
|
|
auto const &childNode = *children[index];
|
|
auto const &baseChildNode = *baseChildren[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[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 ShadowNode::ListOfShared &oldChildren,
|
|
const ShadowNode::ListOfShared &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;
|
|
}
|
|
|
|
size_t 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());
|
|
}
|
|
|
|
size_t 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,
|
|
ShadowTreeDelegate const &delegate,
|
|
ContextContainer const &contextContainer)
|
|
: surfaceId_(surfaceId), delegate_(delegate) {
|
|
const auto noopEventEmitter = std::make_shared<const ViewEventEmitter>(
|
|
nullptr, -1, std::shared_ptr<const EventDispatcher>());
|
|
|
|
static auto globalRootComponentDescriptor =
|
|
std::make_unique<RootComponentDescriptor const>(
|
|
ComponentDescriptorParameters{
|
|
EventDispatcher::Shared{}, nullptr, nullptr});
|
|
|
|
const auto props = std::make_shared<const RootProps>(
|
|
PropsParserContext{surfaceId, contextContainer},
|
|
*RootShadowNode::defaultSharedProps(),
|
|
layoutConstraints,
|
|
layoutContext);
|
|
|
|
auto const fragment =
|
|
ShadowNodeFamilyFragment{surfaceId, surfaceId, noopEventEmitter};
|
|
auto family = globalRootComponentDescriptor->createFamily(fragment, nullptr);
|
|
|
|
auto rootShadowNode = std::static_pointer_cast<const RootShadowNode>(
|
|
globalRootComponentDescriptor->createShadowNode(
|
|
ShadowNodeFragment{
|
|
/* .props = */ props,
|
|
},
|
|
family));
|
|
|
|
currentRevision_ = ShadowTreeRevision{
|
|
rootShadowNode, INITIAL_REVISION, TransactionTelemetry{}};
|
|
|
|
mountingCoordinator_ =
|
|
std::make_shared<MountingCoordinator const>(currentRevision_);
|
|
}
|
|
|
|
ShadowTree::~ShadowTree() {
|
|
mountingCoordinator_->revoke();
|
|
}
|
|
|
|
Tag ShadowTree::getSurfaceId() const {
|
|
return surfaceId_;
|
|
}
|
|
|
|
void ShadowTree::setCommitMode(CommitMode commitMode) const {
|
|
auto revision = ShadowTreeRevision{};
|
|
|
|
{
|
|
std::unique_lock lock(commitMutex_);
|
|
if (commitMode_ == commitMode) {
|
|
return;
|
|
}
|
|
|
|
commitMode_ = commitMode;
|
|
revision = currentRevision_;
|
|
}
|
|
|
|
// initial revision never contains any commits so mounting it here is
|
|
// incorrect
|
|
if (commitMode == CommitMode::Normal && revision.number != INITIAL_REVISION) {
|
|
mount(revision, true);
|
|
}
|
|
}
|
|
|
|
CommitMode ShadowTree::getCommitMode() const {
|
|
std::shared_lock lock(commitMutex_);
|
|
return commitMode_;
|
|
}
|
|
|
|
MountingCoordinator::Shared ShadowTree::getMountingCoordinator() const {
|
|
return mountingCoordinator_;
|
|
}
|
|
|
|
CommitStatus ShadowTree::commit(
|
|
const ShadowTreeCommitTransaction &transaction,
|
|
const CommitOptions &commitOptions) const {
|
|
SystraceSection s("ShadowTree::commit");
|
|
|
|
int attempts = 0;
|
|
|
|
while (true) {
|
|
attempts++;
|
|
|
|
auto status = tryCommit(transaction, commitOptions);
|
|
if (status != CommitStatus::Failed) {
|
|
return status;
|
|
}
|
|
|
|
// After multiple attempts, we failed to commit the transaction.
|
|
// Something internally went terribly wrong.
|
|
react_native_assert(attempts < 1024);
|
|
}
|
|
}
|
|
|
|
CommitStatus ShadowTree::tryCommit(
|
|
const ShadowTreeCommitTransaction &transaction,
|
|
const CommitOptions &commitOptions) const {
|
|
SystraceSection s("ShadowTree::tryCommit");
|
|
|
|
auto telemetry = TransactionTelemetry{};
|
|
telemetry.willCommit();
|
|
|
|
CommitMode commitMode;
|
|
auto oldRevision = ShadowTreeRevision{};
|
|
auto newRevision = ShadowTreeRevision{};
|
|
|
|
{
|
|
// Reading `currentRevision_` in shared manner.
|
|
std::shared_lock lock(commitMutex_);
|
|
commitMode = commitMode_;
|
|
oldRevision = currentRevision_;
|
|
}
|
|
|
|
auto const &oldRootShadowNode = oldRevision.rootShadowNode;
|
|
auto newRootShadowNode = transaction(*oldRevision.rootShadowNode);
|
|
|
|
if (!newRootShadowNode ||
|
|
(commitOptions.shouldYield && commitOptions.shouldYield())) {
|
|
return CommitStatus::Cancelled;
|
|
}
|
|
|
|
if (commitOptions.enableStateReconciliation) {
|
|
auto updatedNewRootShadowNode =
|
|
progressState(*newRootShadowNode, *oldRootShadowNode);
|
|
if (updatedNewRootShadowNode) {
|
|
newRootShadowNode =
|
|
std::static_pointer_cast<RootShadowNode>(updatedNewRootShadowNode);
|
|
}
|
|
}
|
|
|
|
// Run commit hooks.
|
|
newRootShadowNode = delegate_.shadowTreeWillCommit(
|
|
*this, oldRootShadowNode, newRootShadowNode);
|
|
|
|
if (!newRootShadowNode ||
|
|
(commitOptions.shouldYield && commitOptions.shouldYield())) {
|
|
return CommitStatus::Cancelled;
|
|
}
|
|
|
|
// Layout nodes.
|
|
std::vector<LayoutableShadowNode const *> affectedLayoutableNodes{};
|
|
affectedLayoutableNodes.reserve(1024);
|
|
|
|
telemetry.willLayout();
|
|
telemetry.setAsThreadLocal();
|
|
newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
|
|
telemetry.unsetAsThreadLocal();
|
|
telemetry.didLayout();
|
|
|
|
// Seal the shadow node so it can no longer be mutated
|
|
newRootShadowNode->sealRecursive();
|
|
|
|
{
|
|
// Updating `currentRevision_` in unique manner if it hasn't changed.
|
|
std::unique_lock lock(commitMutex_);
|
|
|
|
if (commitOptions.shouldYield && commitOptions.shouldYield()) {
|
|
return CommitStatus::Cancelled;
|
|
}
|
|
|
|
if (currentRevision_.number != oldRevision.number) {
|
|
return CommitStatus::Failed;
|
|
}
|
|
|
|
auto newRevisionNumber = oldRevision.number + 1;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> dispatchLock(EventEmitter::DispatchMutex());
|
|
|
|
updateMountedFlag(
|
|
currentRevision_.rootShadowNode->getChildren(),
|
|
newRootShadowNode->getChildren());
|
|
}
|
|
|
|
telemetry.didCommit();
|
|
telemetry.setRevisionNumber(static_cast<int>(newRevisionNumber));
|
|
|
|
newRevision = ShadowTreeRevision{
|
|
std::move(newRootShadowNode), newRevisionNumber, telemetry};
|
|
|
|
currentRevision_ = newRevision;
|
|
}
|
|
|
|
emitLayoutEvents(affectedLayoutableNodes);
|
|
|
|
if (commitMode == CommitMode::Normal) {
|
|
mount(std::move(newRevision), commitOptions.mountSynchronously);
|
|
}
|
|
|
|
return CommitStatus::Succeeded;
|
|
}
|
|
|
|
ShadowTreeRevision ShadowTree::getCurrentRevision() const {
|
|
std::shared_lock lock(commitMutex_);
|
|
return currentRevision_;
|
|
}
|
|
|
|
void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously)
|
|
const {
|
|
mountingCoordinator_->push(std::move(revision));
|
|
delegate_.shadowTreeDidFinishTransaction(
|
|
mountingCoordinator_, mountSynchronously);
|
|
}
|
|
|
|
void ShadowTree::commitEmptyTree() const {
|
|
commit(
|
|
[](RootShadowNode const &oldRootShadowNode) -> RootShadowNode::Unshared {
|
|
return std::make_shared<RootShadowNode>(
|
|
oldRootShadowNode,
|
|
ShadowNodeFragment{
|
|
/* .props = */ ShadowNodeFragment::propsPlaceholder(),
|
|
/* .children = */ ShadowNode::emptySharedShadowNodeSharedList(),
|
|
});
|
|
},
|
|
{/* default commit options */});
|
|
}
|
|
|
|
void ShadowTree::emitLayoutEvents(
|
|
std::vector<LayoutableShadowNode const *> &affectedLayoutableNodes) const {
|
|
SystraceSection s(
|
|
"ShadowTree::emitLayoutEvents",
|
|
"affectedLayoutableNodes",
|
|
affectedLayoutableNodes.size());
|
|
|
|
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(mountingCoordinator_, true);
|
|
}
|
|
|
|
} // namespace facebook::react
|