/* * Copyright (c) Facebook, Inc. and its 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 #include #include #include #include #include "ShadowView.h" // Uncomment this to enable verbose diffing logs, which can be useful for // debugging. // #define DEBUG_LOGS_DIFFER #ifdef DEBUG_LOGS_DIFFER #include #define DEBUG_LOGS(code) code #else #define DEBUG_LOGS(code) #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 class TinyMap final { public: using Pair = std::pair; 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(); 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) { 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; } better::small_vector vector_; int numErased_{0}; int 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::List &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 void sliceChildShadowNodeViewPairsRecursivelyV2( ShadowViewNodePair::List &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; } // 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 = childShadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView); bool areChildrenFlattened = !childShadowNode.getTraits().check( ShadowNodeTraits::Trait::FormsStackingContext); pairList.push_back( {shadowView, &childShadowNode, areChildrenFlattened, isConcreteView}); if (!childShadowNode.getTraits().check( ShadowNodeTraits::Trait::FormsStackingContext)) { sliceChildShadowNodeViewPairsRecursivelyV2( pairList, origin, childShadowNode); } } } ShadowViewNodePair::List sliceChildShadowNodeViewPairsV2( ShadowNode const &shadowNode, bool allowFlattened) { auto pairList = ShadowViewNodePair::List{}; if (!shadowNode.getTraits().check( ShadowNodeTraits::Trait::FormsStackingContext) && shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView) && !allowFlattened) { return pairList; } sliceChildShadowNodeViewPairsRecursivelyV2(pairList, {0, 0}, 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; } /* * 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::value, "`ShadowViewMutation` must be `move constructible`."); static_assert( std::is_move_constructible::value, "`ShadowView` must be `move constructible`."); static_assert( std::is_move_constructible::value, "`ShadowViewNodePair` must be `move constructible`."); static_assert( std::is_move_constructible::value, "`ShadowViewNodePair::List` must be `move constructible`."); static_assert( std::is_move_assignable::value, "`ShadowViewMutation` must be `move assignable`."); static_assert( std::is_move_assignable::value, "`ShadowView` must be `move assignable`."); static_assert( std::is_move_assignable::value, "`ShadowViewNodePair` must be `move assignable`."); static_assert( std::is_move_assignable::value, "`ShadowViewNodePair::List` must be `move assignable`."); // Forward declaration static void calculateShadowViewMutationsV2( ShadowViewMutation::List &mutations, ShadowView const &parentShadowView, ShadowViewNodePair::List &&oldChildPairs, ShadowViewNodePair::List &&newChildPairs); 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 calculateShadowViewMutationsFlattener( ReparentMode reparentMode, OrderedMutationInstructionContainer &mutationInstructionContainer, ShadowView const &parentShadowView, TinyMap &unvisitedFlattenedNodes, ShadowViewNodePair const &node, TinyMap *parentSubVisitedOtherNewNodes = nullptr, TinyMap *parentSubVisitedOtherOldNodes = nullptr); /** * 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( ReparentMode reparentMode, OrderedMutationInstructionContainer &mutationInstructionContainer, ShadowView const &parentShadowView, TinyMap &unvisitedOtherNodes, ShadowViewNodePair const &node, TinyMap *parentSubVisitedOtherNewNodes, TinyMap *parentSubVisitedOtherOldNodes) { DEBUG_LOGS({ LOG(ERROR) << "Differ Flattener 1: " << (reparentMode == ReparentMode::Unflatten ? "Unflattening" : "Flattening") << " [" << node.shadowView.tag << "]"; }); // Step 1: iterate through entire tree ShadowViewNodePair::List treeChildren = sliceChildShadowNodeViewPairsV2(*node.shadowNode); 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 subVisitedOtherNewNodes{}; TinyMap 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{}; for (size_t index = 0; index < treeChildren.size() && index < treeChildren.size(); index++) { // First, remove all children of the tree being flattened, or insert // children into parent tree if they're being unflattened. Then, look up // each node in the "unvisited" list and update the nodes and subtrees if // appropriate. auto &treeChildPair = treeChildren[index]; // Caller will take care of the corresponding action in the other tree. if (treeChildPair.isConcreteView) { if (reparentMode == ReparentMode::Flatten) { mutationInstructionContainer.removeMutations.push_back( ShadowViewMutation::RemoveMutation( node.shadowView, treeChildPair.shadowView, treeChildPair.mountIndex)); } else { mutationInstructionContainer.insertMutations.push_back( ShadowViewMutation::InsertMutation( node.shadowView, treeChildPair.shadowView, treeChildPair.mountIndex)); } } // 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() ? subVisitedOldMap->find(treeChildPair.shadowView.tag) : subVisitedOldMap->end()); // Find in other tree if (unvisitedIt != unvisitedOtherNodes.end() || subVisitedOtherNewIt != subVisitedNewMap->end() || subVisitedOtherOldIt != subVisitedOldMap->end()) { // 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; } auto &otherTreeNodePair = *(unvisitedIt != unvisitedOtherNodes.end() ? unvisitedIt->second : (subVisitedOtherNewIt != subVisitedNewMap->end() ? subVisitedOtherNewIt->second : subVisitedOtherOldIt->second)); // If we've already done updates, don't repeat it. if (treeChildPair.inOtherTree || otherTreeNodePair.inOtherTree) { continue; } auto &newTreeNodePair = (reparentMode == ReparentMode::Flatten ? otherTreeNodePair : treeChildPair); auto &oldTreeNodePair = (reparentMode == ReparentMode::Flatten ? treeChildPair : otherTreeNodePair); if (newTreeNodePair.shadowView != oldTreeNodePair.shadowView && newTreeNodePair.isConcreteView && oldTreeNodePair.isConcreteView) { mutationInstructionContainer.updateMutations.push_back( ShadowViewMutation::UpdateMutation( oldTreeNodePair.shadowView, newTreeNodePair.shadowView)); } // Update children if appropriate. if (!oldTreeNodePair.flattened && !newTreeNodePair.flattened) { if (oldTreeNodePair.shadowNode != newTreeNodePair.shadowNode) { calculateShadowViewMutationsV2( mutationInstructionContainer.downwardMutations, newTreeNodePair.shadowView, sliceChildShadowNodeViewPairsV2(*oldTreeNodePair.shadowNode), sliceChildShadowNodeViewPairsV2(*newTreeNodePair.shadowNode)); } } 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( childReparentMode, mutationInstructionContainer, (reparentMode == ReparentMode::Flatten ? parentShadowView : newTreeNodePair.shadowView), unvisitedOtherNodes, treeChildPair, subVisitedNewMap, subVisitedOldMap); } else { // Unflatten parent, flatten child if (childReparentMode == ReparentMode::Flatten) { // Construct unvisited nodes map auto unvisitedNewChildPairs = TinyMap{}; // Memory note: these oldFlattenedNodes all disappear at the end of // this "else" block, including any annotations we put on them. auto newFlattenedNodes = sliceChildShadowNodeViewPairsV2( *newTreeNodePair.shadowNode, true); for (size_t i = 0; i < newFlattenedNodes.size(); i++) { auto &newChild = newFlattenedNodes[i]; auto unvisitedOtherNodesIt = unvisitedOtherNodes.find(newChild.shadowView.tag); if (unvisitedOtherNodesIt != unvisitedOtherNodes.end()) { auto &unvisitedItPair = *unvisitedOtherNodesIt->second; unvisitedNewChildPairs.insert( {unvisitedItPair.shadowView.tag, &unvisitedItPair}); } else { unvisitedNewChildPairs.insert( {newChild.shadowView.tag, &newChild}); } } // 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( ReparentMode::Flatten, mutationInstructionContainer, (reparentMode == ReparentMode::Flatten ? parentShadowView : newTreeNodePair.shadowView), unvisitedNewChildPairs, oldTreeNodePair, subVisitedNewMap, subVisitedOldMap); for (auto &newFlattenedNode : newFlattenedNodes) { auto unvisitedOldChildPairIt = unvisitedNewChildPairs.find(newFlattenedNode.shadowView.tag); if (unvisitedOldChildPairIt == unvisitedNewChildPairs.end()) { // Node was visited. auto deleteCreateIt = deletionCreationCandidatePairs.find( newFlattenedNode.shadowView.tag); if (deleteCreateIt != deletionCreationCandidatePairs.end()) { deletionCreationCandidatePairs.erase(deleteCreateIt); } } } } // Flatten parent, unflatten child else { // Construct unvisited nodes map auto unvisitedOldChildPairs = TinyMap{}; // Memory note: these oldFlattenedNodes all disappear at the end of // this "else" block, including any annotations we put on them. auto oldFlattenedNodes = sliceChildShadowNodeViewPairsV2( *oldTreeNodePair.shadowNode, true); for (size_t i = 0; i < oldFlattenedNodes.size(); i++) { auto &oldChild = oldFlattenedNodes[i]; auto unvisitedOtherNodesIt = unvisitedOtherNodes.find(oldChild.shadowView.tag); if (unvisitedOtherNodesIt != unvisitedOtherNodes.end()) { auto &unvisitedItPair = *unvisitedOtherNodesIt->second; unvisitedOldChildPairs.insert( {unvisitedItPair.shadowView.tag, &unvisitedItPair}); } else { unvisitedOldChildPairs.insert( {oldChild.shadowView.tag, &oldChild}); } } // Unflatten old list into new tree calculateShadowViewMutationsFlattener( ReparentMode::Unflatten, mutationInstructionContainer, (reparentMode == ReparentMode::Flatten ? parentShadowView : newTreeNodePair.shadowView), unvisitedOldChildPairs, 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 &oldFlattenedNode : oldFlattenedNodes) { auto unvisitedOldChildPairIt = unvisitedOldChildPairs.find(oldFlattenedNode.shadowView.tag); if (unvisitedOldChildPairIt != unvisitedOldChildPairs.end()) { // Node unvisited - mark the entire subtree for deletion if (oldFlattenedNode.isConcreteView) { auto tag = oldFlattenedNode.shadowView.tag; auto oldRemainingChildInListIt = std::find_if( treeChildren.begin(), treeChildren.end(), [&tag](ShadowViewNodePair &nodePair) { return nodePair.shadowView.tag == tag; }); if (oldRemainingChildInListIt != treeChildren.end()) { auto deleteCreateIt = deletionCreationCandidatePairs.find( oldFlattenedNode.shadowView.tag); if (deleteCreateIt == deletionCreationCandidatePairs.end()) { deletionCreationCandidatePairs.insert( {tag, &*oldRemainingChildInListIt}); } } else { // TODO: we might want to remove this block. It seems // impossible to hit this logically (and empirically, after // testing on lots of randomized and pathologically // constructed trees) but I'm leaving this here out of an // abundance of caution. mutationInstructionContainer.deleteMutations.push_back( ShadowViewMutation::DeleteMutation( oldFlattenedNode.shadowView)); calculateShadowViewMutationsV2( mutationInstructionContainer .destructiveDownwardMutations, oldFlattenedNode.shadowView, sliceChildShadowNodeViewPairsV2( *oldFlattenedNode.shadowNode), {}); } } } 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); } // We also remove it from delete/creation candidates auto deleteCreateIt = deletionCreationCandidatePairs.find( oldFlattenedNode.shadowView.tag); if (deleteCreateIt != deletionCreationCandidatePairs.end()) { deletionCreationCandidatePairs.erase(deleteCreateIt); } } } } } } // 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 && !newTreeNodePair.inOtherTree) { if (newTreeNodePair.isConcreteView) { mutationInstructionContainer.createMutations.push_back( ShadowViewMutation::CreateMutation(newTreeNodePair.shadowView)); } else { mutationInstructionContainer.deleteMutations.push_back( ShadowViewMutation::DeleteMutation(newTreeNodePair.shadowView)); } } treeChildPair.inOtherTree = true; otherTreeNodePair.inOtherTree = true; if (parentSubVisitedOtherNewNodes != nullptr) { parentSubVisitedOtherNewNodes->insert( {newTreeNodePair.shadowView.tag, &newTreeNodePair}); } if (parentSubVisitedOtherOldNodes != nullptr) { parentSubVisitedOtherOldNodes->insert( {oldTreeNodePair.shadowView.tag, &oldTreeNodePair}); } if (unvisitedIt != unvisitedOtherNodes.end()) { unvisitedOtherNodes.erase(unvisitedIt); } } 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 it = deletionCreationCandidatePairs.begin(); it != deletionCreationCandidatePairs.end(); it++) { if (it->first == 0) { continue; } auto &treeChildPair = *it->second; // If node was visited during a flattening/unflattening recursion. if (treeChildPair.inOtherTree) { continue; } if (reparentMode == ReparentMode::Flatten) { mutationInstructionContainer.deleteMutations.push_back( ShadowViewMutation::DeleteMutation(treeChildPair.shadowView)); if (!treeChildPair.flattened) { calculateShadowViewMutationsV2( mutationInstructionContainer.destructiveDownwardMutations, treeChildPair.shadowView, sliceChildShadowNodeViewPairsV2(*treeChildPair.shadowNode), {}); } } else { mutationInstructionContainer.createMutations.push_back( ShadowViewMutation::CreateMutation(treeChildPair.shadowView)); if (!treeChildPair.flattened) { calculateShadowViewMutationsV2( mutationInstructionContainer.downwardMutations, treeChildPair.shadowView, {}, sliceChildShadowNodeViewPairsV2(*treeChildPair.shadowNode)); } } } } static void calculateShadowViewMutationsV2( ShadowViewMutation::List &mutations, ShadowView const &parentShadowView, ShadowViewNodePair::List &&oldChildPairs, ShadowViewNodePair::List &&newChildPairs) { if (oldChildPairs.empty() && newChildPairs.empty()) { return; } size_t index = 0; // Lists of mutations auto createMutations = ShadowViewMutation::List{}; auto deleteMutations = ShadowViewMutation::List{}; auto insertMutations = ShadowViewMutation::List{}; auto removeMutations = ShadowViewMutation::List{}; auto updateMutations = ShadowViewMutation::List{}; auto downwardMutations = ShadowViewMutation::List{}; auto destructiveDownwardMutations = ShadowViewMutation::List{}; auto mutationInstructionContainer = OrderedMutationInstructionContainer{createMutations, deleteMutations, insertMutations, removeMutations, updateMutations, downwardMutations, destructiveDownwardMutations}; 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) { 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) { auto oldGrandChildPairs = sliceChildShadowNodeViewPairsV2(*oldChildPair.shadowNode); auto newGrandChildPairs = sliceChildShadowNodeViewPairsV2(*newChildPair.shadowNode); calculateShadowViewMutationsV2( *(newGrandChildPairs.size() ? &downwardMutations : &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; } deleteMutations.push_back( ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); removeMutations.push_back(ShadowViewMutation::RemoveMutation( parentShadowView, oldChildPair.shadowView, oldChildPair.mountIndex)); // We also have to call the algorithm recursively to clean up the entire // subtree starting from the removed view. calculateShadowViewMutationsV2( destructiveDownwardMutations, oldChildPair.shadowView, sliceChildShadowNodeViewPairsV2(*oldChildPair.shadowNode), {}); } } 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; } insertMutations.push_back(ShadowViewMutation::InsertMutation( parentShadowView, newChildPair.shadowView, newChildPair.mountIndex)); createMutations.push_back( ShadowViewMutation::CreateMutation(newChildPair.shadowView)); calculateShadowViewMutationsV2( downwardMutations, newChildPair.shadowView, {}, sliceChildShadowNodeViewPairsV2(*newChildPair.shadowNode)); } } else { // Collect map of tags in the new list auto newRemainingPairs = TinyMap{}; auto newInsertedPairs = TinyMap{}; auto deletionCandidatePairs = TinyMap{}; 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 << "]"; }); // Check concrete-ness of views // Create/Delete and Insert/Remove if necessary if (oldChildPair.isConcreteView != newChildPair.isConcreteView) { if (newChildPair.isConcreteView) { insertMutations.push_back(ShadowViewMutation::InsertMutation( parentShadowView, newChildPair.shadowView, newChildPair.mountIndex)); createMutations.push_back( ShadowViewMutation::CreateMutation(newChildPair.shadowView)); } else { removeMutations.push_back(ShadowViewMutation::RemoveMutation( parentShadowView, oldChildPair.shadowView, oldChildPair.mountIndex)); deleteMutations.push_back( ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); } } else if ( oldChildPair.isConcreteView && newChildPair.isConcreteView) { // Even if node's children are flattened, it might still be a // concrete view. The case where they're different is handled above. if (oldChildPair.shadowView != newChildPair.shadowView) { updateMutations.push_back(ShadowViewMutation::UpdateMutation( oldChildPair.shadowView, newChildPair.shadowView)); } // Remove from newRemainingPairs auto newRemainingPairIt = newRemainingPairs.find(oldTag); if (newRemainingPairIt != newRemainingPairs.end()) { newRemainingPairs.erase(newRemainingPairIt); } } // Are we flattening or unflattening either one? If node was flattened // in both trees, there's no change, just continue. if (oldChildPair.flattened && newChildPair.flattened) { newIndex++; oldIndex++; continue; } // We are either flattening or unflattening this node. if (oldChildPair.flattened != newChildPair.flattened) { DEBUG_LOGS({ LOG(ERROR) << "Differ: flattening or unflattening at branch 6: [" << oldChildPair.shadowView.tag << "] [" << newChildPair.shadowView.tag << "] " << oldChildPair.flattened << " " << newChildPair.flattened << " with parent: [" << parentShadowView.tag << "]"; }); // Flattening if (!oldChildPair.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( ReparentMode::Flatten, mutationInstructionContainer, parentShadowView, newRemainingPairs, oldChildPair); } // Unflattening else { // Construct unvisited nodes map auto unvisitedOldChildPairs = TinyMap{}; // 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 = sliceChildShadowNodeViewPairsV2( *oldChildPair.shadowNode, 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( ReparentMode::Unflatten, mutationInstructionContainer, parentShadowView, unvisitedOldChildPairs, newChildPair); // 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 &oldFlattenedNode : oldFlattenedNodes) { 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); } } } } newIndex++; oldIndex++; continue; } // Update subtrees if View is not flattened, and if node addresses are // not equal if (oldChildPair.shadowNode != newChildPair.shadowNode) { auto oldGrandChildPairs = sliceChildShadowNodeViewPairsV2(*oldChildPair.shadowNode); auto newGrandChildPairs = sliceChildShadowNodeViewPairsV2(*newChildPair.shadowNode); calculateShadowViewMutationsV2( *(newGrandChildPairs.size() ? &downwardMutations : &destructiveDownwardMutations), oldChildPair.shadowView, std::move(oldGrandChildPairs), std::move(newGrandChildPairs)); } 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; // The node has been reordered and we are also flattening or // unflattening if (oldChildPair.flattened != newChildPair.flattened) { DEBUG_LOGS({ LOG(ERROR) << "Differ: branch 7: Flattening or unflattening already-inserted node upon remove (move/reorder operation)." << oldChildPair.shadowView.tag << " " << oldChildPair.flattened << " // " << newChildPair.shadowView.tag << " " << newChildPair.flattened; }); // Unflattening. // The node in question was already inserted and we are // *unflattening* it, so we just need to update the subtree nodes // and remove them from the view hierarchy. Any of the unvisited // nodes in the old tree will be deleted. // TODO: can we consolidate this code? It's identical to the first // block above. if (!oldChildPair.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( ReparentMode::Flatten, mutationInstructionContainer, parentShadowView, newRemainingPairs, oldChildPair); } // Unflattening else { // Construct unvisited nodes map auto unvisitedOldChildPairs = TinyMap{}; // 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 = sliceChildShadowNodeViewPairsV2( *oldChildPair.shadowNode, 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( ReparentMode::Unflatten, mutationInstructionContainer, parentShadowView, unvisitedOldChildPairs, newChildPair); // 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: delete recursively? create // recursively? for (auto &oldFlattenedNode : oldFlattenedNodes) { 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); } } } } } // Check concrete-ness of views // Create/Delete and Insert/Remove if necessary // TODO: document: Insert should already be handled by outermost loop, // but not Remove if (oldChildPair.isConcreteView != newChildPair.isConcreteView) { if (newChildPair.isConcreteView) { createMutations.push_back( ShadowViewMutation::CreateMutation(newChildPair.shadowView)); } else { removeMutations.push_back(ShadowViewMutation::RemoveMutation( parentShadowView, oldChildPair.shadowView, oldChildPair.mountIndex)); deleteMutations.push_back( ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); } } // We handled this case above. We fall through to check concreteness // of old/new view to remove/insert create/delete above, and then bail // out here. if (oldChildPair.flattened != newChildPair.flattened) { newInsertedPairs.erase(insertedIt); oldIndex++; continue; } // old and new child pairs are both either flattened or unflattened at // this point. If they're not views, we don't need to update subtrees. if (oldChildPair.isConcreteView) { // TODO: do we always want to remove here? There are cases where we // might be able to remove this to prevent unnecessary // removes/inserts in cases of (un)flattening + reorders? removeMutations.push_back(ShadowViewMutation::RemoveMutation( parentShadowView, oldChildPair.shadowView, oldChildPair.mountIndex)); if (oldChildPair.shadowView != newChildPair.shadowView) { updateMutations.push_back(ShadowViewMutation::UpdateMutation( oldChildPair.shadowView, newChildPair.shadowView)); } } if (!oldChildPair.flattened && oldChildPair.shadowNode != newChildPair.shadowNode) { // Update subtrees auto oldGrandChildPairs = sliceChildShadowNodeViewPairsV2(*oldChildPair.shadowNode); auto newGrandChildPairs = sliceChildShadowNodeViewPairsV2(*newChildPair.shadowNode); calculateShadowViewMutationsV2( *(newGrandChildPairs.size() ? &downwardMutations : &destructiveDownwardMutations), oldChildPair.shadowView, std::move(oldGrandChildPairs), std::move(newGrandChildPairs)); } 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()) { 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 << "]"; }); if (oldChildPair.isConcreteView) { removeMutations.push_back(ShadowViewMutation::RemoveMutation( parentShadowView, oldChildPair.shadowView, oldChildPair.mountIndex)); deletionCandidatePairs.insert( {oldChildPair.shadowView.tag, &oldChildPair}); } oldIndex++; 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) { insertMutations.push_back(ShadowViewMutation::InsertMutation( parentShadowView, newChildPair.shadowView, newChildPair.mountIndex)); } 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 it = deletionCandidatePairs.begin(); it != deletionCandidatePairs.end(); it++) { if (it->first == 0) { continue; } auto const &oldChildPair = *it->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 << "]"; }); // This can happen when the parent is unflattened if (!oldChildPair.inOtherTree) { 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. calculateShadowViewMutationsV2( destructiveDownwardMutations, oldChildPair.shadowView, sliceChildShadowNodeViewPairsV2(*oldChildPair.shadowNode), {}); } } // Final step: generate Create instructions for entirely new subtrees/nodes // that are not the result of flattening or unflattening. for (auto it = newInsertedPairs.begin(); it != newInsertedPairs.end(); it++) { // 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 (it->first == 0) { continue; } auto const &newChildPair = *it->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; } createMutations.push_back( ShadowViewMutation::CreateMutation(newChildPair.shadowView)); calculateShadowViewMutationsV2( downwardMutations, newChildPair.shadowView, {}, sliceChildShadowNodeViewPairsV2(*newChildPair.shadowNode)); } } // All mutations in an optimal order: std::move( destructiveDownwardMutations.begin(), destructiveDownwardMutations.end(), std::back_inserter(mutations)); std::move( updateMutations.begin(), updateMutations.end(), std::back_inserter(mutations)); std::move( removeMutations.rbegin(), removeMutations.rend(), std::back_inserter(mutations)); std::move( deleteMutations.begin(), deleteMutations.end(), std::back_inserter(mutations)); std::move( createMutations.begin(), createMutations.end(), std::back_inserter(mutations)); std::move( downwardMutations.begin(), downwardMutations.end(), std::back_inserter(mutations)); std::move( insertMutations.begin(), insertMutations.end(), std::back_inserter(mutations)); } /** * Only used by unit tests currently. */ static void sliceChildShadowNodeViewPairsRecursivelyLegacy( ShadowViewNodePair::List &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::List sliceChildShadowNodeViewPairsLegacy( ShadowNode const &shadowNode) { auto pairList = ShadowViewNodePair::List{}; 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. assert(ShadowNode::sameFamily(oldRootShadowNode, newRootShadowNode)); 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( mutations, ShadowView(oldRootShadowNode), sliceChildShadowNodeViewPairsV2(oldRootShadowNode), sliceChildShadowNodeViewPairsV2(newRootShadowNode)); return mutations; } } // namespace react } // namespace facebook