From 4edca1bdbda2b52a6216d34f8d6d905823acf764 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 21 May 2026 04:06:58 -0700 Subject: [PATCH] Merge all intermediate revisions when props accumulation is not guaranteed (#56914) Summary: Changelog: [ANDROID][FIXED] Fix commit branching dropping updates when `enableAccumulatedUpdatesInRawPropsAndroid` is not enabled. Without `enableAccumulatedUpdatesInRawPropsAndroid` each shadow tree revision on Android carries only the props applied in that specific revision. To handle this properly, all intermediate revisions need to pass through the mounting layer so the platform is aware of all the props. Differential Revision: D105835162 --- .../react/renderer/mounting/ShadowTree.cpp | 128 +++++++++++++----- .../react/renderer/mounting/ShadowTree.h | 2 + 2 files changed, 99 insertions(+), 31 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index b6fee6090738..93554c5b47c3 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -38,6 +38,14 @@ std::string getShadowTreeCommitSourceName(ShadowTreeCommitSource source) { return "ReactRevisionMerge"; } } + +inline bool isPropsUpdatesAccumulationGuaranteed() { +#ifdef __ANDROID__ + return ReactNativeFeatureFlags::enableAccumulatedUpdatesInRawPropsAndroid(); +#else + return true; +#endif +} } // namespace using CommitStatus = ShadowTree::CommitStatus; @@ -482,31 +490,63 @@ void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously) void ShadowTree::mergeReactRevision() const { ShadowTreeRevision promotedRevision; + std::vector promotedRevisions; + // If props updates accumulation is guaranteed, we can merge the promoted + // react revision, otherwise we need to merge all queued revisions. { UniqueLock lock = uniqueRevisionLock(false); - if (!reactRevisionToBePromoted_.has_value()) { - return; - } + if (isPropsUpdatesAccumulationGuaranteed()) { + if (!reactRevisionToBePromoted_.has_value()) { + return; + } - promotedRevision = - std::exchange(reactRevisionToBePromoted_, std::nullopt).value(); - } + promotedRevision = + std::exchange(reactRevisionToBePromoted_, std::nullopt).value(); + } else { + if (promotedReactRevisions_.empty()) { + return; + } - const auto mergedRevisionNumber = promotedRevision.number; + promotedReactRevisions_.swap(promotedRevisions); + } + } - this->commit( - [revision = std::move(promotedRevision)]( - const RootShadowNode& /*oldRootShadowNode*/) { - return std::make_shared( - *revision.rootShadowNode, ShadowNodeFragment{}); - }, - { - .enableStateReconciliation = true, - .mountSynchronously = true, - .source = CommitSource::ReactRevisionMerge, - }); + ShadowTreeRevision::Number lastMergedRevisionNumber; + + if (isPropsUpdatesAccumulationGuaranteed()) { + lastMergedRevisionNumber = promotedRevision.number; + this->commit( + [revision = std::move(promotedRevision)]( + const RootShadowNode& /*oldRootShadowNode*/) { + return std::make_shared( + *revision.rootShadowNode, ShadowNodeFragment{}); + }, + { + .enableStateReconciliation = true, + .mountSynchronously = true, + .source = CommitSource::ReactRevisionMerge, + }); + } else { + for (size_t i = 0; i < promotedRevisions.size(); ++i) { + auto& revision = promotedRevisions[i]; + bool isLast = i == promotedRevisions.size() - 1; + lastMergedRevisionNumber = revision.number; + + this->commit( + [revision = std::move(revision)]( + const RootShadowNode& /*oldRootShadowNode*/) { + return std::make_shared( + *revision.rootShadowNode, ShadowNodeFragment{}); + }, + { + .enableStateReconciliation = isLast, + .mountSynchronously = true, + .source = CommitSource::ReactRevisionMerge, + }); + } + } { UniqueLock commitLock = uniqueRevisionLock( @@ -515,34 +555,60 @@ void ShadowTree::mergeReactRevision() const { // If the current react revision is the same as the one that was just // merged, clear it. if (currentReactRevision_.has_value() && - mergedRevisionNumber == currentReactRevision_.value().number) { + lastMergedRevisionNumber == currentReactRevision_.value().number) { currentReactRevision_.reset(); } } } void ShadowTree::promoteReactRevision() const { - ShadowTreeRevision currentReactRevision; - { - SharedLock lock = sharedRevisionLock(); - // Promotion happens at the end of the event loop tick, it's possible to - // have more than one promotion in a row. In this case, all but the first - // one should no-op. - if (!currentReactRevision_.has_value()) { - return; + // Promote only when props updates accumulation is guaranteed. Otherwise, + // queuedReactRevisions_ will be used instead. + if (isPropsUpdatesAccumulationGuaranteed()) { + ShadowTreeRevision currentReactRevision; + { + SharedLock lock = sharedRevisionLock(); + // Promotion happens at the end of the event loop tick, it's possible to + // have more than one promotion in a row. In this case, all but the first + // one should no-op. + if (!currentReactRevision_.has_value()) { + return; + } + currentReactRevision = currentReactRevision_.value(); } - currentReactRevision = currentReactRevision_.value(); - } - { + { + UniqueLock lock = uniqueRevisionLock(false); + reactRevisionToBePromoted_ = std::move(currentReactRevision); + } + } else { UniqueLock lock = uniqueRevisionLock(false); - reactRevisionToBePromoted_ = std::move(currentReactRevision); + + if (queuedReactRevisions_.empty()) { + return; + } + + // Move all queued revisions to the promoted revisions. + promotedReactRevisions_.insert( + promotedReactRevisions_.end(), + std::make_move_iterator(queuedReactRevisions_.begin()), + std::make_move_iterator(queuedReactRevisions_.end())); + queuedReactRevisions_.clear(); } delegate_.shadowTreeDidPromoteReactRevision(*this); } void ShadowTree::scheduleReactRevisionPromotion() const { + if (!isPropsUpdatesAccumulationGuaranteed()) { + // If props updates accumulation is not guaranteed, we need to store each + // revision to be merged separately. + UniqueLock lock = uniqueRevisionLock(false); + if (currentReactRevision_.has_value()) { + queuedReactRevisions_.push_back(currentReactRevision_.value()); + } + } + delegate_.shadowTreeDidFinishReactCommit(*this); } diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h index b97680b8c25e..b875ccc80e21 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h @@ -173,6 +173,8 @@ class ShadowTree final { mutable ShadowTreeRevision currentRevision_; // Protected by `revisionMutex_`. mutable std::optional currentReactRevision_; // Protected by `revisionMutex_`. mutable std::optional reactRevisionToBePromoted_; // Protected by `revisionMutex_`. + mutable std::vector queuedReactRevisions_; // Protected by `revisionMutex_`. + mutable std::vector promotedReactRevisions_; // Protected by `revisionMutex_`. std::shared_ptr mountingCoordinator_; using UniqueLock = std::variant, std::unique_lock>;