Skip to content

Commit 27981ad

Browse files
JoshuaGrossfacebook-github-bot
authored andcommitted
Core: Add "state reconciliation" to commit phase, pre-layout
Summary: This implements proposal #2 in our State architecture doc: https://fb.quip.com/bm2EAVwL7jQ5 Problem description: see the text in the comment of TreeStateReconciliation.h Solution: see also comments in TreeStateReconciliation.h. Changelog: [internal] Reviewed By: mdvacca Differential Revision: D19617329 fbshipit-source-id: 845fb5fe27f2591be433b6d77799707b3516fb1a
1 parent 1f88b0d commit 27981ad

File tree

5 files changed

+178
-1
lines changed

5 files changed

+178
-1
lines changed

ReactCommon/fabric/core/shadownode/ShadowNode.cpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
#include "ShadowNode.h"
9+
#include "ShadowNodeFragment.h"
910

1011
#include <better/small_vector.h>
1112

@@ -29,6 +30,21 @@ bool ShadowNode::sameFamily(const ShadowNode &first, const ShadowNode &second) {
2930

3031
#pragma mark - Constructors
3132

33+
static int computeStateRevision(
34+
const State::Shared &state,
35+
const SharedShadowNodeSharedList &children) {
36+
int fragmentStateRevision = state ? state->getRevision() : 0;
37+
int childrenSum = 0;
38+
39+
if (children) {
40+
for (const auto &child : *children) {
41+
childrenSum += child->getStateRevision();
42+
}
43+
}
44+
45+
return fragmentStateRevision + childrenSum;
46+
}
47+
3248
ShadowNode::ShadowNode(
3349
ShadowNodeFragment const &fragment,
3450
ShadowNodeFamily::Shared const &family,
@@ -42,6 +58,7 @@ ShadowNode::ShadowNode(
4258
fragment.children ? fragment.children
4359
: emptySharedShadowNodeSharedList()),
4460
state_(fragment.state),
61+
stateRevision_(computeStateRevision(state_, children_)),
4562
family_(family),
4663
traits_(traits) {
4764
assert(props_);
@@ -67,6 +84,7 @@ ShadowNode::ShadowNode(
6784
state_(
6885
fragment.state ? fragment.state
6986
: sourceShadowNode.getMostRecentState()),
87+
stateRevision_(computeStateRevision(state_, children_)),
7088
family_(sourceShadowNode.family_),
7189
traits_(sourceShadowNode.traits_) {
7290

@@ -217,6 +235,10 @@ ShadowNodeFamily const &ShadowNode::getFamily() const {
217235
return *family_;
218236
}
219237

238+
int ShadowNode::getStateRevision() const {
239+
return stateRevision_;
240+
}
241+
220242
#pragma mark - DebugStringConvertible
221243

222244
#if RN_DEBUG_STRING_CONVERTIBLE
@@ -225,7 +247,9 @@ std::string ShadowNode::getDebugName() const {
225247
}
226248

227249
std::string ShadowNode::getDebugValue() const {
228-
return "r" + folly::to<std::string>(revision_) +
250+
return "r" + folly::to<std::string>(revision_) + "/sr" +
251+
folly::to<std::string>(stateRevision_) + "/s" +
252+
folly::to<std::string>(state_ ? state_->getRevision() : 0) +
229253
(getSealed() ? "/sealed" : "");
230254
}
231255

ReactCommon/fabric/core/shadownode/ShadowNode.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ class ShadowNode : public virtual Sealable,
144144
*/
145145
void setMounted(bool mounted) const;
146146

147+
int getStateRevision() const;
148+
147149
#pragma mark - DebugStringConvertible
148150

149151
#if RN_DEBUG_STRING_CONVERTIBLE
@@ -167,6 +169,15 @@ class ShadowNode : public virtual Sealable,
167169

168170
private:
169171
friend ShadowNodeFamily;
172+
173+
/**
174+
* This number is deterministically, statelessly recomputable (it's dependent
175+
* only on the immutable properties stored in this class). It tells us the
176+
* version of the state of the entire subtree, including this component and
177+
* all descendants.
178+
*/
179+
int const stateRevision_;
180+
170181
/*
171182
* Clones the list of children (and creates a new `shared_ptr` to it) if
172183
* `childrenAreShared_` flag is `true`.

ReactCommon/fabric/mounting/ShadowTree.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <react/mounting/ShadowViewMutation.h>
1818

1919
#include "ShadowTreeDelegate.h"
20+
#include "TreeStateReconciliation.h"
2021

2122
namespace facebook {
2223
namespace react {
@@ -165,13 +166,24 @@ bool ShadowTree::tryCommit(ShadowTreeCommitTransaction transaction) const {
165166
return false;
166167
}
167168

169+
// Compare state revisions of old and new root
170+
// Children of the root node may be mutated in-place
171+
UnsharedShadowNode reconciledNode =
172+
reconcileStateWithTree(newRootShadowNode.get(), oldRootShadowNode);
173+
if (reconciledNode != nullptr) {
174+
newRootShadowNode =
175+
std::make_shared<RootShadowNode>(*reconciledNode, ShadowNodeFragment{});
176+
}
177+
178+
// Layout nodes
168179
std::vector<LayoutableShadowNode const *> affectedLayoutableNodes{};
169180
affectedLayoutableNodes.reserve(1024);
170181

171182
telemetry.willLayout();
172183
newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes);
173184
telemetry.didLayout();
174185

186+
// Seal the shadow node so it can no longer be mutated
175187
newRootShadowNode->sealRecursive();
176188

177189
auto revisionNumber = ShadowTreeRevision::Number{};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its 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 "TreeStateReconciliation.h"
9+
10+
namespace facebook {
11+
namespace react {
12+
13+
using ChangedShadowNodePairs =
14+
std::vector<std::pair<SharedShadowNode, UnsharedShadowNode>>;
15+
16+
/**
17+
* Clones any children in the subtree that need to be cloned, and adds those to
18+
* the `changedPairs` vector argument.
19+
*
20+
* @param parent
21+
* @param newChildren
22+
* @param oldChildren
23+
* @param changedPairs
24+
*/
25+
static void reconcileStateWithChildren(
26+
ShadowNode const *parent,
27+
const SharedShadowNodeList &newChildren,
28+
const SharedShadowNodeList &oldChildren,
29+
ChangedShadowNodePairs &changedPairs) {
30+
// Find children that are the same family in both trees.
31+
// We only want to find nodes that existing in the new tree - if they
32+
// don't exist in the new tree, they're being deleted; if they don't exist
33+
// in the old tree, they're new. We don't need to deal with either of those
34+
// cases here.
35+
// Currently we use a naive double loop - this could be improved, but we need
36+
// to be able to handle cases where nodes are entirely reordered, for
37+
// instance.
38+
for (int i = 0; i < newChildren.size(); i++) {
39+
bool found = false;
40+
for (int j = 0; j < oldChildren.size() && !found; j++) {
41+
if (ShadowNode::sameFamily(*newChildren[i], *oldChildren[j])) {
42+
UnsharedShadowNode newChild =
43+
reconcileStateWithTree(newChildren[i].get(), oldChildren[j]);
44+
if (newChild != nullptr) {
45+
changedPairs.push_back(std::make_pair(newChildren[i], newChild));
46+
}
47+
found = true;
48+
}
49+
}
50+
}
51+
}
52+
53+
UnsharedShadowNode reconcileStateWithTree(
54+
ShadowNode const *newNode,
55+
SharedShadowNode committedNode) {
56+
// If the revisions on the node are the same, we can finish here.
57+
// Subtrees are guaranteed to be identical at this point, too.
58+
if (committedNode->getStateRevision() <= newNode->getStateRevision()) {
59+
return nullptr;
60+
}
61+
62+
// If we got this fair, we're guaranteed that the state of 1) this node,
63+
// and/or 2) some descendant node is out-of-date and must be reconciled.
64+
// This requires traversing all children, and we must at *least* clone
65+
// this node, whether or not we clone and update any children.
66+
const auto &newChildren = newNode->getChildren();
67+
const auto &oldChildren = committedNode->getChildren();
68+
ChangedShadowNodePairs changedPairs;
69+
reconcileStateWithChildren(newNode, newChildren, oldChildren, changedPairs);
70+
71+
ShadowNode::SharedListOfShared clonedChildren =
72+
ShadowNodeFragment::childrenPlaceholder();
73+
74+
// If any children were cloned, we need to recreate the child list.
75+
// This won't cause any children to be cloned that weren't already cloned -
76+
// it just collects all children, cloned or uncloned, into a new list.
77+
if (changedPairs.size() > 0) {
78+
ShadowNode::UnsharedListOfShared newList =
79+
std::make_shared<ShadowNode::ListOfShared>();
80+
for (int i = 0, j = 0; i < newChildren.size(); i++) {
81+
if (j < changedPairs.size() && changedPairs[j].first == newChildren[i]) {
82+
newList->push_back(changedPairs[j].second);
83+
j++;
84+
} else {
85+
newList->push_back(newChildren[i]);
86+
}
87+
}
88+
clonedChildren = newList;
89+
}
90+
91+
return newNode->clone({/* .props = */ ShadowNodeFragment::propsPlaceholder(),
92+
/* .children = */ clonedChildren,
93+
/* .state = */ newNode->getMostRecentState()});
94+
}
95+
96+
} // namespace react
97+
} // namespace facebook
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its 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 <react/core/ShadowNode.h>
9+
#include <react/core/ShadowNodeFragment.h>
10+
11+
namespace facebook {
12+
namespace react {
13+
14+
/**
15+
* Problem Description: because of C++ State, the React Native C++ ShadowTree
16+
* can diverge from the ReactJS ShadowTree; ReactJS communicates all tree
17+
* changes to C++, but C++ state commits are not propagated to ReactJS (ReactJS
18+
* may or may not clone nodes with state changes, but it has no way of knowing
19+
* if it /should/ clone those nodes; so those clones may never happen). This
20+
* causes a number of problems. This function resolves the problem by taking a
21+
* candidate tree being committed, and sees if any State changes need to be
22+
* applied to it. If any changes need to be made, a new ShadowNode is returned;
23+
* otherwise, nullptr is returned if the node is already consistent with the
24+
* latest tree, including all state changes.
25+
*
26+
* This should be called during the commit phase, pre-layout and pre-diff.
27+
*/
28+
UnsharedShadowNode reconcileStateWithTree(
29+
ShadowNode const *newNode,
30+
SharedShadowNode committedNode);
31+
32+
} // namespace react
33+
} // namespace facebook

0 commit comments

Comments
 (0)