Skip to content

Commit c65845c

Browse files
j-piaseckimeta-codesync[bot]
authored andcommitted
Fix dropped React revision when merging commit branches (#57424)
Summary: Pull Request resolved: #57424 With commit branching (enableFabricCommitBranching), commits from React land on a separate revision (currentReactRevision_) that is later merged into the main tree. React-branch revisions are numbered off the main revision, so two React commits that occur before the main revision advances get the SAME revision number. mergeReactRevision decided whether to clear currentReactRevision_ by comparing revision *numbers*. When a newer React revision landed before the merge of the previous one completed, its coincidentally-equal number caused it to be cleared, silently dropping a pending update. It also cleared the revision even when the merge commit did not succeed. Fix: - Clear currentReactRevision_ by comparing root shadow node identity instead of the revision number, and only when the merge commit actually succeeded. - Compute the React-branch revision number from the snapshot taken under the shared lock instead of reading currentRevision_ without the (deferred) unique lock. Changelog: [General][Fixed] Fixed potential revision drop during merge Reviewed By: rubennorte Differential Revision: D110577969 fbshipit-source-id: b3f1f5aad8b3783e5080fde851571b62ceceaf94
1 parent 2361189 commit c65845c

2 files changed

Lines changed: 155 additions & 8 deletions

File tree

packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,9 @@ CommitStatus ShadowTree::tryCommit(
428428
return CommitStatus::Failed;
429429
}
430430

431-
auto newRevisionNumber = currentRevision_.number + 1;
431+
auto newRevisionNumber = isReactBranch
432+
? oldRevisionForStateProgression.number + 1
433+
: currentRevision_.number + 1;
432434

433435
{
434436
std::scoped_lock dispatchLock(EventEmitter::DispatchMutex());
@@ -513,11 +515,12 @@ void ShadowTree::mergeReactRevision() const {
513515
}
514516
}
515517

516-
ShadowTreeRevision::Number lastMergedRevisionNumber;
518+
RootShadowNode::Shared lastMergedRootShadowNode;
519+
bool lastMergeSucceeded = false;
517520

518521
if (isPropsUpdatesAccumulationGuaranteed()) {
519-
lastMergedRevisionNumber = promotedRevision.number;
520-
this->commit(
522+
lastMergedRootShadowNode = promotedRevision.rootShadowNode;
523+
auto status = this->commit(
521524
[revision = std::move(promotedRevision)](
522525
const RootShadowNode& /*oldRootShadowNode*/) {
523526
return std::make_shared<RootShadowNode>(
@@ -528,13 +531,14 @@ void ShadowTree::mergeReactRevision() const {
528531
.mountSynchronously = true,
529532
.source = CommitSource::ReactRevisionMerge,
530533
});
534+
lastMergeSucceeded = status == CommitStatus::Succeeded;
531535
} else {
532536
for (size_t i = 0; i < promotedRevisions.size(); ++i) {
533537
auto& revision = promotedRevisions[i];
534538
bool isLast = i == promotedRevisions.size() - 1;
535-
lastMergedRevisionNumber = revision.number;
539+
lastMergedRootShadowNode = revision.rootShadowNode;
536540

537-
this->commit(
541+
auto status = this->commit(
538542
[revision = std::move(revision)](
539543
const RootShadowNode& /*oldRootShadowNode*/) {
540544
return std::make_shared<RootShadowNode>(
@@ -545,6 +549,7 @@ void ShadowTree::mergeReactRevision() const {
545549
.mountSynchronously = true,
546550
.source = CommitSource::ReactRevisionMerge,
547551
});
552+
lastMergeSucceeded = status == CommitStatus::Succeeded;
548553
}
549554
}
550555

@@ -554,8 +559,9 @@ void ShadowTree::mergeReactRevision() const {
554559

555560
// If the current react revision is the same as the one that was just
556561
// merged, clear it.
557-
if (currentReactRevision_.has_value() &&
558-
lastMergedRevisionNumber == currentReactRevision_.value().number) {
562+
if (lastMergeSucceeded && currentReactRevision_.has_value() &&
563+
currentReactRevision_.value().rootShadowNode ==
564+
lastMergedRootShadowNode) {
559565
currentReactRevision_.reset();
560566
}
561567
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <memory>
9+
10+
#include <gtest/gtest.h>
11+
12+
#include <react/featureflags/ReactNativeFeatureFlags.h>
13+
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
14+
#include <react/renderer/components/view/ViewComponentDescriptor.h>
15+
#include <react/renderer/element/ComponentBuilder.h>
16+
#include <react/renderer/element/Element.h>
17+
#include <react/renderer/element/testUtils.h>
18+
#include <react/renderer/mounting/MountingCoordinator.h>
19+
#include <react/renderer/mounting/ShadowTree.h>
20+
#include <react/renderer/mounting/ShadowTreeDelegate.h>
21+
22+
using namespace facebook::react;
23+
24+
namespace {
25+
26+
class BranchingEnabledFlags : public ReactNativeFeatureFlagsDefaults {
27+
public:
28+
bool enableFabricCommitBranching() override {
29+
return true;
30+
}
31+
};
32+
33+
class DummyShadowTreeDelegate : public ShadowTreeDelegate {
34+
public:
35+
RootShadowNode::Unshared shadowTreeWillCommit(
36+
const ShadowTree& /*shadowTree*/,
37+
const RootShadowNode::Shared& /*oldRootShadowNode*/,
38+
const RootShadowNode::Unshared& newRootShadowNode,
39+
const ShadowTree::CommitOptions& /*commitOptions*/) const override {
40+
return newRootShadowNode;
41+
}
42+
43+
void shadowTreeDidFinishTransaction(
44+
std::shared_ptr<const MountingCoordinator> /*mountingCoordinator*/,
45+
bool /*mountSynchronously*/) const override {}
46+
47+
void shadowTreeDidFinishReactCommit(
48+
const ShadowTree& /*shadowTree*/) const override {}
49+
50+
void shadowTreeDidPromoteReactRevision(
51+
const ShadowTree& /*shadowTree*/) const override {}
52+
};
53+
54+
} // namespace
55+
56+
class ShadowTreeReactBranchingTest : public ::testing::Test {
57+
protected:
58+
void SetUp() override {
59+
// Must override before any component construction reads a flag.
60+
ReactNativeFeatureFlags::dangerouslyReset();
61+
ReactNativeFeatureFlags::override(
62+
std::make_unique<BranchingEnabledFlags>());
63+
}
64+
65+
void TearDown() override {
66+
ReactNativeFeatureFlags::dangerouslyReset();
67+
}
68+
};
69+
70+
// Two React revisions committed before a merge share a revision number.
71+
// Merging the first must not clear the second: the old number-based comparison
72+
// cleared it (dropping a pending update); the fix compares root node identity.
73+
TEST_F(ShadowTreeReactBranchingTest, mergeDoesNotDropNewerReactRevision) {
74+
// clang-format off
75+
auto element =
76+
Element<RootShadowNode>()
77+
.children({
78+
Element<ViewShadowNode>()
79+
});
80+
// clang-format on
81+
82+
ContextContainer contextContainer{};
83+
auto builder = simpleComponentBuilder();
84+
auto initialRootShadowNode = builder.build(element);
85+
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
86+
87+
ShadowTree shadowTree{
88+
SurfaceId{11},
89+
LayoutConstraints{},
90+
LayoutContext{},
91+
shadowTreeDelegate,
92+
contextContainer};
93+
94+
// Initial (non-React) commit establishes `currentRevision_`.
95+
shadowTree.commit(
96+
[&](const RootShadowNode& /*oldRootShadowNode*/) {
97+
return std::static_pointer_cast<RootShadowNode>(initialRootShadowNode);
98+
},
99+
{.enableStateReconciliation = false});
100+
101+
// First React revision (R1).
102+
auto rootR1 = std::static_pointer_cast<RootShadowNode>(
103+
initialRootShadowNode->ShadowNode::clone({}));
104+
shadowTree.commit(
105+
[&](const RootShadowNode& /*oldRootShadowNode*/) { return rootR1; },
106+
{.enableStateReconciliation = false,
107+
.mountSynchronously = false,
108+
.source = ShadowTreeCommitSource::React});
109+
110+
auto revisionAfterR1 = shadowTree.getCurrentReactRevision();
111+
ASSERT_TRUE(revisionAfterR1.has_value());
112+
113+
// Promote R1 (end-of-tick), but do not merge it yet.
114+
shadowTree.promoteReactRevision();
115+
116+
// Second React revision (R2) lands before the merge. The main revision has
117+
// not advanced, so R2 gets R1's revision number but is a distinct tree.
118+
auto rootR2 = std::static_pointer_cast<RootShadowNode>(
119+
initialRootShadowNode->ShadowNode::clone({}));
120+
shadowTree.commit(
121+
[&](const RootShadowNode& /*oldRootShadowNode*/) { return rootR2; },
122+
{.enableStateReconciliation = false,
123+
.mountSynchronously = false,
124+
.source = ShadowTreeCommitSource::React});
125+
126+
auto revisionAfterR2 = shadowTree.getCurrentReactRevision();
127+
ASSERT_TRUE(revisionAfterR2.has_value());
128+
129+
// Precondition for the bug: same number, different revisions.
130+
EXPECT_EQ(revisionAfterR1->number, revisionAfterR2->number);
131+
EXPECT_NE(revisionAfterR2->rootShadowNode, revisionAfterR1->rootShadowNode);
132+
133+
// Merging R1 must not clear R2.
134+
shadowTree.mergeReactRevision();
135+
136+
auto revisionAfterMerge = shadowTree.getCurrentReactRevision();
137+
ASSERT_TRUE(revisionAfterMerge.has_value())
138+
<< "Newer React revision (R2) was incorrectly dropped while merging R1";
139+
EXPECT_EQ(
140+
revisionAfterMerge->rootShadowNode, revisionAfterR2->rootShadowNode);
141+
}

0 commit comments

Comments
 (0)