diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp index 737c20ced71b98..91d97e5f72f2ae 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp @@ -282,6 +282,18 @@ void ShadowNode::setMounted(bool mounted) const { family_->eventEmitter_->setEnabled(mounted); } +void ShadowNode::progressStateIfNecessary() { + if (!hasBeenMounted_ && state_) { + ensureUnsealed(); + auto mostRecentState = family_->getMostRecentStateIfObsolete(*state_); + if (mostRecentState) { + state_ = mostRecentState; + const auto& componentDescriptor = family_->componentDescriptor_; + componentDescriptor.adopt(*this); + } + } +} + const ShadowNodeFamily& ShadowNode::getFamily() const { return *family_; } diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h index 8203dbc8ba447c..0d6c64fbb1209c 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h @@ -177,6 +177,15 @@ class ShadowNode : public Sealable, */ void setMounted(bool mounted) const; + /* + * Applies the most recent state to the ShadowNode if following conditions are + * met: + * - ShadowNode has a state. + * - ShadowNode has not been mounted before. + * - ShadowNode's current state is obsolete. + */ + void progressStateIfNecessary(); + #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index 4f189ff675f83a..563f3c38a96efb 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -25,6 +25,60 @@ namespace facebook::react { using CommitStatus = ShadowTree::CommitStatus; using CommitMode = ShadowTree::CommitMode; +// --- Clone-less progress state algorithm --- +// Note: Ideally, we don't have to const_cast but our use of constness in +// C++ is overly restrictive. We do const_cast here but the only place where +// we change ShadowNode is by calling `ShadowNode::progressStateIfNecessary` +// where checks are in place to avoid manipulating a sealed ShadowNode. + +static void progressStateIfNecessary(ShadowNode& newShadowNode) { + newShadowNode.progressStateIfNecessary(); + + for (const auto& childNode : newShadowNode.getChildren()) { + progressStateIfNecessary(const_cast(*childNode)); + } +} + +static void progressStateIfNecessary( + ShadowNode& newShadowNode, + const ShadowNode& baseShadowNode) { + newShadowNode.progressStateIfNecessary(); + + auto& newChildren = newShadowNode.getChildren(); + auto& baseChildren = baseShadowNode.getChildren(); + + auto newChildrenSize = newChildren.size(); + auto baseChildrenSize = baseChildren.size(); + auto index = size_t{0}; + + for (index = 0; index < newChildrenSize && index < baseChildrenSize; + ++index) { + const auto& newChildNode = *newChildren[index]; + const auto& baseChildNode = *baseChildren[index]; + + if (&newChildNode == &baseChildNode) { + // Nodes are identical. They are shared between `newShadowNode` and + // `baseShadowNode` and it is safe to skipping. + continue; + } + + if (!ShadowNode::sameFamily(newChildNode, baseChildNode)) { + // The nodes are not of the same family. Tree hierarchy has changed + // and we have to fall back to full sub-tree traversal from this point on. + break; + } + + progressStateIfNecessary( + const_cast(newChildNode), baseChildNode); + } + + for (; index < newChildrenSize; ++index) { + const auto& newChildNode = *newChildren[index]; + progressStateIfNecessary(const_cast(newChildNode)); + } +} +// --- End of Clone-less progress state algorithm --- + /* * 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 @@ -344,11 +398,15 @@ CommitStatus ShadowTree::tryCommit( } if (commitOptions.enableStateReconciliation) { - auto updatedNewRootShadowNode = - progressState(*newRootShadowNode, *oldRootShadowNode); - if (updatedNewRootShadowNode) { - newRootShadowNode = - std::static_pointer_cast(updatedNewRootShadowNode); + if (CoreFeatures::enableClonelessStateProgression) { + progressStateIfNecessary(*newRootShadowNode, *oldRootShadowNode); + } else { + auto updatedNewRootShadowNode = + progressState(*newRootShadowNode, *oldRootShadowNode); + if (updatedNewRootShadowNode) { + newRootShadowNode = + std::static_pointer_cast(updatedNewRootShadowNode); + } } } diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp index 0e1882d254adcd..c46b0c352f8a62 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp @@ -184,3 +184,117 @@ TEST(StateReconciliationTest, testStateReconciliation) { EXPECT_EQ(findDescendantNode(shadowTree, family)->getState(), state3); } + +TEST(StateReconciliationTest, testCloneslessStateReconciliationDoesntClone) { + CoreFeatures::enableClonelessStateProgression = true; + auto builder = simpleComponentBuilder(); + + auto shadowNodeA = std::shared_ptr{}; + auto shadowNodeAA = std::shared_ptr{}; + auto shadowNodeAB = std::shared_ptr{}; + + // clang-format off + auto element = + Element() + .reference(shadowNodeA) + .children({ + Element() + .reference(shadowNodeAA), + Element() + .reference(shadowNodeAB) + }); + // clang-format on + + ContextContainer contextContainer{}; + + auto initialRootShadowNode = builder.build(element); + + auto rootShadowNode1 = initialRootShadowNode->ShadowNode::clone({}); + + auto& scrollViewComponentDescriptor = shadowNodeAB->getComponentDescriptor(); + auto& family = shadowNodeAB->getFamily(); + auto state1 = shadowNodeAB->getState(); + auto shadowTreeDelegate = DummyShadowTreeDelegate{}; + ShadowTree shadowTree{ + SurfaceId{11}, + LayoutConstraints{}, + LayoutContext{}, + shadowTreeDelegate, + contextContainer}; + + shadowTree.commit( + [&](const RootShadowNode& /*oldRootShadowNode*/) { + return std::static_pointer_cast(rootShadowNode1); + }, + {true}); + + EXPECT_EQ(state1->getMostRecentState(), state1); + + EXPECT_EQ(findDescendantNode(*rootShadowNode1, family)->getState(), state1); + + auto state2 = scrollViewComponentDescriptor.createState( + family, std::make_shared()); + + auto rootShadowNode2 = + rootShadowNode1->cloneTree(family, [&](const ShadowNode& oldShadowNode) { + return oldShadowNode.clone( + {ShadowNodeFragment::propsPlaceholder(), + ShadowNodeFragment::childrenPlaceholder(), + state2}); + }); + + EXPECT_EQ(findDescendantNode(*rootShadowNode2, family)->getState(), state2); + EXPECT_EQ(state1->getMostRecentState(), state1); + + shadowTree.commit( + [&](const RootShadowNode& /*oldRootShadowNode*/) { + return std::static_pointer_cast(rootShadowNode2); + }, + {true}); + + EXPECT_EQ(state1->getMostRecentState(), state2); + EXPECT_EQ(state2->getMostRecentState(), state2); + + ShadowNode::Unshared newlyClonedShadowNode; + + auto rootShadowNodeClonedFromReact = + rootShadowNode2->cloneTree(family, [&](const ShadowNode& oldShadowNode) { + newlyClonedShadowNode = oldShadowNode.clone( + {ShadowNodeFragment::propsPlaceholder(), + ShadowNodeFragment::childrenPlaceholder(), + ShadowNodeFragment::statePlaceholder()}); + return newlyClonedShadowNode; + }); + + auto state3 = scrollViewComponentDescriptor.createState( + family, std::make_shared()); + + auto rootShadowNodeClonedFromStateUpdate = + rootShadowNode2->cloneTree(family, [&](const ShadowNode& oldShadowNode) { + return oldShadowNode.clone( + {ShadowNodeFragment::propsPlaceholder(), + ShadowNodeFragment::childrenPlaceholder(), + state3}); + }); + + shadowTree.commit( + [&](const RootShadowNode& /*oldRootShadowNode*/) { + return std::static_pointer_cast( + rootShadowNodeClonedFromStateUpdate); + }, + {}); + + shadowTree.commit( + [&](const RootShadowNode& /*oldRootShadowNode*/) { + return std::static_pointer_cast( + rootShadowNodeClonedFromReact); + }, + {true}); + + auto scrollViewShadowNode = findDescendantNode(shadowTree, family); + + EXPECT_EQ(scrollViewShadowNode->getState(), state3); + // Checking that newlyClonedShadowNode was not cloned unnecessarly by state + // progression. This fails with the old algorithm. + EXPECT_EQ(scrollViewShadowNode, newlyClonedShadowNode.get()); +} diff --git a/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp b/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp index 88def7c490d0d4..c428d1e665dab8 100644 --- a/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp +++ b/packages/react-native/ReactCommon/react/utils/CoreFeatures.cpp @@ -22,5 +22,6 @@ bool CoreFeatures::enableCleanParagraphYogaNode = false; bool CoreFeatures::disableScrollEventThrottleRequirement = false; bool CoreFeatures::enableGranularShadowTreeStateReconciliation = false; bool CoreFeatures::enableDefaultAsyncBatchedPriority = false; +bool CoreFeatures::enableClonelessStateProgression = false; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/utils/CoreFeatures.h b/packages/react-native/ReactCommon/react/utils/CoreFeatures.h index 8b3a53bc353527..919748cfd34be7 100644 --- a/packages/react-native/ReactCommon/react/utils/CoreFeatures.h +++ b/packages/react-native/ReactCommon/react/utils/CoreFeatures.h @@ -67,6 +67,9 @@ class CoreFeatures { // Default state updates and events to async batched priority. static bool enableDefaultAsyncBatchedPriority; + + // When enabled, Fabric will avoid cloning notes to perform state progression. + static bool enableClonelessStateProgression; }; } // namespace facebook::react