/* * 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 #include #include #include #include #include #include #include #include #include "updateMountedFlag.h" #include "ShadowTreeDelegate.h" namespace facebook::react { namespace { const int MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING = 3; } // namespace 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 std::shared_ptr progressState(const ShadowNode& shadowNode) { auto isStateChanged = false; auto areChildrenChanged = false; auto newState = shadowNode.getState(); if (newState) { newState = newState->getMostRecentStateIfObsolete(); if (newState) { isStateChanged = true; } } auto newChildren = std::vector>{}; if (!shadowNode.getChildren().empty()) { auto index = size_t{0}; for (const auto& 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< const std::vector>>( 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 std::shared_ptr progressState( const ShadowNode& shadowNode, const ShadowNode& 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 = std::vector>{}; 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[index]; const auto& 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< const std::vector>>( std::move(newChildren)) : ShadowNodeFragment::childrenPlaceholder(), isStateChanged ? newState : ShadowNodeFragment::statePlaceholder(), }); } ShadowTree::ShadowTree( SurfaceId surfaceId, const LayoutConstraints& layoutConstraints, const LayoutContext& layoutContext, const ShadowTreeDelegate& delegate, const ContextContainer& contextContainer) : surfaceId_(surfaceId), delegate_(delegate) { static RootComponentDescriptor globalRootComponentDescriptor( ComponentDescriptorParameters{ EventDispatcher::Shared{}, nullptr, nullptr}); const auto props = std::make_shared( PropsParserContext{surfaceId, contextContainer}, *RootShadowNode::defaultSharedProps(), layoutConstraints, layoutContext); auto family = globalRootComponentDescriptor.createFamily( {surfaceId, surfaceId, nullptr}); auto rootShadowNode = std::static_pointer_cast( globalRootComponentDescriptor.createShadowNode( ShadowNodeFragment{ /* .props = */ props, }, family)); currentRevision_ = ShadowTreeRevision{ rootShadowNode, INITIAL_REVISION, TransactionTelemetry{}}; mountingCoordinator_ = std::make_shared(currentRevision_); } ShadowTree::~ShadowTree() { mountingCoordinator_->revoke(); } Tag ShadowTree::getSurfaceId() const { return surfaceId_; } void ShadowTree::setCommitMode(CommitMode commitMode) const { auto revision = ShadowTreeRevision{}; { ShadowTree::UniqueLock lock = uniqueCommitLock(); 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 { SharedLock lock = sharedCommitLock(); return commitMode_; } std::shared_ptr ShadowTree::getMountingCoordinator() const { return mountingCoordinator_; } CommitStatus ShadowTree::commit( const ShadowTreeCommitTransaction& transaction, const CommitOptions& commitOptions) const { [[maybe_unused]] int attempts = 0; if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) { while (attempts < MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING) { auto status = tryCommit(transaction, commitOptions); if (status != CommitStatus::Failed) { return status; } attempts++; } { std::unique_lock lock(commitMutexRecursive_); return tryCommit(transaction, commitOptions); } } else { 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 { TraceSection s("ShadowTree::commit"); auto telemetry = TransactionTelemetry{}; telemetry.willCommit(); CommitMode commitMode; auto oldRevision = ShadowTreeRevision{}; auto newRevision = ShadowTreeRevision{}; { // Reading `currentRevision_` in shared manner. SharedLock lock = sharedCommitLock(); commitMode = commitMode_; oldRevision = currentRevision_; } const auto& oldRootShadowNode = oldRevision.rootShadowNode; auto newRootShadowNode = transaction(*oldRevision.rootShadowNode); if (!newRootShadowNode) { return CommitStatus::Cancelled; } if (commitOptions.enableStateReconciliation) { auto updatedNewRootShadowNode = progressState(*newRootShadowNode, *oldRootShadowNode); if (updatedNewRootShadowNode) { newRootShadowNode = std::static_pointer_cast(updatedNewRootShadowNode); } } // Run commit hooks. newRootShadowNode = delegate_.shadowTreeWillCommit( *this, oldRootShadowNode, newRootShadowNode, commitOptions); if (!newRootShadowNode) { return CommitStatus::Cancelled; } // Layout nodes. std::vector affectedLayoutableNodes{}; affectedLayoutableNodes.reserve(1024); telemetry.willLayout(); telemetry.setAsThreadLocal(); newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes); telemetry.unsetAsThreadLocal(); telemetry.didLayout(static_cast(affectedLayoutableNodes.size())); { // Updating `currentRevision_` in unique manner if it hasn't changed. UniqueLock lock = uniqueCommitLock(); if (currentRevision_.number != oldRevision.number) { return CommitStatus::Failed; } auto newRevisionNumber = currentRevision_.number + 1; { std::scoped_lock dispatchLock(EventEmitter::DispatchMutex()); updateMountedFlag( currentRevision_.rootShadowNode->getChildren(), newRootShadowNode->getChildren(), commitOptions.source); } telemetry.didCommit(); telemetry.setRevisionNumber(static_cast(newRevisionNumber)); // Seal the shadow node so it can no longer be mutated // Does nothing in release. newRootShadowNode->sealRecursive(); 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 { SharedLock lock = sharedCommitLock(); return currentRevision_; } void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously) const { mountingCoordinator_->push(std::move(revision)); delegate_.shadowTreeDidFinishTransaction( mountingCoordinator_, mountSynchronously); } void ShadowTree::commitEmptyTree() const { commit( [](const RootShadowNode& oldRootShadowNode) -> std::shared_ptr { return std::make_shared( oldRootShadowNode, ShadowNodeFragment{ /* .props = */ ShadowNodeFragment::propsPlaceholder(), /* .children = */ ShadowNode::emptySharedShadowNodeSharedList(), }); }, {/* default commit options */}); } void ShadowTree::emitLayoutEvents( std::vector& affectedLayoutableNodes) const { TraceSection s( "ShadowTree::emitLayoutEvents", "affectedLayoutableNodes", affectedLayoutableNodes.size()); for (const auto* layoutableNode : affectedLayoutableNodes) { if (auto viewProps = dynamic_cast(layoutableNode->getProps().get())) { if (viewProps->onLayout) { static_cast( *layoutableNode->getEventEmitter()) .onLayout(layoutableNode->getLayoutMetrics()); } } } } void ShadowTree::notifyDelegatesOfUpdates() const { delegate_.shadowTreeDidFinishTransaction(mountingCoordinator_, true); } inline ShadowTree::UniqueLock ShadowTree::uniqueCommitLock() const { if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) { return std::unique_lock{commitMutexRecursive_}; } else { return std::unique_lock{commitMutex_}; } } inline ShadowTree::SharedLock ShadowTree::sharedCommitLock() const { if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) { return std::unique_lock{commitMutexRecursive_}; } else { return std::shared_lock{commitMutex_}; } } } // namespace facebook::react