From 50a34bcd7f3f2cc1b7089c274bbc6ac3e669d404 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Fri, 27 Mar 2020 19:05:36 -0700 Subject: [PATCH] Optimize diff algorithm to produce fewer remove+insert ("move") paired instructions Summary: An evolution of D20633188 but more performant. There are three optimized paths before the slow path. The first optimized path tries to pair identical nodes from old/new tree, and generate Update mutations, until we hit nodes that are different (indicating either a remove or an insert). This already existed. The next two optimizations, introduced by Tim in his JS pseudocode, were inspired by ReactJS's diffing algorithm. They work in cases where the rest of the nodes are (1) all removals/deletes or (2) all creates+inserts. Finally, if those final two optimized paths can't run, it's because there is a mix of delete+remove, create+insert, and "move" operations, mixed at the beginning, middle, and/or end of the list. This has slightly better average/best-case complexity as the previous implementation. In particularly pathological cases where all nodes are arbitrarily reordered, or reversed, for instance (ABCDE->EDCBA) the algorithm has the same complexity as the previous algorithm (quadratic). For now iOS is pinned to the older differ Changelog: [Internal] Experiment to optimize diffing algorithm in Fabric Reviewed By: shergin Differential Revision: D20684094 fbshipit-source-id: d29fba95a0328156c023e1c87804f23770ee1d91 --- React/Fabric/Mounting/RCTMountingManager.mm | 2 +- .../com/facebook/react/fabric/jni/Binding.cpp | 7 +- .../com/facebook/react/fabric/jni/Binding.h | 1 + .../fabric/mounting/Differentiator.cpp | 309 +++++++++++++++++- ReactCommon/fabric/mounting/Differentiator.h | 3 + .../fabric/mounting/MountingCoordinator.cpp | 9 +- .../fabric/mounting/MountingCoordinator.h | 4 +- 7 files changed, 317 insertions(+), 18 deletions(-) diff --git a/React/Fabric/Mounting/RCTMountingManager.mm b/React/Fabric/Mounting/RCTMountingManager.mm index f0c690cd6df..712d735b340 100644 --- a/React/Fabric/Mounting/RCTMountingManager.mm +++ b/React/Fabric/Mounting/RCTMountingManager.mm @@ -266,7 +266,7 @@ static void RNPerformMountInstructions( SystraceSection s("-[RCTMountingManager performTransaction:]"); RCTAssertMainQueue(); - auto transaction = mountingCoordinator->pullTransaction(); + auto transaction = mountingCoordinator->pullTransaction(DifferentiatorMode::Classic); if (!transaction.has_value()) { return; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp index c40737e19b4..daf70b9afb6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp @@ -261,6 +261,9 @@ void Binding::installFabricUIManager( disablePreallocateViews_ = reactNativeConfig_->getBool( "react_fabric:disabled_view_preallocation_android"); + enableOptimizedMovesDiffer_ = reactNativeConfig_->getBool( + "react_fabric:enabled_optimized_moves_differ_android"); + auto toolbox = SchedulerToolbox{}; toolbox.contextContainer = contextContainer; toolbox.componentRegistryFactory = componentsRegistry->buildRegistryFunction; @@ -570,7 +573,9 @@ void Binding::schedulerDidFinishTransaction( return; } - auto mountingTransaction = mountingCoordinator->pullTransaction(); + auto mountingTransaction = mountingCoordinator->pullTransaction( + enableOptimizedMovesDiffer_ ? DifferentiatorMode::OptimizedMoves + : DifferentiatorMode::Classic); if (!mountingTransaction.has_value()) { return; diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h index 612e37b3420..ddd87cd11f4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h @@ -108,6 +108,7 @@ class Binding : public jni::HybridClass, public SchedulerDelegate { bool collapseDeleteCreateMountingInstructions_{false}; bool disablePreallocateViews_{false}; bool disableVirtualNodePreallocation_{false}; + bool enableOptimizedMovesDiffer_{false}; }; } // namespace react diff --git a/ReactCommon/fabric/mounting/Differentiator.cpp b/ReactCommon/fabric/mounting/Differentiator.cpp index 7b9bf63af1a..0ff94162c9f 100644 --- a/ReactCommon/fabric/mounting/Differentiator.cpp +++ b/ReactCommon/fabric/mounting/Differentiator.cpp @@ -183,12 +183,12 @@ static_assert( std::is_move_assignable::value, "`ShadowViewNodePair::List` must be `move assignable`."); -static void calculateShadowViewMutations( +static void calculateShadowViewMutationsClassic( ShadowViewMutation::List &mutations, ShadowView const &parentShadowView, ShadowViewNodePair::List &&oldChildPairs, ShadowViewNodePair::List &&newChildPairs) { - // The current version of the algorithm is optimized for simplicity, + // This version of the algorithm is optimized for simplicity, // not for performance or optimal result. if (oldChildPairs.size() == 0 && newChildPairs.size() == 0) { @@ -236,7 +236,7 @@ static void calculateShadowViewMutations( sliceChildShadowNodeViewPairs(*oldChildPair.shadowNode); auto newGrandChildPairs = sliceChildShadowNodeViewPairs(*newChildPair.shadowNode); - calculateShadowViewMutations( + calculateShadowViewMutationsClassic( *(newGrandChildPairs.size() ? &downwardMutations : &destructiveDownwardMutations), oldChildPair.shadowView, @@ -277,7 +277,7 @@ static void calculateShadowViewMutations( // We also have to call the algorithm recursively to clean up the entire // subtree starting from the removed view. - calculateShadowViewMutations( + calculateShadowViewMutationsClassic( destructiveDownwardMutations, oldChildPair.shadowView, sliceChildShadowNodeViewPairs(*oldChildPair.shadowNode), @@ -293,7 +293,7 @@ static void calculateShadowViewMutations( sliceChildShadowNodeViewPairs(*oldChildPair.shadowNode); auto newGrandChildPairs = sliceChildShadowNodeViewPairs(*newChildPair.shadowNode); - calculateShadowViewMutations( + calculateShadowViewMutationsClassic( *(newGrandChildPairs.size() ? &downwardMutations : &destructiveDownwardMutations), newChildPair.shadowView, @@ -323,7 +323,7 @@ static void calculateShadowViewMutations( createMutations.push_back( ShadowViewMutation::CreateMutation(newChildPair.shadowView)); - calculateShadowViewMutations( + calculateShadowViewMutationsClassic( downwardMutations, newChildPair.shadowView, {}, @@ -361,7 +361,286 @@ static void calculateShadowViewMutations( std::back_inserter(mutations)); } +static void calculateShadowViewMutationsOptimizedMoves( + ShadowViewMutation::List &mutations, + ShadowView const &parentShadowView, + ShadowViewNodePair::List &&oldChildPairs, + ShadowViewNodePair::List &&newChildPairs) { + if (oldChildPairs.size() == 0 && newChildPairs.size() == 0) { + return; + } + + // Sorting pairs based on `orderIndex` if needed. + reorderInPlaceIfNeeded(oldChildPairs); + reorderInPlaceIfNeeded(newChildPairs); + + auto index = int{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{}; + + // Stage 1: Collecting `Update` mutations + for (index = 0; index < oldChildPairs.size() && index < newChildPairs.size(); + index++) { + auto const &oldChildPair = oldChildPairs[index]; + auto const &newChildPair = newChildPairs[index]; + + if (oldChildPair.shadowView.tag != newChildPair.shadowView.tag) { + // Totally different nodes, updating is impossible. + break; + } + + if (oldChildPair.shadowView != newChildPair.shadowView) { + updateMutations.push_back(ShadowViewMutation::UpdateMutation( + parentShadowView, + oldChildPair.shadowView, + newChildPair.shadowView, + index)); + } + + auto oldGrandChildPairs = + sliceChildShadowNodeViewPairs(*oldChildPair.shadowNode); + auto newGrandChildPairs = + sliceChildShadowNodeViewPairs(*newChildPair.shadowNode); + calculateShadowViewMutationsOptimizedMoves( + *(newGrandChildPairs.size() ? &downwardMutations + : &destructiveDownwardMutations), + oldChildPair.shadowView, + std::move(oldGrandChildPairs), + std::move(newGrandChildPairs)); + } + + int 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]; + + deleteMutations.push_back( + ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); + removeMutations.push_back(ShadowViewMutation::RemoveMutation( + parentShadowView, oldChildPair.shadowView, index)); + + // We also have to call the algorithm recursively to clean up the entire + // subtree starting from the removed view. + calculateShadowViewMutationsOptimizedMoves( + destructiveDownwardMutations, + oldChildPair.shadowView, + sliceChildShadowNodeViewPairs(*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]; + + insertMutations.push_back(ShadowViewMutation::InsertMutation( + parentShadowView, newChildPair.shadowView, index)); + createMutations.push_back( + ShadowViewMutation::CreateMutation(newChildPair.shadowView)); + + calculateShadowViewMutationsOptimizedMoves( + downwardMutations, + newChildPair.shadowView, + {}, + sliceChildShadowNodeViewPairs(*newChildPair.shadowNode)); + } + } else { + // Collect map of tags in the new list + // In the future it would be nice to use TinyMap for newInsertedPairs, but + // it's challenging to build an iterator that will work for our use-case + // here. + auto newRemainingPairs = TinyMap{}; + auto newInsertedPairs = better::map{}; + for (; index < newChildPairs.size(); index++) { + auto const &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. + int 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 &newChildPair = newChildPairs[newIndex]; + auto const &oldChildPair = oldChildPairs[oldIndex]; + + int newTag = newChildPair.shadowView.tag; + int oldTag = oldChildPair.shadowView.tag; + + if (newTag == oldTag) { + // Generate Update instructions + if (oldChildPair.shadowView != newChildPair.shadowView) { + updateMutations.push_back(ShadowViewMutation::UpdateMutation( + parentShadowView, + oldChildPair.shadowView, + newChildPair.shadowView, + index)); + } + + // Remove from newRemainingPairs + auto newRemainingPairIt = newRemainingPairs.find(oldTag); + if (newRemainingPairIt != newRemainingPairs.end()) { + newRemainingPairs.erase(newRemainingPairIt); + } + + // Update subtrees + auto oldGrandChildPairs = + sliceChildShadowNodeViewPairs(*oldChildPair.shadowNode); + auto newGrandChildPairs = + sliceChildShadowNodeViewPairs(*newChildPair.shadowNode); + calculateShadowViewMutationsOptimizedMoves( + *(newGrandChildPairs.size() ? &downwardMutations + : &destructiveDownwardMutations), + oldChildPair.shadowView, + std::move(oldGrandChildPairs), + std::move(newGrandChildPairs)); + + newIndex++; + oldIndex++; + continue; + } + } + + if (haveOldPair) { + auto const &oldChildPair = oldChildPairs[oldIndex]; + int 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. + auto const insertedIt = newInsertedPairs.find(oldTag); + if (insertedIt != newInsertedPairs.end()) { + removeMutations.push_back(ShadowViewMutation::RemoveMutation( + parentShadowView, oldChildPair.shadowView, oldIndex)); + + // Generate update instruction since we have an iterator ref to the + // new node + auto const &newChildPair = *insertedIt->second; + if (oldChildPair.shadowView != newChildPair.shadowView) { + updateMutations.push_back(ShadowViewMutation::UpdateMutation( + parentShadowView, + oldChildPair.shadowView, + newChildPair.shadowView, + index)); + } + + // Update subtrees + auto oldGrandChildPairs = + sliceChildShadowNodeViewPairs(*oldChildPair.shadowNode); + auto newGrandChildPairs = + sliceChildShadowNodeViewPairs(*newChildPair.shadowNode); + calculateShadowViewMutationsOptimizedMoves( + *(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()) { + removeMutations.push_back(ShadowViewMutation::RemoveMutation( + parentShadowView, oldChildPair.shadowView, oldIndex)); + 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. + calculateShadowViewMutationsOptimizedMoves( + destructiveDownwardMutations, + oldChildPair.shadowView, + sliceChildShadowNodeViewPairs(*oldChildPair.shadowNode), + {}); + + oldIndex++; + continue; + } else { + newRemainingPairs.erase(newIt); + } + } + + // 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 const &newChildPair = newChildPairs[newIndex]; + insertMutations.push_back(ShadowViewMutation::InsertMutation( + parentShadowView, newChildPair.shadowView, newIndex)); + newInsertedPairs.insert(std::pair( + newChildPair.shadowView.tag, &newChildPair)); + newIndex++; + } + + // Final step: generate Create instructions for new nodes + for (auto const &item : newInsertedPairs) { + auto const &newChildPair = *item.second; + createMutations.push_back( + ShadowViewMutation::CreateMutation(newChildPair.shadowView)); + + calculateShadowViewMutationsOptimizedMoves( + downwardMutations, + newChildPair.shadowView, + {}, + sliceChildShadowNodeViewPairs(*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)); +} + ShadowViewMutation::List calculateShadowViewMutations( + DifferentiatorMode differentiatorMode, ShadowNode const &oldRootShadowNode, ShadowNode const &newRootShadowNode) { SystraceSection s("calculateShadowViewMutations"); @@ -380,11 +659,19 @@ ShadowViewMutation::List calculateShadowViewMutations( ShadowView(), oldRootShadowView, newRootShadowView, -1)); } - calculateShadowViewMutations( - mutations, - ShadowView(oldRootShadowNode), - sliceChildShadowNodeViewPairs(oldRootShadowNode), - sliceChildShadowNodeViewPairs(newRootShadowNode)); + if (differentiatorMode == DifferentiatorMode::Classic) { + calculateShadowViewMutationsClassic( + mutations, + ShadowView(oldRootShadowNode), + sliceChildShadowNodeViewPairs(oldRootShadowNode), + sliceChildShadowNodeViewPairs(newRootShadowNode)); + } else { + calculateShadowViewMutationsOptimizedMoves( + mutations, + ShadowView(oldRootShadowNode), + sliceChildShadowNodeViewPairs(oldRootShadowNode), + sliceChildShadowNodeViewPairs(newRootShadowNode)); + } return mutations; } diff --git a/ReactCommon/fabric/mounting/Differentiator.h b/ReactCommon/fabric/mounting/Differentiator.h index 39727beec8d..96adb5b6ed5 100644 --- a/ReactCommon/fabric/mounting/Differentiator.h +++ b/ReactCommon/fabric/mounting/Differentiator.h @@ -13,12 +13,15 @@ namespace facebook { namespace react { +enum class DifferentiatorMode { Classic, OptimizedMoves }; + /* * Calculates a list of view mutations which describes how the old * `ShadowTree` can be transformed to the new one. * The list of mutations might be and might not be optimal. */ ShadowViewMutationList calculateShadowViewMutations( + DifferentiatorMode differentiatorMode, ShadowNode const &oldRootShadowNode, ShadowNode const &newRootShadowNode); diff --git a/ReactCommon/fabric/mounting/MountingCoordinator.cpp b/ReactCommon/fabric/mounting/MountingCoordinator.cpp index 9d36097c60b..396740c1fd9 100644 --- a/ReactCommon/fabric/mounting/MountingCoordinator.cpp +++ b/ReactCommon/fabric/mounting/MountingCoordinator.cpp @@ -14,7 +14,6 @@ #include -#include #include namespace facebook { @@ -67,8 +66,8 @@ bool MountingCoordinator::waitForTransaction( lock, timeout, [this]() { return lastRevision_.has_value(); }); } -better::optional MountingCoordinator::pullTransaction() - const { +better::optional MountingCoordinator::pullTransaction( + DifferentiatorMode differentiatorMode) const { std::lock_guard lock(mutex_); if (!lastRevision_.has_value()) { @@ -81,7 +80,9 @@ better::optional MountingCoordinator::pullTransaction() telemetry.willDiff(); auto mutations = calculateShadowViewMutations( - baseRevision_.getRootShadowNode(), lastRevision_->getRootShadowNode()); + differentiatorMode, + baseRevision_.getRootShadowNode(), + lastRevision_->getRootShadowNode()); telemetry.didDiff(); diff --git a/ReactCommon/fabric/mounting/MountingCoordinator.h b/ReactCommon/fabric/mounting/MountingCoordinator.h index aaac9185d12..3926f49618b 100644 --- a/ReactCommon/fabric/mounting/MountingCoordinator.h +++ b/ReactCommon/fabric/mounting/MountingCoordinator.h @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -51,7 +52,8 @@ class MountingCoordinator final { * However, a consumer should always call it on the same thread (e.g. on the * main thread) or ensure sequentiality of mount transactions separately. */ - better::optional pullTransaction() const; + better::optional pullTransaction( + DifferentiatorMode differentiatorMode) const; /* * Blocks the current thread until a new mounting transaction is available or