mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
b6bbbf8efa
Summary: TL;DR: For applications using JS navigation, save 50-95% of CPU during mounting phase in N>2 navigations that replace ~most of screen. During investigation of performance on the UI thread of React Native applications, I noticed that the /initial/ render of an screen for an application using JS navigation is /mostly/ consumed (on the UI thread) by tearing-down the previous View hierarchy. In one 185ms segment on the UI thread in production, 95% of the CPU time was Remove/Delete instructions and only 5% of CPU time was consumed by actually displaying the new hierarchy (this is specific to Android and also assumes that View Preallocation is being used, so post-commit work consists of Insert and UpdateLayout mutations primarily). There are /some/ cases where the C++ differ knows that we are deleting an entire subtree and therefore we could communicate this to the mounting layer. All that matters is that these Views are removed from the View hierarchy immediately; and secondarily that their memory is cleaned up ASAP, but that doesn't need to happen immediately. Some additional constraints and notes: 1) As noted in the comments, we cannot simply stop producing Remove and Delete instructions. We need to produce /both/ the new RemoveDeleteTree instruction, /and/ produce all the Remove/Delete instructions, primarily because LayoutAnimations relies heavily on these Remove/Delete instructions and certain things would break if we removed those instructions entirely. However, we can mark those Remove/Delete instructions as redundant, process them only in LayoutAnimations, and not send them to the Android mounting layer. 2) We want to make sure that View Recycling is not impacted. Since Android cannot take advantage of View Recycling until /after/ the second major render (preallocation of views will happen before any views are recycled), this doesn't impact View Recycling and we'll make sure Views are recycled whenever they are deleted. Thus, we do two things: 1) Introduce a new RemoveDeleteTree operation that can delete an entire subtree recursively as part of one operation. This allows us to avoid serializing hundreds or thousands of instructions and prevents JNI traffic. 2) Besides removing the topmost View from the View hierarchy, and ensuring it's not drawn, the full teardown and recycling of the tree can happen /after/ the paint. In some flows with JS navigation this saves us 95% of CPU during the mount phase. In the general case it is probably closer to 25-50% of CPU time that is saved and/or deferred. Changelog: [Android][Changed] Significant perf optimization to Fabric Remove/Delete operations Reviewed By: ryancat Differential Revision: D37257864 fbshipit-source-id: a7d33fc74683939965cfb98be4db7890644110b2
1718 lines
65 KiB
C++
1718 lines
65 KiB
C++
/*
|
|
* 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 "Differentiator.h"
|
|
|
|
#include <butter/map.h>
|
|
#include <butter/small_vector.h>
|
|
#include <react/debug/react_native_assert.h>
|
|
#include <react/renderer/core/LayoutableShadowNode.h>
|
|
#include <react/renderer/debug/SystraceSection.h>
|
|
#include <algorithm>
|
|
#include "ShadowView.h"
|
|
|
|
#ifdef DEBUG_LOGS_DIFFER
|
|
#include <glog/logging.h>
|
|
#define DEBUG_LOGS_BREADCRUMBS 1
|
|
#define DEBUG_LOGS(code) code
|
|
#else
|
|
#define DEBUG_LOGS(code)
|
|
#endif
|
|
|
|
#ifdef DEBUG_LOGS_BREADCRUMBS
|
|
#define BREADCRUMB_TYPE std::string
|
|
#define DIFF_BREADCRUMB(X) (breadcrumb + " - " + std::string(X))
|
|
#define CREATE_DIFF_BREADCRUMB(X) std::to_string(X)
|
|
#else
|
|
|
|
enum class NoBreadcrumb {};
|
|
|
|
#define BREADCRUMB_TYPE NoBreadcrumb const &
|
|
#define DIFF_BREADCRUMB(X) \
|
|
{}
|
|
#define CREATE_DIFF_BREADCRUMB(X) \
|
|
{}
|
|
#endif
|
|
|
|
namespace facebook {
|
|
namespace react {
|
|
|
|
/*
|
|
* Extremely simple and naive implementation of a map.
|
|
* The map is simple but it's optimized for particular constraints that we have
|
|
* here.
|
|
*
|
|
* A regular map implementation (e.g. `std::unordered_map`) has some basic
|
|
* performance guarantees like constant average insertion and lookup complexity.
|
|
* This is nice, but it's *average* complexity measured on a non-trivial amount
|
|
* of data. The regular map is a very complex data structure that using hashing,
|
|
* buckets, multiple comprising operations, multiple allocations and so on.
|
|
*
|
|
* In our particular case, we need a map for `int` to `void *` with a dozen
|
|
* values. In these conditions, nothing can beat a naive implementation using a
|
|
* stack-allocated vector. And this implementation is exactly this: no
|
|
* allocation, no hashing, no complex branching, no buckets, no iterators, no
|
|
* rehashing, no other guarantees. It's crazy limited, unsafe, and performant on
|
|
* a trivial amount of data.
|
|
*
|
|
* Besides that, we also need to optimize for insertion performance (the case
|
|
* where a bunch of views appears on the screen first time); in this
|
|
* implementation, this is as performant as vector `push_back`.
|
|
*/
|
|
template <typename KeyT, typename ValueT, int DefaultSize = 16>
|
|
class TinyMap final {
|
|
public:
|
|
using Pair = std::pair<KeyT, ValueT>;
|
|
using Iterator = Pair *;
|
|
|
|
/**
|
|
* This must strictly only be called from outside of this class.
|
|
*/
|
|
inline Iterator begin() {
|
|
// Force a clean so that iterating over this TinyMap doesn't iterate over
|
|
// erased elements. If all elements erased are at the front of the vector,
|
|
// then we don't need to clean.
|
|
cleanVector(erasedAtFront_ != numErased_);
|
|
|
|
Iterator it = begin_();
|
|
|
|
if (it != nullptr) {
|
|
return it + erasedAtFront_;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
inline Iterator end() {
|
|
// `back()` asserts on the vector being non-empty
|
|
if (vector_.empty() || numErased_ == vector_.size()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return &vector_.back() + 1;
|
|
}
|
|
|
|
inline Iterator find(KeyT key) {
|
|
cleanVector();
|
|
|
|
react_native_assert(key != 0);
|
|
|
|
if (begin_() == nullptr) {
|
|
return end();
|
|
}
|
|
|
|
for (auto it = begin_() + erasedAtFront_; it != end(); it++) {
|
|
if (it->first == key) {
|
|
return it;
|
|
}
|
|
}
|
|
|
|
return end();
|
|
}
|
|
|
|
inline void insert(Pair pair) {
|
|
react_native_assert(pair.first != 0);
|
|
vector_.push_back(pair);
|
|
}
|
|
|
|
inline void erase(Iterator iterator) {
|
|
// Invalidate tag.
|
|
iterator->first = 0;
|
|
|
|
if (iterator == begin_() + erasedAtFront_) {
|
|
erasedAtFront_++;
|
|
}
|
|
|
|
numErased_++;
|
|
}
|
|
|
|
private:
|
|
/**
|
|
* Same as begin() but doesn't call cleanVector at the beginning.
|
|
*/
|
|
inline Iterator begin_() {
|
|
// `front()` asserts on the vector being non-empty
|
|
if (vector_.empty() || vector_.size() == numErased_) {
|
|
return nullptr;
|
|
}
|
|
|
|
return &vector_.front();
|
|
}
|
|
|
|
/**
|
|
* Remove erased elements from internal vector.
|
|
* We only modify the vector if erased elements are at least half of the
|
|
* vector.
|
|
*/
|
|
inline void cleanVector(bool forceClean = false) {
|
|
if ((numErased_ < (vector_.size() / 2) && !forceClean) || vector_.empty() ||
|
|
numErased_ == 0 || numErased_ == erasedAtFront_) {
|
|
return;
|
|
}
|
|
|
|
if (numErased_ == vector_.size()) {
|
|
vector_.clear();
|
|
} else {
|
|
vector_.erase(
|
|
std::remove_if(
|
|
vector_.begin(),
|
|
vector_.end(),
|
|
[](auto const &item) { return item.first == 0; }),
|
|
vector_.end());
|
|
}
|
|
numErased_ = 0;
|
|
erasedAtFront_ = 0;
|
|
}
|
|
|
|
butter::small_vector<Pair, DefaultSize> vector_;
|
|
size_t numErased_{0};
|
|
size_t erasedAtFront_{0};
|
|
};
|
|
|
|
/*
|
|
* Sorting comparator for `reorderInPlaceIfNeeded`.
|
|
*/
|
|
static bool shouldFirstPairComesBeforeSecondOne(
|
|
ShadowViewNodePair const *lhs,
|
|
ShadowViewNodePair const *rhs) noexcept {
|
|
return lhs->shadowNode->getOrderIndex() < rhs->shadowNode->getOrderIndex();
|
|
}
|
|
|
|
/*
|
|
* Reorders pairs in-place based on `orderIndex` using a stable sort algorithm.
|
|
*/
|
|
static void reorderInPlaceIfNeeded(
|
|
ShadowViewNodePair::NonOwningList &pairs) noexcept {
|
|
if (pairs.size() < 2) {
|
|
return;
|
|
}
|
|
|
|
auto isReorderNeeded = false;
|
|
for (auto const &pair : pairs) {
|
|
if (pair->shadowNode->getOrderIndex() != 0) {
|
|
isReorderNeeded = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isReorderNeeded) {
|
|
return;
|
|
}
|
|
|
|
std::stable_sort(
|
|
pairs.begin(), pairs.end(), &shouldFirstPairComesBeforeSecondOne);
|
|
}
|
|
|
|
static inline bool shadowNodeIsConcrete(ShadowNode const &shadowNode) {
|
|
return shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView);
|
|
}
|
|
|
|
static void sliceChildShadowNodeViewPairsRecursivelyV2(
|
|
ShadowViewNodePair::NonOwningList &pairList,
|
|
ViewNodePairScope &scope,
|
|
Point layoutOffset,
|
|
ShadowNode const &shadowNode) {
|
|
for (auto const &sharedChildShadowNode : shadowNode.getChildren()) {
|
|
auto &childShadowNode = *sharedChildShadowNode;
|
|
|
|
#ifndef ANDROID
|
|
// Temporary disabled on Android because the mounting infrastructure
|
|
// is not fully ready yet.
|
|
if (childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) {
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
auto shadowView = ShadowView(childShadowNode);
|
|
auto origin = layoutOffset;
|
|
if (shadowView.layoutMetrics != EmptyLayoutMetrics) {
|
|
origin += shadowView.layoutMetrics.frame.origin;
|
|
shadowView.layoutMetrics.frame.origin += layoutOffset;
|
|
}
|
|
|
|
// This might not be a FormsView, or a FormsStackingContext. We let the
|
|
// differ handle removal of flattened views from the Mounting layer and
|
|
// shuffling their children around.
|
|
bool isConcreteView = shadowNodeIsConcrete(childShadowNode);
|
|
bool areChildrenFlattened = !childShadowNode.getTraits().check(
|
|
ShadowNodeTraits::Trait::FormsStackingContext);
|
|
Point storedOrigin = {};
|
|
if (areChildrenFlattened) {
|
|
storedOrigin = origin;
|
|
}
|
|
scope.push_back(
|
|
{shadowView,
|
|
&childShadowNode,
|
|
areChildrenFlattened,
|
|
isConcreteView,
|
|
storedOrigin});
|
|
pairList.push_back(&scope.back());
|
|
|
|
if (areChildrenFlattened) {
|
|
sliceChildShadowNodeViewPairsRecursivelyV2(
|
|
pairList, scope, origin, childShadowNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
ShadowViewNodePair::NonOwningList sliceChildShadowNodeViewPairsV2(
|
|
ShadowNode const &shadowNode,
|
|
ViewNodePairScope &scope,
|
|
bool allowFlattened,
|
|
Point layoutOffset) {
|
|
auto pairList = ShadowViewNodePair::NonOwningList{};
|
|
|
|
if (!shadowNode.getTraits().check(
|
|
ShadowNodeTraits::Trait::FormsStackingContext) &&
|
|
shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView) &&
|
|
!allowFlattened) {
|
|
return pairList;
|
|
}
|
|
|
|
sliceChildShadowNodeViewPairsRecursivelyV2(
|
|
pairList, scope, layoutOffset, shadowNode);
|
|
|
|
// Sorting pairs based on `orderIndex` if needed.
|
|
reorderInPlaceIfNeeded(pairList);
|
|
|
|
// Set list and mountIndex for each after reordering
|
|
size_t mountIndex = 0;
|
|
for (auto child : pairList) {
|
|
child->mountIndex = (child->isConcreteView ? mountIndex++ : -1);
|
|
}
|
|
|
|
return pairList;
|
|
}
|
|
|
|
/**
|
|
* Prefer calling this over `sliceChildShadowNodeViewPairsV2` directly, when
|
|
* possible. This can account for adding parent LayoutMetrics that are
|
|
* important to take into account, but tricky, in (un)flattening cases.
|
|
*/
|
|
static ShadowViewNodePair::NonOwningList
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
ShadowViewNodePair const &shadowViewNodePair,
|
|
ViewNodePairScope &scope,
|
|
bool allowFlattened = false) {
|
|
return sliceChildShadowNodeViewPairsV2(
|
|
*shadowViewNodePair.shadowNode,
|
|
scope,
|
|
allowFlattened,
|
|
shadowViewNodePair.contextOrigin);
|
|
}
|
|
|
|
/*
|
|
* Before we start to diff, let's make sure all our core data structures are
|
|
* in good shape to deliver the best performance.
|
|
*/
|
|
static_assert(
|
|
std::is_move_constructible<ShadowViewMutation>::value,
|
|
"`ShadowViewMutation` must be `move constructible`.");
|
|
static_assert(
|
|
std::is_move_constructible<ShadowView>::value,
|
|
"`ShadowView` must be `move constructible`.");
|
|
static_assert(
|
|
std::is_move_constructible<ShadowViewNodePair>::value,
|
|
"`ShadowViewNodePair` must be `move constructible`.");
|
|
static_assert(
|
|
std::is_move_constructible<ShadowViewNodePair::NonOwningList>::value,
|
|
"`ShadowViewNodePair::NonOwningList` must be `move constructible`.");
|
|
|
|
static_assert(
|
|
std::is_move_assignable<ShadowViewMutation>::value,
|
|
"`ShadowViewMutation` must be `move assignable`.");
|
|
static_assert(
|
|
std::is_move_assignable<ShadowView>::value,
|
|
"`ShadowView` must be `move assignable`.");
|
|
static_assert(
|
|
std::is_move_assignable<ShadowViewNodePair>::value,
|
|
"`ShadowViewNodePair` must be `move assignable`.");
|
|
static_assert(
|
|
std::is_move_assignable<ShadowViewNodePair::NonOwningList>::value,
|
|
"`ShadowViewNodePair::NonOwningList` must be `move assignable`.");
|
|
|
|
static void calculateShadowViewMutationsV2(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
ViewNodePairScope &scope,
|
|
ShadowViewMutation::List &mutations,
|
|
ShadowView const &parentShadowView,
|
|
ShadowViewNodePair::NonOwningList &&oldChildPairs,
|
|
ShadowViewNodePair::NonOwningList &&newChildPairs,
|
|
bool isRecursionRedundant = false);
|
|
|
|
struct OrderedMutationInstructionContainer {
|
|
ShadowViewMutation::List createMutations{};
|
|
ShadowViewMutation::List deleteMutations{};
|
|
ShadowViewMutation::List insertMutations{};
|
|
ShadowViewMutation::List removeMutations{};
|
|
ShadowViewMutation::List updateMutations{};
|
|
ShadowViewMutation::List downwardMutations{};
|
|
ShadowViewMutation::List destructiveDownwardMutations{};
|
|
};
|
|
|
|
static void updateMatchedPairSubtrees(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
ViewNodePairScope &scope,
|
|
OrderedMutationInstructionContainer &mutationContainer,
|
|
TinyMap<Tag, ShadowViewNodePair *> &newRemainingPairs,
|
|
ShadowViewNodePair::NonOwningList &oldChildPairs,
|
|
ShadowViewNodePair::NonOwningList &newChildPairs,
|
|
ShadowView const &parentShadowView,
|
|
ShadowViewNodePair const &oldPair,
|
|
ShadowViewNodePair const &newPair);
|
|
|
|
static void updateMatchedPair(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
OrderedMutationInstructionContainer &mutationContainer,
|
|
bool oldNodeFoundInOrder,
|
|
bool newNodeFoundInOrder,
|
|
ShadowView const &parentShadowView,
|
|
ShadowViewNodePair const &oldPair,
|
|
ShadowViewNodePair const &newPair);
|
|
|
|
static void calculateShadowViewMutationsFlattener(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
ViewNodePairScope &scope,
|
|
ReparentMode reparentMode,
|
|
OrderedMutationInstructionContainer &mutationContainer,
|
|
ShadowView const &parentShadowView,
|
|
TinyMap<Tag, ShadowViewNodePair *> &unvisitedFlattenedNodes,
|
|
ShadowViewNodePair const &node,
|
|
TinyMap<Tag, ShadowViewNodePair *> *parentSubVisitedOtherNewNodes = nullptr,
|
|
TinyMap<Tag, ShadowViewNodePair *> *parentSubVisitedOtherOldNodes =
|
|
nullptr);
|
|
|
|
/**
|
|
* Updates the subtrees of any matched ShadowViewNodePair. This handles
|
|
* all cases of flattening/unflattening.
|
|
*
|
|
* This may modify data-structures passed to it and owned by the caller,
|
|
* specifically `newRemainingPairs`, and so the caller must also own
|
|
* the ViewNodePairScope used within.
|
|
*/
|
|
static void updateMatchedPairSubtrees(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
ViewNodePairScope &scope,
|
|
OrderedMutationInstructionContainer &mutationContainer,
|
|
TinyMap<Tag, ShadowViewNodePair *> &newRemainingPairs,
|
|
ShadowViewNodePair::NonOwningList &oldChildPairs,
|
|
ShadowViewNodePair::NonOwningList &newChildPairs,
|
|
ShadowView const &parentShadowView,
|
|
ShadowViewNodePair const &oldPair,
|
|
ShadowViewNodePair const &newPair) {
|
|
// Are we flattening or unflattening either one? If node was
|
|
// flattened in both trees, there's no change, just continue.
|
|
if (oldPair.flattened && newPair.flattened) {
|
|
return;
|
|
}
|
|
|
|
// We are either flattening or unflattening this node.
|
|
if (oldPair.flattened != newPair.flattened) {
|
|
DEBUG_LOGS({
|
|
LOG(ERROR)
|
|
<< "Differ: flattening or unflattening in updateMatchedPairSubtrees: ["
|
|
<< oldPair.shadowView.tag << "] [" << newPair.shadowView.tag << "] "
|
|
<< oldPair.flattened << " " << newPair.flattened << " with parent: ["
|
|
<< parentShadowView.tag << "]";
|
|
});
|
|
|
|
// Flattening
|
|
if (!oldPair.flattened) {
|
|
// Flatten old tree into new list
|
|
// At the end of this loop we still want to know which of these
|
|
// children are visited, so we reuse the `newRemainingPairs`
|
|
// map.
|
|
calculateShadowViewMutationsFlattener(
|
|
DIFF_BREADCRUMB(
|
|
"Flatten tree " + std::to_string(parentShadowView.tag) +
|
|
" into list " + std::to_string(oldPair.shadowView.tag)),
|
|
scope,
|
|
ReparentMode::Flatten,
|
|
mutationContainer,
|
|
parentShadowView,
|
|
newRemainingPairs,
|
|
oldPair);
|
|
}
|
|
// Unflattening
|
|
else {
|
|
// Construct unvisited nodes map
|
|
auto unvisitedOldChildPairs = TinyMap<Tag, ShadowViewNodePair *>{};
|
|
// We don't know where all the children of oldChildPair are
|
|
// within oldChildPairs, but we know that they're in the same
|
|
// relative order. The reason for this is because of flattening
|
|
// + zIndex: the children could be listed before the parent,
|
|
// interwoven with children from other nodes, etc.
|
|
auto oldFlattenedNodes =
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(oldPair, scope, true);
|
|
for (size_t i = 0, j = 0;
|
|
i < oldChildPairs.size() && j < oldFlattenedNodes.size();
|
|
i++) {
|
|
auto &oldChild = *oldChildPairs[i];
|
|
if (oldChild.shadowView.tag == oldFlattenedNodes[j]->shadowView.tag) {
|
|
unvisitedOldChildPairs.insert({oldChild.shadowView.tag, &oldChild});
|
|
j++;
|
|
}
|
|
}
|
|
|
|
// Unflatten old list into new tree
|
|
calculateShadowViewMutationsFlattener(
|
|
DIFF_BREADCRUMB(
|
|
"Unflatten old list " + std::to_string(parentShadowView.tag) +
|
|
" into new tree " + std::to_string(newPair.shadowView.tag)),
|
|
scope,
|
|
ReparentMode::Unflatten,
|
|
mutationContainer,
|
|
parentShadowView,
|
|
unvisitedOldChildPairs,
|
|
newPair);
|
|
|
|
// If old nodes were not visited, we know that we can delete
|
|
// them now. They will be removed from the hierarchy by the
|
|
// outermost loop of this function.
|
|
// TODO: is this necessary anymore?
|
|
for (auto &oldFlattenedNodePtr : oldFlattenedNodes) {
|
|
auto &oldFlattenedNode = *oldFlattenedNodePtr;
|
|
auto unvisitedOldChildPairIt =
|
|
unvisitedOldChildPairs.find(oldFlattenedNode.shadowView.tag);
|
|
if (unvisitedOldChildPairIt == unvisitedOldChildPairs.end()) {
|
|
// Node was visited - make sure to remove it from
|
|
// "newRemainingPairs" map
|
|
auto newRemainingIt =
|
|
newRemainingPairs.find(oldFlattenedNode.shadowView.tag);
|
|
if (newRemainingIt != newRemainingPairs.end()) {
|
|
newRemainingPairs.erase(newRemainingIt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Update subtrees if View is not flattened, and if node addresses
|
|
// are not equal
|
|
if (oldPair.shadowNode != newPair.shadowNode) {
|
|
ViewNodePairScope innerScope{};
|
|
auto oldGrandChildPairs =
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(oldPair, innerScope);
|
|
auto newGrandChildPairs =
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(newPair, innerScope);
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Non-trivial update " + std::to_string(oldPair.shadowView.tag)),
|
|
innerScope,
|
|
*(newGrandChildPairs.size()
|
|
? &mutationContainer.downwardMutations
|
|
: &mutationContainer.destructiveDownwardMutations),
|
|
oldPair.shadowView,
|
|
std::move(oldGrandChildPairs),
|
|
std::move(newGrandChildPairs));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle updates to a matched node pair, but NOT to their subtrees.
|
|
*
|
|
* Here we have (and need) knowledge of whether a node was found during
|
|
* in-order traversal, or out-of-order via a map lookup. Nodes are only REMOVEd
|
|
* or INSERTed when they are encountered via in-order-traversal, to ensure
|
|
* correct ordering of INSERT and REMOVE mutations.
|
|
*/
|
|
static void updateMatchedPair(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
OrderedMutationInstructionContainer &mutationContainer,
|
|
bool oldNodeFoundInOrder,
|
|
bool newNodeFoundInOrder,
|
|
ShadowView const &parentShadowView,
|
|
ShadowViewNodePair const &oldPair,
|
|
ShadowViewNodePair const &newPair) {
|
|
oldPair.otherTreePair = &newPair;
|
|
newPair.otherTreePair = &oldPair;
|
|
|
|
// Check concrete-ness of views
|
|
// Create/Delete and Insert/Remove if necessary
|
|
if (oldPair.isConcreteView != newPair.isConcreteView) {
|
|
if (newPair.isConcreteView) {
|
|
if (newNodeFoundInOrder) {
|
|
mutationContainer.insertMutations.push_back(
|
|
ShadowViewMutation::InsertMutation(
|
|
parentShadowView,
|
|
newPair.shadowView,
|
|
static_cast<int>(newPair.mountIndex)));
|
|
}
|
|
mutationContainer.createMutations.push_back(
|
|
ShadowViewMutation::CreateMutation(newPair.shadowView));
|
|
} else {
|
|
if (oldNodeFoundInOrder) {
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveMutation(
|
|
parentShadowView,
|
|
oldPair.shadowView,
|
|
static_cast<int>(oldPair.mountIndex)));
|
|
}
|
|
mutationContainer.deleteMutations.push_back(
|
|
ShadowViewMutation::DeleteMutation(oldPair.shadowView));
|
|
}
|
|
} else if (oldPair.isConcreteView && newPair.isConcreteView) {
|
|
// If we found the old node by traversing, but not the new node,
|
|
// it means that there's some reordering requiring a REMOVE mutation.
|
|
if (oldNodeFoundInOrder && !newNodeFoundInOrder) {
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveMutation(
|
|
parentShadowView,
|
|
newPair.shadowView,
|
|
static_cast<int>(oldPair.mountIndex)));
|
|
}
|
|
|
|
// Even if node's children are flattened, it might still be a
|
|
// concrete view. The case where they're different is handled
|
|
// above.
|
|
if (oldPair.shadowView != newPair.shadowView) {
|
|
mutationContainer.updateMutations.push_back(
|
|
ShadowViewMutation::UpdateMutation(
|
|
oldPair.shadowView, newPair.shadowView));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Here we flatten or unflatten a subtree, given an unflattened node in either
|
|
* the old or new tree, and a list of flattened nodes in the other tree.
|
|
*
|
|
* For example: if you are Flattening, the node will be in the old tree and
|
|
the
|
|
* list will be from the new tree. If you are Unflattening, the opposite is
|
|
true.
|
|
|
|
* It is currently not possible for ReactJS, and therefore React Native, to
|
|
move
|
|
* a node *from* one parent to another without an entirely new subtree being
|
|
* created. When we "reparent" in React Native here it is only because
|
|
intermediate
|
|
* ShadowNodes/ShadowViews, which *always* exist, are flattened or unflattened
|
|
away.
|
|
* Thus, this algorithm handles the very specialized cases of the tree
|
|
collapsing or
|
|
* expanding vertically in that way.
|
|
|
|
* Sketch of algorithm:
|
|
* 0. Create a map of nodes in the flattened list. This should be done
|
|
*before*
|
|
* calling this function.
|
|
* 1. Traverse the Node Subtree; remove elements from the map as they are
|
|
* visited in the tree.
|
|
* Perform a Remove/Insert depending on if we're flattening or unflattening
|
|
* If Tree node is not in Map/List, perform Delete/Create.
|
|
* 2. Traverse the list.
|
|
* Perform linear remove from the old View, or insert into the new parent
|
|
* View if we're flattening.
|
|
* If a node is in the list but not the map, it means it's been visited and
|
|
* Update has already been
|
|
* performed in the subtree. If it *is* in the map, it means the node is
|
|
not
|
|
* * in the Tree, and should be Deleted/Created
|
|
* **after this function is called**, by the caller.
|
|
*/
|
|
static void calculateShadowViewMutationsFlattener(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
ViewNodePairScope &scope,
|
|
ReparentMode reparentMode,
|
|
OrderedMutationInstructionContainer &mutationContainer,
|
|
ShadowView const &parentShadowView,
|
|
TinyMap<Tag, ShadowViewNodePair *> &unvisitedOtherNodes,
|
|
ShadowViewNodePair const &node,
|
|
TinyMap<Tag, ShadowViewNodePair *> *parentSubVisitedOtherNewNodes,
|
|
TinyMap<Tag, ShadowViewNodePair *> *parentSubVisitedOtherOldNodes) {
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Flattener 1: "
|
|
<< (reparentMode == ReparentMode::Unflatten ? "Unflattening"
|
|
: "Flattening")
|
|
<< " [" << node.shadowView.tag << "]";
|
|
});
|
|
|
|
// Step 1: iterate through entire tree
|
|
ShadowViewNodePair::NonOwningList treeChildren =
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(node, scope);
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Flattener 1.4: "
|
|
<< (reparentMode == ReparentMode::Unflatten ? "Unflattening"
|
|
: "Flattening")
|
|
<< " [" << node.shadowView.tag << "]";
|
|
LOG(ERROR) << "Differ Flattener Entry: Child Pairs: ";
|
|
std::string strTreeChildPairs;
|
|
for (size_t k = 0; k < treeChildren.size(); k++) {
|
|
strTreeChildPairs.append(std::to_string(treeChildren[k]->shadowView.tag));
|
|
strTreeChildPairs.append(treeChildren[k]->isConcreteView ? "" : "'");
|
|
strTreeChildPairs.append(treeChildren[k]->flattened ? "*" : "");
|
|
strTreeChildPairs.append(", ");
|
|
}
|
|
std::string strListChildPairs;
|
|
for (auto &unvisitedNode : unvisitedOtherNodes) {
|
|
strListChildPairs.append(
|
|
std::to_string(unvisitedNode.second->shadowView.tag));
|
|
strListChildPairs.append(unvisitedNode.second->isConcreteView ? "" : "'");
|
|
strListChildPairs.append(unvisitedNode.second->flattened ? "*" : "");
|
|
strListChildPairs.append(", ");
|
|
}
|
|
LOG(ERROR) << "Differ Flattener Entry: Tree Child Pairs: "
|
|
<< strTreeChildPairs;
|
|
LOG(ERROR) << "Differ Flattener Entry: List Child Pairs: "
|
|
<< strListChildPairs;
|
|
});
|
|
|
|
// Views in other tree that are visited by sub-flattening or
|
|
// sub-unflattening
|
|
TinyMap<Tag, ShadowViewNodePair *> subVisitedOtherNewNodes{};
|
|
TinyMap<Tag, ShadowViewNodePair *> subVisitedOtherOldNodes{};
|
|
auto subVisitedNewMap =
|
|
(parentSubVisitedOtherNewNodes != nullptr ? parentSubVisitedOtherNewNodes
|
|
: &subVisitedOtherNewNodes);
|
|
auto subVisitedOldMap =
|
|
(parentSubVisitedOtherOldNodes != nullptr ? parentSubVisitedOtherOldNodes
|
|
: &subVisitedOtherOldNodes);
|
|
|
|
// Candidates for full tree creation or deletion at the end of this function
|
|
auto deletionCreationCandidatePairs =
|
|
TinyMap<Tag, ShadowViewNodePair const *>{};
|
|
|
|
for (size_t index = 0;
|
|
index < treeChildren.size() && index < treeChildren.size();
|
|
index++) {
|
|
auto &treeChildPair = *treeChildren[index];
|
|
|
|
// Try to find node in other tree
|
|
auto unvisitedIt = unvisitedOtherNodes.find(treeChildPair.shadowView.tag);
|
|
auto subVisitedOtherNewIt =
|
|
(unvisitedIt == unvisitedOtherNodes.end()
|
|
? subVisitedNewMap->find(treeChildPair.shadowView.tag)
|
|
: subVisitedNewMap->end());
|
|
auto subVisitedOtherOldIt =
|
|
(unvisitedIt == unvisitedOtherNodes.end() && subVisitedNewMap->end()
|
|
? subVisitedOldMap->find(treeChildPair.shadowView.tag)
|
|
: subVisitedOldMap->end());
|
|
|
|
bool existsInOtherTree = unvisitedIt != unvisitedOtherNodes.end() ||
|
|
subVisitedOtherNewIt != subVisitedNewMap->end() ||
|
|
subVisitedOtherOldIt != subVisitedOldMap->end();
|
|
|
|
auto otherTreeNodePairPtr =
|
|
(existsInOtherTree
|
|
? (unvisitedIt != unvisitedOtherNodes.end()
|
|
? unvisitedIt->second
|
|
: (subVisitedOtherNewIt != subVisitedNewMap->end()
|
|
? subVisitedOtherNewIt->second
|
|
: subVisitedOtherOldIt->second))
|
|
: nullptr);
|
|
|
|
react_native_assert(
|
|
!existsInOtherTree ||
|
|
(unvisitedIt != unvisitedOtherNodes.end() ||
|
|
subVisitedOtherNewIt != subVisitedNewMap->end() ||
|
|
subVisitedOtherOldIt != subVisitedOldMap->end()));
|
|
react_native_assert(
|
|
unvisitedIt == unvisitedOtherNodes.end() ||
|
|
unvisitedIt->second->shadowView.tag == treeChildPair.shadowView.tag);
|
|
react_native_assert(
|
|
subVisitedOtherNewIt == subVisitedNewMap->end() ||
|
|
subVisitedOtherNewIt->second->shadowView.tag ==
|
|
treeChildPair.shadowView.tag);
|
|
react_native_assert(
|
|
subVisitedOtherOldIt == subVisitedOldMap->end() ||
|
|
subVisitedOtherOldIt->second->shadowView.tag ==
|
|
treeChildPair.shadowView.tag);
|
|
|
|
bool alreadyUpdated = false;
|
|
|
|
// Find in other tree and updated `otherTreePair` pointers
|
|
if (existsInOtherTree) {
|
|
react_native_assert(otherTreeNodePairPtr != nullptr);
|
|
auto newTreeNodePair =
|
|
(reparentMode == ReparentMode::Flatten ? otherTreeNodePairPtr
|
|
: &treeChildPair);
|
|
auto oldTreeNodePair =
|
|
(reparentMode == ReparentMode::Flatten ? &treeChildPair
|
|
: otherTreeNodePairPtr);
|
|
|
|
react_native_assert(newTreeNodePair->shadowView.tag != 0);
|
|
react_native_assert(oldTreeNodePair->shadowView.tag != 0);
|
|
react_native_assert(
|
|
oldTreeNodePair->shadowView.tag == newTreeNodePair->shadowView.tag);
|
|
|
|
alreadyUpdated =
|
|
newTreeNodePair->inOtherTree() || oldTreeNodePair->inOtherTree();
|
|
|
|
// We want to update these values unconditionally. Always do this
|
|
// before hitting any "continue" statements.
|
|
newTreeNodePair->otherTreePair = oldTreeNodePair;
|
|
oldTreeNodePair->otherTreePair = newTreeNodePair;
|
|
react_native_assert(treeChildPair.otherTreePair != nullptr);
|
|
}
|
|
|
|
// Remove all children (non-recursively) of tree being flattened, or
|
|
// insert children into parent tree if they're being unflattened.
|
|
// Caller will take care of the corresponding action in the other tree
|
|
// (caller will handle DELETE case if we REMOVE here; caller will handle
|
|
// CREATE case if we INSERT here).
|
|
if (treeChildPair.isConcreteView) {
|
|
if (reparentMode == ReparentMode::Flatten) {
|
|
// treeChildPair.shadowView represents the "old" view in this case.
|
|
// If there's a "new" view, an UPDATE new -> old will be generated
|
|
// and will be executed before the REMOVE. Thus, we must actually
|
|
// perform a REMOVE (new view) FROM (old index) in this case so that
|
|
// we don't hit asserts in StubViewTree's REMOVE path.
|
|
// We also only do this if the "other" (newer) view is concrete. If
|
|
// it's not concrete, there will be no UPDATE mutation.
|
|
react_native_assert(existsInOtherTree == treeChildPair.inOtherTree());
|
|
if (treeChildPair.inOtherTree() &&
|
|
treeChildPair.otherTreePair->isConcreteView) {
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveMutation(
|
|
node.shadowView,
|
|
treeChildPair.otherTreePair->shadowView,
|
|
static_cast<int>(treeChildPair.mountIndex)));
|
|
} else {
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveMutation(
|
|
node.shadowView,
|
|
treeChildPair.shadowView,
|
|
static_cast<int>(treeChildPair.mountIndex)));
|
|
}
|
|
} else {
|
|
// treeChildParent represents the "new" version of the node, so
|
|
// we can safely insert it without checking in the other tree
|
|
mutationContainer.insertMutations.push_back(
|
|
ShadowViewMutation::InsertMutation(
|
|
node.shadowView,
|
|
treeChildPair.shadowView,
|
|
static_cast<int>(treeChildPair.mountIndex)));
|
|
}
|
|
}
|
|
|
|
// Find in other tree
|
|
if (existsInOtherTree) {
|
|
react_native_assert(otherTreeNodePairPtr != nullptr);
|
|
auto &otherTreeNodePair = *otherTreeNodePairPtr;
|
|
|
|
auto &newTreeNodePair =
|
|
(reparentMode == ReparentMode::Flatten ? otherTreeNodePair
|
|
: treeChildPair);
|
|
auto &oldTreeNodePair =
|
|
(reparentMode == ReparentMode::Flatten ? treeChildPair
|
|
: otherTreeNodePair);
|
|
|
|
react_native_assert(newTreeNodePair.shadowView.tag != 0);
|
|
react_native_assert(oldTreeNodePair.shadowView.tag != 0);
|
|
react_native_assert(
|
|
oldTreeNodePair.shadowView.tag == newTreeNodePair.shadowView.tag);
|
|
|
|
// If we've already done updates, don't repeat it.
|
|
if (alreadyUpdated) {
|
|
continue;
|
|
}
|
|
|
|
// If we've already done updates on this node, don't repeat.
|
|
if (reparentMode == ReparentMode::Flatten &&
|
|
unvisitedIt == unvisitedOtherNodes.end() &&
|
|
subVisitedOtherOldIt != subVisitedOldMap->end()) {
|
|
continue;
|
|
} else if (
|
|
reparentMode == ReparentMode::Unflatten &&
|
|
unvisitedIt == unvisitedOtherNodes.end() &&
|
|
subVisitedOtherNewIt != subVisitedNewMap->end()) {
|
|
continue;
|
|
}
|
|
|
|
// TODO: compare ShadowNode pointer instead of ShadowView here?
|
|
// Or ShadowNode ptr comparison before comparing ShadowView, to allow for
|
|
// short-circuiting? ShadowView comparison is relatively expensive vs
|
|
// ShadowNode.
|
|
if (newTreeNodePair.shadowView != oldTreeNodePair.shadowView &&
|
|
newTreeNodePair.isConcreteView && oldTreeNodePair.isConcreteView) {
|
|
mutationContainer.updateMutations.push_back(
|
|
ShadowViewMutation::UpdateMutation(
|
|
oldTreeNodePair.shadowView, newTreeNodePair.shadowView));
|
|
}
|
|
|
|
// Update children if appropriate.
|
|
if (!oldTreeNodePair.flattened && !newTreeNodePair.flattened) {
|
|
if (oldTreeNodePair.shadowNode != newTreeNodePair.shadowNode) {
|
|
ViewNodePairScope innerScope{};
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"(Un)Flattener trivial update of " +
|
|
std::to_string(newTreeNodePair.shadowView.tag)),
|
|
innerScope,
|
|
mutationContainer.downwardMutations,
|
|
newTreeNodePair.shadowView,
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
oldTreeNodePair, innerScope),
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
newTreeNodePair, innerScope));
|
|
}
|
|
} else if (oldTreeNodePair.flattened != newTreeNodePair.flattened) {
|
|
// We need to handle one of the children being flattened or
|
|
// unflattened, in the context of a parent flattening or unflattening.
|
|
ReparentMode childReparentMode =
|
|
(oldTreeNodePair.flattened ? ReparentMode::Unflatten
|
|
: ReparentMode::Flatten);
|
|
|
|
// Case 1: child mode is the same as parent.
|
|
// This is a flatten-flatten, or unflatten-unflatten.
|
|
if (childReparentMode == reparentMode) {
|
|
calculateShadowViewMutationsFlattener(
|
|
DIFF_BREADCRUMB(
|
|
std::string(
|
|
reparentMode == ReparentMode::Flatten
|
|
? "Flatten-Flatten"
|
|
: "Unflatten-Unflatten") +
|
|
" new:" +
|
|
std::to_string(
|
|
reparentMode == ReparentMode::Flatten
|
|
? parentShadowView.tag
|
|
: newTreeNodePair.shadowView.tag) +
|
|
" old:" + std::to_string(treeChildPair.shadowView.tag)),
|
|
scope,
|
|
childReparentMode,
|
|
mutationContainer,
|
|
(reparentMode == ReparentMode::Flatten
|
|
? parentShadowView
|
|
: newTreeNodePair.shadowView),
|
|
unvisitedOtherNodes,
|
|
treeChildPair,
|
|
subVisitedNewMap,
|
|
subVisitedOldMap);
|
|
} else {
|
|
// Get flattened nodes from either new or old tree
|
|
auto flattenedNodes = sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
(childReparentMode == ReparentMode::Flatten ? newTreeNodePair
|
|
: oldTreeNodePair),
|
|
scope,
|
|
true);
|
|
// Construct unvisited nodes map
|
|
auto unvisitedRecursiveChildPairs =
|
|
TinyMap<Tag, ShadowViewNodePair *>{};
|
|
for (auto &flattenedNode : flattenedNodes) {
|
|
auto &newChild = *flattenedNode;
|
|
|
|
auto unvisitedOtherNodesIt =
|
|
unvisitedOtherNodes.find(newChild.shadowView.tag);
|
|
if (unvisitedOtherNodesIt != unvisitedOtherNodes.end()) {
|
|
auto unvisitedItPair = *unvisitedOtherNodesIt->second;
|
|
unvisitedRecursiveChildPairs.insert(
|
|
{unvisitedItPair.shadowView.tag, &unvisitedItPair});
|
|
} else {
|
|
unvisitedRecursiveChildPairs.insert(
|
|
{newChild.shadowView.tag, &newChild});
|
|
}
|
|
}
|
|
|
|
// Unflatten parent, flatten child
|
|
if (childReparentMode == ReparentMode::Flatten) {
|
|
// Flatten old tree into new list
|
|
// At the end of this loop we still want to know which of these
|
|
// children are visited, so we reuse the `newRemainingPairs` map.
|
|
calculateShadowViewMutationsFlattener(
|
|
DIFF_BREADCRUMB(
|
|
std::string("Flatten old tree into new list; new:") +
|
|
std::to_string(
|
|
reparentMode == ReparentMode::Flatten
|
|
? parentShadowView.tag
|
|
: newTreeNodePair.shadowView.tag) +
|
|
" old:" + std::to_string(oldTreeNodePair.shadowView.tag)),
|
|
scope,
|
|
ReparentMode::Flatten,
|
|
mutationContainer,
|
|
(reparentMode == ReparentMode::Flatten
|
|
? parentShadowView
|
|
: newTreeNodePair.shadowView),
|
|
unvisitedRecursiveChildPairs,
|
|
oldTreeNodePair,
|
|
subVisitedNewMap,
|
|
subVisitedOldMap);
|
|
}
|
|
// Flatten parent, unflatten child
|
|
else {
|
|
// Unflatten old list into new tree
|
|
calculateShadowViewMutationsFlattener(
|
|
DIFF_BREADCRUMB(
|
|
"Unflatten old list into new tree; old:" +
|
|
std::to_string(
|
|
reparentMode == ReparentMode::Flatten
|
|
? parentShadowView.tag
|
|
: newTreeNodePair.shadowView.tag) +
|
|
" new:" + std::to_string(newTreeNodePair.shadowView.tag)),
|
|
scope,
|
|
ReparentMode::Unflatten,
|
|
mutationContainer,
|
|
(reparentMode == ReparentMode::Flatten
|
|
? parentShadowView
|
|
: newTreeNodePair.shadowView),
|
|
unvisitedRecursiveChildPairs,
|
|
newTreeNodePair,
|
|
subVisitedNewMap,
|
|
subVisitedOldMap);
|
|
|
|
// If old nodes were not visited, we know that we can delete them
|
|
// now. They will be removed from the hierarchy by the outermost
|
|
// loop of this function.
|
|
for (auto &unvisitedRecursiveChildPair :
|
|
unvisitedRecursiveChildPairs) {
|
|
if (unvisitedRecursiveChildPair.first == 0) {
|
|
continue;
|
|
}
|
|
auto &oldFlattenedNode = *unvisitedRecursiveChildPair.second;
|
|
|
|
// Node unvisited - mark the entire subtree for deletion
|
|
if (oldFlattenedNode.isConcreteView &&
|
|
!oldFlattenedNode.inOtherTree()) {
|
|
Tag tag = oldFlattenedNode.shadowView.tag;
|
|
auto deleteCreateIt = deletionCreationCandidatePairs.find(
|
|
oldFlattenedNode.shadowView.tag);
|
|
if (deleteCreateIt == deletionCreationCandidatePairs.end()) {
|
|
deletionCreationCandidatePairs.insert(
|
|
{tag, &oldFlattenedNode});
|
|
}
|
|
} else {
|
|
// Node was visited - make sure to remove it from
|
|
// "newRemainingPairs" map
|
|
auto newRemainingIt =
|
|
unvisitedOtherNodes.find(oldFlattenedNode.shadowView.tag);
|
|
if (newRemainingIt != unvisitedOtherNodes.end()) {
|
|
unvisitedOtherNodes.erase(newRemainingIt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark that node exists in another tree, but only if the tree node is a
|
|
// concrete view. Removing the node from the unvisited list prevents the
|
|
// caller from taking further action on this node, so make sure to
|
|
// delete/create if the Concreteness of the node has changed.
|
|
if (newTreeNodePair.isConcreteView != oldTreeNodePair.isConcreteView) {
|
|
if (newTreeNodePair.isConcreteView) {
|
|
mutationContainer.createMutations.push_back(
|
|
ShadowViewMutation::CreateMutation(newTreeNodePair.shadowView));
|
|
} else {
|
|
mutationContainer.deleteMutations.push_back(
|
|
ShadowViewMutation::DeleteMutation(oldTreeNodePair.shadowView));
|
|
}
|
|
}
|
|
|
|
subVisitedNewMap->insert(
|
|
{newTreeNodePair.shadowView.tag, &newTreeNodePair});
|
|
subVisitedOldMap->insert(
|
|
{oldTreeNodePair.shadowView.tag, &oldTreeNodePair});
|
|
} else {
|
|
// Node does not in exist in other tree.
|
|
if (treeChildPair.isConcreteView && !treeChildPair.inOtherTree()) {
|
|
auto deletionCreationIt =
|
|
deletionCreationCandidatePairs.find(treeChildPair.shadowView.tag);
|
|
if (deletionCreationIt == deletionCreationCandidatePairs.end()) {
|
|
deletionCreationCandidatePairs.insert(
|
|
{treeChildPair.shadowView.tag, &treeChildPair});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final step: go through creation/deletion candidates and delete/create
|
|
// subtrees if they were never visited during the execution of the above
|
|
// loop and recursions.
|
|
for (auto &deletionCreationCandidatePair : deletionCreationCandidatePairs) {
|
|
if (deletionCreationCandidatePair.first == 0) {
|
|
continue;
|
|
}
|
|
auto &treeChildPair = *deletionCreationCandidatePair.second;
|
|
|
|
// If node was visited during a flattening/unflattening recursion,
|
|
// and the node in the other tree is concrete, that means it was
|
|
// already created/deleted and we don't need to do that here.
|
|
// It is always the responsibility of the matcher to update subtrees when
|
|
// nodes are matched.
|
|
if (treeChildPair.inOtherTree()) {
|
|
continue;
|
|
}
|
|
|
|
if (reparentMode == ReparentMode::Flatten) {
|
|
mutationContainer.deleteMutations.push_back(
|
|
ShadowViewMutation::DeleteMutation(treeChildPair.shadowView));
|
|
|
|
if (!treeChildPair.flattened) {
|
|
ViewNodePairScope innerScope{};
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Recursively delete tree child pair (flatten case): " +
|
|
std::to_string(treeChildPair.shadowView.tag)),
|
|
innerScope,
|
|
mutationContainer.destructiveDownwardMutations,
|
|
treeChildPair.shadowView,
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
treeChildPair, innerScope),
|
|
{});
|
|
}
|
|
} else {
|
|
mutationContainer.createMutations.push_back(
|
|
ShadowViewMutation::CreateMutation(treeChildPair.shadowView));
|
|
|
|
if (!treeChildPair.flattened) {
|
|
ViewNodePairScope innerScope{};
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Recursively delete tree child pair (unflatten case): " +
|
|
std::to_string(treeChildPair.shadowView.tag)),
|
|
innerScope,
|
|
mutationContainer.downwardMutations,
|
|
treeChildPair.shadowView,
|
|
{},
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
treeChildPair, innerScope));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void calculateShadowViewMutationsV2(
|
|
BREADCRUMB_TYPE breadcrumb,
|
|
ViewNodePairScope &scope,
|
|
ShadowViewMutation::List &mutations,
|
|
ShadowView const &parentShadowView,
|
|
ShadowViewNodePair::NonOwningList &&oldChildPairs,
|
|
ShadowViewNodePair::NonOwningList &&newChildPairs,
|
|
bool isRecursionRedundant) {
|
|
SystraceSection s("Differentiator::calculateShadowViewMutationsV2");
|
|
if (oldChildPairs.empty() && newChildPairs.empty()) {
|
|
return;
|
|
}
|
|
|
|
size_t index = 0;
|
|
|
|
// Lists of mutations
|
|
auto mutationContainer = OrderedMutationInstructionContainer{};
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Entry: Child Pairs of node: [" << parentShadowView.tag
|
|
<< "]";
|
|
std::string strOldChildPairs;
|
|
for (size_t oldIndex = 0; oldIndex < oldChildPairs.size(); oldIndex++) {
|
|
strOldChildPairs.append(
|
|
std::to_string(oldChildPairs[oldIndex]->shadowView.tag));
|
|
strOldChildPairs.append(
|
|
oldChildPairs[oldIndex]->isConcreteView ? "" : "'");
|
|
strOldChildPairs.append(oldChildPairs[oldIndex]->flattened ? "*" : "");
|
|
strOldChildPairs.append(", ");
|
|
}
|
|
std::string strNewChildPairs;
|
|
for (size_t newIndex = 0; newIndex < newChildPairs.size(); newIndex++) {
|
|
strNewChildPairs.append(
|
|
std::to_string(newChildPairs[newIndex]->shadowView.tag));
|
|
strNewChildPairs.append(
|
|
newChildPairs[newIndex]->isConcreteView ? "" : "'");
|
|
strNewChildPairs.append(newChildPairs[newIndex]->flattened ? "*" : "");
|
|
strNewChildPairs.append(", ");
|
|
}
|
|
LOG(ERROR) << "Differ Entry: Old Child Pairs: " << strOldChildPairs;
|
|
LOG(ERROR) << "Differ Entry: New Child Pairs: " << strNewChildPairs;
|
|
});
|
|
|
|
// Stage 1: Collecting `Update` mutations
|
|
for (index = 0; index < oldChildPairs.size() && index < newChildPairs.size();
|
|
index++) {
|
|
auto &oldChildPair = *oldChildPairs[index];
|
|
auto &newChildPair = *newChildPairs[index];
|
|
|
|
if (oldChildPair.shadowView.tag != newChildPair.shadowView.tag) {
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Branch 1.1: Tags Different: ["
|
|
<< oldChildPair.shadowView.tag << "] ["
|
|
<< newChildPair.shadowView.tag << "]"
|
|
<< " with parent: [" << parentShadowView.tag << "]";
|
|
});
|
|
|
|
// Totally different nodes, updating is impossible.
|
|
break;
|
|
}
|
|
|
|
// If either view was flattened, and that has changed this frame, don't
|
|
// try to update
|
|
if (oldChildPair.flattened != newChildPair.flattened ||
|
|
oldChildPair.isConcreteView != newChildPair.isConcreteView) {
|
|
break;
|
|
}
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Branch 1.2: Same tags, update and recurse: ["
|
|
<< oldChildPair.shadowView.tag << "]"
|
|
<< (oldChildPair.flattened ? " (flattened)" : "")
|
|
<< (oldChildPair.isConcreteView ? " (concrete)" : "") << "["
|
|
<< newChildPair.shadowView.tag << "]"
|
|
<< (newChildPair.flattened ? " (flattened)" : "")
|
|
<< (newChildPair.isConcreteView ? " (concrete)" : "")
|
|
<< " with parent: [" << parentShadowView.tag << "]";
|
|
});
|
|
|
|
if (newChildPair.isConcreteView &&
|
|
oldChildPair.shadowView != newChildPair.shadowView) {
|
|
mutationContainer.updateMutations.push_back(
|
|
ShadowViewMutation::UpdateMutation(
|
|
oldChildPair.shadowView, newChildPair.shadowView));
|
|
}
|
|
|
|
// Recursively update tree if ShadowNode pointers are not equal
|
|
if (!oldChildPair.flattened &&
|
|
oldChildPair.shadowNode != newChildPair.shadowNode) {
|
|
ViewNodePairScope innerScope{};
|
|
auto oldGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
oldChildPair, innerScope);
|
|
auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
newChildPair, innerScope);
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Stage 1: Recurse on " +
|
|
std::to_string(oldChildPair.shadowView.tag)),
|
|
innerScope,
|
|
*(newGrandChildPairs.size()
|
|
? &mutationContainer.downwardMutations
|
|
: &mutationContainer.destructiveDownwardMutations),
|
|
oldChildPair.shadowView,
|
|
std::move(oldGrandChildPairs),
|
|
std::move(newGrandChildPairs));
|
|
}
|
|
}
|
|
|
|
size_t lastIndexAfterFirstStage = index;
|
|
|
|
if (index == newChildPairs.size()) {
|
|
// We've reached the end of the new children. We can delete+remove the
|
|
// rest.
|
|
for (; index < oldChildPairs.size(); index++) {
|
|
auto const &oldChildPair = *oldChildPairs[index];
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Branch 2: Deleting Tag/Tree: ["
|
|
<< oldChildPair.shadowView.tag << "]"
|
|
<< " with parent: [" << parentShadowView.tag << "]";
|
|
});
|
|
|
|
if (!oldChildPair.isConcreteView) {
|
|
continue;
|
|
}
|
|
|
|
// If we take this path, technically the operations and recursion below
|
|
// are redundant. However, some parts of the Fabric ecosystem (namely, as
|
|
// of writing this, LayoutAnimations) rely heavily on getting /explicit/
|
|
// Remove/Delete instructions for every single node in the tree. Thus, we
|
|
// generate the "RemoveDeleteTree" instruction as well as all of the
|
|
// individual Remove/Delete operations below, but we mark those as
|
|
// redundant. The platform layer can then discard the unnecessary
|
|
// instructions. RemoveDeleteTreeMutation is a significant performance
|
|
// improvement but could be improved significantly by eliminating the need
|
|
// for any of the redundant instructions in the future.
|
|
if (ShadowViewMutation::PlatformSupportsRemoveDeleteTreeInstruction &&
|
|
!isRecursionRedundant) {
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveDeleteTreeMutation(
|
|
parentShadowView,
|
|
oldChildPair.shadowView,
|
|
static_cast<int>(oldChildPair.mountIndex)));
|
|
}
|
|
|
|
mutationContainer.deleteMutations.push_back(
|
|
ShadowViewMutation::DeleteMutation(
|
|
oldChildPair.shadowView,
|
|
isRecursionRedundant ||
|
|
ShadowViewMutation::
|
|
PlatformSupportsRemoveDeleteTreeInstruction));
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveMutation(
|
|
parentShadowView,
|
|
oldChildPair.shadowView,
|
|
static_cast<int>(oldChildPair.mountIndex),
|
|
isRecursionRedundant ||
|
|
ShadowViewMutation::
|
|
PlatformSupportsRemoveDeleteTreeInstruction));
|
|
|
|
// We also have to call the algorithm recursively to clean up the entire
|
|
// subtree starting from the removed view.
|
|
ViewNodePairScope innerScope{};
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Trivial delete " + std::to_string(oldChildPair.shadowView.tag)),
|
|
innerScope,
|
|
mutationContainer.destructiveDownwardMutations,
|
|
oldChildPair.shadowView,
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
oldChildPair, innerScope),
|
|
{},
|
|
ShadowViewMutation::PlatformSupportsRemoveDeleteTreeInstruction);
|
|
}
|
|
} else if (index == oldChildPairs.size()) {
|
|
// If we don't have any more existing children we can choose a fast path
|
|
// since the rest will all be create+insert.
|
|
for (; index < newChildPairs.size(); index++) {
|
|
auto const &newChildPair = *newChildPairs[index];
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Branch 3: Creating Tag/Tree: ["
|
|
<< newChildPair.shadowView.tag << "]"
|
|
<< " with parent: [" << parentShadowView.tag << "]";
|
|
});
|
|
|
|
if (!newChildPair.isConcreteView) {
|
|
continue;
|
|
}
|
|
|
|
mutationContainer.insertMutations.push_back(
|
|
ShadowViewMutation::InsertMutation(
|
|
parentShadowView,
|
|
newChildPair.shadowView,
|
|
static_cast<int>(newChildPair.mountIndex)));
|
|
mutationContainer.createMutations.push_back(
|
|
ShadowViewMutation::CreateMutation(newChildPair.shadowView));
|
|
|
|
ViewNodePairScope innerScope{};
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Trivial create " + std::to_string(newChildPair.shadowView.tag)),
|
|
innerScope,
|
|
mutationContainer.downwardMutations,
|
|
newChildPair.shadowView,
|
|
{},
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
newChildPair, innerScope));
|
|
}
|
|
} else {
|
|
// Collect map of tags in the new list
|
|
auto newRemainingPairs = TinyMap<Tag, ShadowViewNodePair *>{};
|
|
auto newInsertedPairs = TinyMap<Tag, ShadowViewNodePair *>{};
|
|
auto deletionCandidatePairs = TinyMap<Tag, ShadowViewNodePair const *>{};
|
|
for (; index < newChildPairs.size(); index++) {
|
|
auto &newChildPair = *newChildPairs[index];
|
|
newRemainingPairs.insert({newChildPair.shadowView.tag, &newChildPair});
|
|
}
|
|
|
|
// Walk through both lists at the same time
|
|
// We will perform updates, create+insert, remove+delete, remove+insert
|
|
// (move) here.
|
|
size_t oldIndex = lastIndexAfterFirstStage,
|
|
newIndex = lastIndexAfterFirstStage, newSize = newChildPairs.size(),
|
|
oldSize = oldChildPairs.size();
|
|
while (newIndex < newSize || oldIndex < oldSize) {
|
|
bool haveNewPair = newIndex < newSize;
|
|
bool haveOldPair = oldIndex < oldSize;
|
|
|
|
// Advance both pointers if pointing to the same element
|
|
if (haveNewPair && haveOldPair) {
|
|
auto const &oldChildPair = *oldChildPairs[oldIndex];
|
|
auto const &newChildPair = *newChildPairs[newIndex];
|
|
|
|
Tag newTag = newChildPair.shadowView.tag;
|
|
Tag oldTag = oldChildPair.shadowView.tag;
|
|
|
|
if (newTag == oldTag) {
|
|
DEBUG_LOGS({
|
|
LOG(ERROR) << "Differ Branch 5: Matched Tags at indices: "
|
|
<< oldIndex << " " << newIndex << ": ["
|
|
<< oldChildPair.shadowView.tag << "]"
|
|
<< (oldChildPair.flattened ? "(flattened)" : "")
|
|
<< (oldChildPair.isConcreteView ? "(concrete)" : "")
|
|
<< " [" << newChildPair.shadowView.tag << "]"
|
|
<< (newChildPair.flattened ? "(flattened)" : "")
|
|
<< (newChildPair.isConcreteView ? "(concrete)" : "")
|
|
<< " with parent: [" << parentShadowView.tag << "]";
|
|
});
|
|
|
|
updateMatchedPair(
|
|
DIFF_BREADCRUMB(
|
|
"Update Matched Pairs (1): " +
|
|
std::to_string(oldChildPair.shadowView.tag)),
|
|
mutationContainer,
|
|
true,
|
|
true,
|
|
parentShadowView,
|
|
oldChildPair,
|
|
newChildPair);
|
|
|
|
updateMatchedPairSubtrees(
|
|
DIFF_BREADCRUMB(
|
|
"Update Matched Pair Subtrees (1): " +
|
|
std::to_string(oldChildPair.shadowView.tag)),
|
|
scope,
|
|
mutationContainer,
|
|
newRemainingPairs,
|
|
oldChildPairs,
|
|
newChildPairs,
|
|
parentShadowView,
|
|
oldChildPair,
|
|
newChildPair);
|
|
|
|
newIndex++;
|
|
oldIndex++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// We have an old pair, but we either don't have any remaining new pairs
|
|
// or we have one but it's not matched up with the old pair
|
|
if (haveOldPair) {
|
|
auto const &oldChildPair = *oldChildPairs[oldIndex];
|
|
|
|
Tag oldTag = oldChildPair.shadowView.tag;
|
|
|
|
// Was oldTag already inserted? This indicates a reordering, not just
|
|
// a move. The new node has already been inserted, we just need to
|
|
// remove the node from its old position now, and update the node's
|
|
// subtree.
|
|
auto const insertedIt = newInsertedPairs.find(oldTag);
|
|
if (insertedIt != newInsertedPairs.end()) {
|
|
auto const &newChildPair = *insertedIt->second;
|
|
|
|
updateMatchedPair(
|
|
DIFF_BREADCRUMB(
|
|
"Update Matched Pairs (2): " +
|
|
std::to_string(oldChildPair.shadowView.tag)),
|
|
mutationContainer,
|
|
true,
|
|
false,
|
|
parentShadowView,
|
|
oldChildPair,
|
|
newChildPair);
|
|
|
|
updateMatchedPairSubtrees(
|
|
DIFF_BREADCRUMB(
|
|
"Update Matched Pair Subtrees (2): " +
|
|
std::to_string(oldChildPair.shadowView.tag)),
|
|
scope,
|
|
mutationContainer,
|
|
newRemainingPairs,
|
|
oldChildPairs,
|
|
newChildPairs,
|
|
parentShadowView,
|
|
oldChildPair,
|
|
newChildPair);
|
|
|
|
newInsertedPairs.erase(insertedIt);
|
|
oldIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Should we generate a delete+remove instruction for the old node?
|
|
// If there's an old node and it's not found in the "new" list, we
|
|
// generate remove+delete for this node and its subtree.
|
|
auto const newIt = newRemainingPairs.find(oldTag);
|
|
if (newIt == newRemainingPairs.end()) {
|
|
oldIndex++;
|
|
|
|
if (!oldChildPair.isConcreteView) {
|
|
continue;
|
|
}
|
|
|
|
// From here, we know the oldChildPair is concrete.
|
|
// We *probably* need to generate a REMOVE mutation (see edge-case
|
|
// notes below).
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR)
|
|
<< "Differ Branch 9: Removing tag that was not reinserted: "
|
|
<< oldIndex << ": [" << oldChildPair.shadowView.tag << "]"
|
|
<< (oldChildPair.flattened ? " (flattened)" : "")
|
|
<< (oldChildPair.isConcreteView ? " (concrete)" : "")
|
|
<< " with parent: [" << parentShadowView.tag << "] "
|
|
<< "node is in other tree? "
|
|
<< (oldChildPair.inOtherTree() ? "yes" : "no");
|
|
});
|
|
|
|
// Edge case: node is not found in `newRemainingPairs`, due to
|
|
// complex (un)flattening cases, but exists in other tree *and* is
|
|
// concrete.
|
|
if (oldChildPair.inOtherTree() &&
|
|
oldChildPair.otherTreePair->isConcreteView) {
|
|
ShadowView const &otherTreeView =
|
|
oldChildPair.otherTreePair->shadowView;
|
|
|
|
// Remove, but remove using the *new* node, since we know
|
|
// an UPDATE mutation from old -> new has been generated.
|
|
// Practically this shouldn't matter for most mounting layer
|
|
// implementations, but helps adhere to the invariant that
|
|
// for all mutation instructions, "oldViewShadowNode" == "current
|
|
// node on mounting layer / stubView".
|
|
// Here we do *not" need to generate a potential DELETE mutation
|
|
// because we know the view is concrete, and still in the new
|
|
// hierarchy.
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveMutation(
|
|
parentShadowView,
|
|
otherTreeView,
|
|
static_cast<int>(oldChildPair.mountIndex)));
|
|
continue;
|
|
}
|
|
|
|
mutationContainer.removeMutations.push_back(
|
|
ShadowViewMutation::RemoveMutation(
|
|
parentShadowView,
|
|
oldChildPair.shadowView,
|
|
static_cast<int>(oldChildPair.mountIndex)));
|
|
|
|
deletionCandidatePairs.insert(
|
|
{oldChildPair.shadowView.tag, &oldChildPair});
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// At this point, oldTag is -1 or is in the new list, and hasn't been
|
|
// inserted or matched yet. We're not sure yet if the new node is in the
|
|
// old list - generate an insert instruction for the new node.
|
|
auto &newChildPair = *newChildPairs[newIndex];
|
|
DEBUG_LOGS({
|
|
LOG(ERROR)
|
|
<< "Differ Branch 10: Inserting tag/tree that was not (yet?) removed from hierarchy: "
|
|
<< newIndex << "/" << newSize << ": ["
|
|
<< newChildPair.shadowView.tag << "]"
|
|
<< (newChildPair.flattened ? " (flattened)" : "")
|
|
<< (newChildPair.isConcreteView ? " (concrete)" : "")
|
|
<< " with parent: [" << parentShadowView.tag << "]";
|
|
});
|
|
if (newChildPair.isConcreteView) {
|
|
mutationContainer.insertMutations.push_back(
|
|
ShadowViewMutation::InsertMutation(
|
|
parentShadowView,
|
|
newChildPair.shadowView,
|
|
static_cast<int>(newChildPair.mountIndex)));
|
|
}
|
|
|
|
// `inOtherTree` is only set to true during flattening/unflattening of
|
|
// parent. If the parent isn't (un)flattened, this will always be
|
|
// `false`, even if the node is in the other (old) tree. In this case,
|
|
// we expect the node to be removed from `newInsertedPairs` when we
|
|
// later encounter it in this loop.
|
|
if (!newChildPair.inOtherTree()) {
|
|
newInsertedPairs.insert({newChildPair.shadowView.tag, &newChildPair});
|
|
}
|
|
|
|
newIndex++;
|
|
}
|
|
|
|
// Penultimate step: generate Delete instructions for entirely deleted
|
|
// subtrees/nodes. We do this here because we need to traverse the entire
|
|
// list to make sure that a node was not reparented into an unflattened
|
|
// node that occurs *after* it in the hierarchy, due to zIndex ordering.
|
|
for (auto &deletionCandidatePair : deletionCandidatePairs) {
|
|
if (deletionCandidatePair.first == 0) {
|
|
continue;
|
|
}
|
|
|
|
auto const &oldChildPair = *deletionCandidatePair.second;
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR)
|
|
<< "Differ Branch 11: Deleting tag/tree that was not in new hierarchy: "
|
|
<< "[" << oldChildPair.shadowView.tag << "]"
|
|
<< (oldChildPair.flattened ? "(flattened)" : "")
|
|
<< (oldChildPair.isConcreteView ? "(concrete)" : "")
|
|
<< (oldChildPair.inOtherTree() ? "(in other tree)" : "")
|
|
<< " with parent: [" << parentShadowView.tag << "] ##"
|
|
<< std::hash<ShadowView>{}(oldChildPair.shadowView);
|
|
});
|
|
|
|
// This can happen when the parent is unflattened
|
|
if (!oldChildPair.inOtherTree() && oldChildPair.isConcreteView) {
|
|
mutationContainer.deleteMutations.push_back(
|
|
ShadowViewMutation::DeleteMutation(oldChildPair.shadowView));
|
|
|
|
// We also have to call the algorithm recursively to clean up the
|
|
// entire subtree starting from the removed view.
|
|
ViewNodePairScope innerScope{};
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Non-trivial delete " +
|
|
std::to_string(oldChildPair.shadowView.tag)),
|
|
innerScope,
|
|
mutationContainer.destructiveDownwardMutations,
|
|
oldChildPair.shadowView,
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
oldChildPair, innerScope),
|
|
{});
|
|
}
|
|
}
|
|
|
|
// Final step: generate Create instructions for entirely new
|
|
// subtrees/nodes that are not the result of flattening or unflattening.
|
|
for (auto &newInsertedPair : newInsertedPairs) {
|
|
// Erased elements of a TinyMap will have a Tag/key of 0 - skip those
|
|
// These *should* be removed by the map; there are currently no KNOWN
|
|
// cases where TinyMap will do the wrong thing, but there are not yet
|
|
// any unit tests explicitly for TinyMap, so this is safer for now.
|
|
if (newInsertedPair.first == 0) {
|
|
continue;
|
|
}
|
|
|
|
auto const &newChildPair = *newInsertedPair.second;
|
|
|
|
DEBUG_LOGS({
|
|
LOG(ERROR)
|
|
<< "Differ Branch 12: Inserting tag/tree that was not in old hierarchy: "
|
|
<< "[" << newChildPair.shadowView.tag << "]"
|
|
<< (newChildPair.flattened ? "(flattened)" : "")
|
|
<< (newChildPair.isConcreteView ? "(concrete)" : "")
|
|
<< (newChildPair.inOtherTree() ? "(in other tree)" : "")
|
|
<< " with parent: [" << parentShadowView.tag << "]";
|
|
});
|
|
|
|
if (!newChildPair.isConcreteView) {
|
|
continue;
|
|
}
|
|
if (newChildPair.inOtherTree()) {
|
|
continue;
|
|
}
|
|
|
|
mutationContainer.createMutations.push_back(
|
|
ShadowViewMutation::CreateMutation(newChildPair.shadowView));
|
|
|
|
ViewNodePairScope innerScope{};
|
|
calculateShadowViewMutationsV2(
|
|
DIFF_BREADCRUMB(
|
|
"Non-trivial create " +
|
|
std::to_string(newChildPair.shadowView.tag)),
|
|
innerScope,
|
|
mutationContainer.downwardMutations,
|
|
newChildPair.shadowView,
|
|
{},
|
|
sliceChildShadowNodeViewPairsFromViewNodePair(
|
|
newChildPair, innerScope));
|
|
}
|
|
}
|
|
|
|
// All mutations in an optimal order:
|
|
std::move(
|
|
mutationContainer.destructiveDownwardMutations.begin(),
|
|
mutationContainer.destructiveDownwardMutations.end(),
|
|
std::back_inserter(mutations));
|
|
std::move(
|
|
mutationContainer.updateMutations.begin(),
|
|
mutationContainer.updateMutations.end(),
|
|
std::back_inserter(mutations));
|
|
std::move(
|
|
mutationContainer.removeMutations.rbegin(),
|
|
mutationContainer.removeMutations.rend(),
|
|
std::back_inserter(mutations));
|
|
std::move(
|
|
mutationContainer.deleteMutations.begin(),
|
|
mutationContainer.deleteMutations.end(),
|
|
std::back_inserter(mutations));
|
|
std::move(
|
|
mutationContainer.createMutations.begin(),
|
|
mutationContainer.createMutations.end(),
|
|
std::back_inserter(mutations));
|
|
std::move(
|
|
mutationContainer.downwardMutations.begin(),
|
|
mutationContainer.downwardMutations.end(),
|
|
std::back_inserter(mutations));
|
|
std::move(
|
|
mutationContainer.insertMutations.begin(),
|
|
mutationContainer.insertMutations.end(),
|
|
std::back_inserter(mutations));
|
|
}
|
|
|
|
/**
|
|
* Only used by unit tests currently.
|
|
*/
|
|
static void sliceChildShadowNodeViewPairsRecursivelyLegacy(
|
|
ShadowViewNodePair::OwningList &pairList,
|
|
Point layoutOffset,
|
|
ShadowNode const &shadowNode) {
|
|
for (auto const &sharedChildShadowNode : shadowNode.getChildren()) {
|
|
auto &childShadowNode = *sharedChildShadowNode;
|
|
|
|
#ifndef ANDROID
|
|
// Temporary disabled on Android because the mounting infrastructure
|
|
// is not fully ready yet.
|
|
if (childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) {
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
auto shadowView = ShadowView(childShadowNode);
|
|
auto origin = layoutOffset;
|
|
if (shadowView.layoutMetrics != EmptyLayoutMetrics) {
|
|
origin += shadowView.layoutMetrics.frame.origin;
|
|
shadowView.layoutMetrics.frame.origin += layoutOffset;
|
|
}
|
|
|
|
if (childShadowNode.getTraits().check(
|
|
ShadowNodeTraits::Trait::FormsStackingContext)) {
|
|
pairList.push_back({shadowView, &childShadowNode});
|
|
} else {
|
|
if (childShadowNode.getTraits().check(
|
|
ShadowNodeTraits::Trait::FormsView)) {
|
|
pairList.push_back({shadowView, &childShadowNode});
|
|
}
|
|
|
|
sliceChildShadowNodeViewPairsRecursivelyLegacy(
|
|
pairList, origin, childShadowNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Only used by unit tests currently.
|
|
*/
|
|
ShadowViewNodePair::OwningList sliceChildShadowNodeViewPairsLegacy(
|
|
ShadowNode const &shadowNode) {
|
|
auto pairList = ShadowViewNodePair::OwningList{};
|
|
|
|
if (!shadowNode.getTraits().check(
|
|
ShadowNodeTraits::Trait::FormsStackingContext) &&
|
|
shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView)) {
|
|
return pairList;
|
|
}
|
|
|
|
sliceChildShadowNodeViewPairsRecursivelyLegacy(pairList, {0, 0}, shadowNode);
|
|
|
|
return pairList;
|
|
}
|
|
|
|
ShadowViewMutation::List calculateShadowViewMutations(
|
|
ShadowNode const &oldRootShadowNode,
|
|
ShadowNode const &newRootShadowNode) {
|
|
SystraceSection s("calculateShadowViewMutations");
|
|
|
|
// Root shadow nodes must be belong the same family.
|
|
react_native_assert(
|
|
ShadowNode::sameFamily(oldRootShadowNode, newRootShadowNode));
|
|
|
|
// See explanation of scope in Differentiator.h.
|
|
ViewNodePairScope viewNodePairScope{};
|
|
ViewNodePairScope innerViewNodePairScope{};
|
|
|
|
auto mutations = ShadowViewMutation::List{};
|
|
mutations.reserve(256);
|
|
|
|
auto oldRootShadowView = ShadowView(oldRootShadowNode);
|
|
auto newRootShadowView = ShadowView(newRootShadowNode);
|
|
|
|
if (oldRootShadowView != newRootShadowView) {
|
|
mutations.push_back(ShadowViewMutation::UpdateMutation(
|
|
oldRootShadowView, newRootShadowView));
|
|
}
|
|
|
|
calculateShadowViewMutationsV2(
|
|
CREATE_DIFF_BREADCRUMB(oldRootShadowView.tag),
|
|
innerViewNodePairScope,
|
|
mutations,
|
|
ShadowView(oldRootShadowNode),
|
|
sliceChildShadowNodeViewPairsV2(oldRootShadowNode, viewNodePairScope),
|
|
sliceChildShadowNodeViewPairsV2(newRootShadowNode, viewNodePairScope));
|
|
|
|
return mutations;
|
|
}
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|