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