diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js index 10930caf2f98..a0bbcb74c69b 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @fantom_flags useLISAlgorithmInDifferentiator:* * @flow strict-local * @format */ @@ -173,4 +174,98 @@ Fantom.unstable_benchmark root.destroy(); }, }), + ) + .test.each( + [10, 50, 100], + n => `reorder ${n.toString()} children (move first to last)`, + () => { + Fantom.runTask(() => root.render(testViews)); + }, + n => { + let original: React.MixedElement; + let reordered: React.MixedElement; + return { + beforeAll: () => { + const children = []; + for (let i = 0; i < n; i++) { + children.push( + , + ); + } + original = ( + + {children} + + ); + // Move first child to last + const reorderedChildren = [...children.slice(1), children[0]]; + reordered = ( + + {reorderedChildren} + + ); + }, + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => root.render(original)); + // $FlowExpectedError[incompatible-type] + testViews = reordered; + }, + afterEach: () => { + root.destroy(); + }, + }; + }, + ) + .test.each( + [10, 50, 100], + n => `reorder ${n.toString()} children (swap first two)`, + () => { + Fantom.runTask(() => root.render(testViews)); + }, + n => { + let original: React.MixedElement; + let reordered: React.MixedElement; + return { + beforeAll: () => { + const children = []; + for (let i = 0; i < n; i++) { + children.push( + , + ); + } + original = ( + + {children} + + ); + // Swap first two children — both algorithms handle this equally + const swapped = [children[1], children[0], ...children.slice(2)]; + reordered = ( + + {swapped} + + ); + }, + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => root.render(original)); + // $FlowExpectedError[incompatible-type] + testViews = reordered; + }, + afterEach: () => { + root.destroy(); + }, + }; + }, ); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7be4cb067bba..d49efd1aadc7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5144fb0350b71394206d614c68ef87f0>> + * @generated SignedSource<<61964fd9ddf11ed5c2848da3f4d0b490>> */ /** @@ -510,6 +510,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun useFabricInterop(): Boolean = accessor.useFabricInterop() + /** + * Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation. + */ + @JvmStatic + public fun useLISAlgorithmInDifferentiator(): Boolean = accessor.useLISAlgorithmInDifferentiator() + /** * When enabled, the native view configs are used in bridgeless mode. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index a77a2ff90fe9..fc7fc079ed5f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<4ce2605ff71e60b6096a211ff902e994>> */ /** @@ -100,6 +100,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null private var useFabricInteropCache: Boolean? = null + private var useLISAlgorithmInDifferentiatorCache: Boolean? = null private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null private var useNestedScrollViewAndroidCache: Boolean? = null private var useSharedAnimatedBackendCache: Boolean? = null @@ -831,6 +832,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun useLISAlgorithmInDifferentiator(): Boolean { + var cached = useLISAlgorithmInDifferentiatorCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.useLISAlgorithmInDifferentiator() + useLISAlgorithmInDifferentiatorCache = cached + } + return cached + } + override fun useNativeViewConfigsInBridgelessMode(): Boolean { var cached = useNativeViewConfigsInBridgelessModeCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index fa8758c0901b..e3ffd419a8e8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<5f3573c9983cb54c5df527a3053ebbae>> */ /** @@ -188,6 +188,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun useFabricInterop(): Boolean + @DoNotStrip @JvmStatic public external fun useLISAlgorithmInDifferentiator(): Boolean + @DoNotStrip @JvmStatic public external fun useNativeViewConfigsInBridgelessMode(): Boolean @DoNotStrip @JvmStatic public external fun useNestedScrollViewAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index e467f8cd7327..e8791ff06299 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<67d638f79b7b06a087f63563c2e5ff95>> */ /** @@ -183,6 +183,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun useFabricInterop(): Boolean = true + override fun useLISAlgorithmInDifferentiator(): Boolean = false + override fun useNativeViewConfigsInBridgelessMode(): Boolean = false override fun useNestedScrollViewAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 99d211c64a30..6ae836432104 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7b87f5541ecf881d8ce51c5edd5b99b0>> + * @generated SignedSource<> */ /** @@ -104,6 +104,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null private var useFabricInteropCache: Boolean? = null + private var useLISAlgorithmInDifferentiatorCache: Boolean? = null private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null private var useNestedScrollViewAndroidCache: Boolean? = null private var useSharedAnimatedBackendCache: Boolean? = null @@ -915,6 +916,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun useLISAlgorithmInDifferentiator(): Boolean { + var cached = useLISAlgorithmInDifferentiatorCache + if (cached == null) { + cached = currentProvider.useLISAlgorithmInDifferentiator() + accessedFeatureFlags.add("useLISAlgorithmInDifferentiator") + useLISAlgorithmInDifferentiatorCache = cached + } + return cached + } + override fun useNativeViewConfigsInBridgelessMode(): Boolean { var cached = useNativeViewConfigsInBridgelessModeCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index de1d05f86ef3..a952c252dbdb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<540700c0c0f3259a093a98ad639478ba>> */ /** @@ -183,6 +183,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun useFabricInterop(): Boolean + @DoNotStrip public fun useLISAlgorithmInDifferentiator(): Boolean + @DoNotStrip public fun useNativeViewConfigsInBridgelessMode(): Boolean @DoNotStrip public fun useNestedScrollViewAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index fe397f8b1e41..0bedbceeee25 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<6c088ccf18868fc6e54d83c7483b6607>> */ /** @@ -519,6 +519,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool useLISAlgorithmInDifferentiator() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useLISAlgorithmInDifferentiator"); + return method(javaProvider_); + } + bool useNativeViewConfigsInBridgelessMode() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useNativeViewConfigsInBridgelessMode"); @@ -983,6 +989,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useFabricInterop( return ReactNativeFeatureFlags::useFabricInterop(); } +bool JReactNativeFeatureFlagsCxxInterop::useLISAlgorithmInDifferentiator( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator(); +} + bool JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode(); @@ -1304,6 +1315,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "useFabricInterop", JReactNativeFeatureFlagsCxxInterop::useFabricInterop), + makeNativeMethod( + "useLISAlgorithmInDifferentiator", + JReactNativeFeatureFlagsCxxInterop::useLISAlgorithmInDifferentiator), makeNativeMethod( "useNativeViewConfigsInBridgelessMode", JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 08276eab5edd..e4b4e467fdd7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5433b4a2f4a0574591a38017422edac8>> + * @generated SignedSource<<73cfe749b34b786e25b683c499889e48>> */ /** @@ -270,6 +270,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool useFabricInterop( facebook::jni::alias_ref); + static bool useLISAlgorithmInDifferentiator( + facebook::jni::alias_ref); + static bool useNativeViewConfigsInBridgelessMode( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 4f058038abbd..2a0bc39e528d 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -346,6 +346,10 @@ bool ReactNativeFeatureFlags::useFabricInterop() { return getAccessor().useFabricInterop(); } +bool ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator() { + return getAccessor().useLISAlgorithmInDifferentiator(); +} + bool ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode() { return getAccessor().useNativeViewConfigsInBridgelessMode(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 7185d625c251..9b24d7eec3b1 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4811a81c7839f2be5c8a127e6c8e310b>> + * @generated SignedSource<<6c175f21aaa8d084c7b0be0625d5d77d>> */ /** @@ -439,6 +439,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool useFabricInterop(); + /** + * Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation. + */ + RN_EXPORT static bool useLISAlgorithmInDifferentiator(); + /** * When enabled, the native view configs are used in bridgeless mode. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 936e3c590d50..3b529d3f83bf 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -1469,6 +1469,24 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::useLISAlgorithmInDifferentiator() { + auto flagValue = useLISAlgorithmInDifferentiator_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(80, "useLISAlgorithmInDifferentiator"); + + flagValue = currentProvider_->useLISAlgorithmInDifferentiator(); + useLISAlgorithmInDifferentiator_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { auto flagValue = useNativeViewConfigsInBridgelessMode_.load(); @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(81, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useNestedScrollViewAndroid"); + markFlagAsAccessed(82, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "useSharedAnimatedBackend"); + markFlagAsAccessed(83, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(84, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "useTurboModuleInterop"); + markFlagAsAccessed(85, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useTurboModules"); + markFlagAsAccessed(86, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useUnorderedMapInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useUnorderedMapInDifferentiator"); + markFlagAsAccessed(87, "useUnorderedMapInDifferentiator"); flagValue = currentProvider_->useUnorderedMapInDifferentiator(); useUnorderedMapInDifferentiator_ = flagValue; @@ -1604,7 +1622,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "viewCullingOutsetRatio"); + markFlagAsAccessed(88, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "viewTransitionEnabled"); + markFlagAsAccessed(89, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1640,7 +1658,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "virtualViewPrerenderRatio"); + markFlagAsAccessed(90, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index e76f1322f58d..b572c9d7b944 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<06dba77b9b76d06d5c338ed8a97e33f5>> + * @generated SignedSource<<29e7a9ef1807aaecaf5440e024a5a2f2>> */ /** @@ -112,6 +112,7 @@ class ReactNativeFeatureFlagsAccessor { bool updateRuntimeShadowNodeReferencesOnCommitThread(); bool useAlwaysAvailableJSErrorHandling(); bool useFabricInterop(); + bool useLISAlgorithmInDifferentiator(); bool useNativeViewConfigsInBridgelessMode(); bool useNestedScrollViewAndroid(); bool useSharedAnimatedBackend(); @@ -133,7 +134,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 90> accessedFeatureFlags_; + std::array, 91> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -215,6 +216,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> updateRuntimeShadowNodeReferencesOnCommitThread_; std::atomic> useAlwaysAvailableJSErrorHandling_; std::atomic> useFabricInterop_; + std::atomic> useLISAlgorithmInDifferentiator_; std::atomic> useNativeViewConfigsInBridgelessMode_; std::atomic> useNestedScrollViewAndroid_; std::atomic> useSharedAnimatedBackend_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 1a6289888d15..c64f1748335b 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0934d867533630904fc69e30e7a929b3>> + * @generated SignedSource<<2cf1c7be6b0086da159550454273ce2d>> */ /** @@ -347,6 +347,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return true; } + bool useLISAlgorithmInDifferentiator() override { + return false; + } + bool useNativeViewConfigsInBridgelessMode() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 5c939775920a..141d4c5f2dbc 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<25d1f9cb509dbd8274e3a00237d2ea62>> + * @generated SignedSource<<20a808bb8708d8088f3d7aae8d58b6b2>> */ /** @@ -765,6 +765,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::useFabricInterop(); } + bool useLISAlgorithmInDifferentiator() override { + auto value = values_["useLISAlgorithmInDifferentiator"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useLISAlgorithmInDifferentiator(); + } + bool useNativeViewConfigsInBridgelessMode() override { auto value = values_["useNativeViewConfigsInBridgelessMode"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index cc3fbc19b88d..42c8c97942d5 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<8c52d3da48fbc27e8e23493f81a93d55>> */ /** @@ -105,6 +105,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool updateRuntimeShadowNodeReferencesOnCommitThread() = 0; virtual bool useAlwaysAvailableJSErrorHandling() = 0; virtual bool useFabricInterop() = 0; + virtual bool useLISAlgorithmInDifferentiator() = 0; virtual bool useNativeViewConfigsInBridgelessMode() = 0; virtual bool useNestedScrollViewAndroid() = 0; virtual bool useSharedAnimatedBackend() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 1fbd4198e5d2..54f33abe3339 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0b3534a570416860aa1ffc7e1d808090>> + * @generated SignedSource<<51f57b07e238ca9c13f4c7ec363eb9a0>> */ /** @@ -444,6 +444,11 @@ bool NativeReactNativeFeatureFlags::useFabricInterop( return ReactNativeFeatureFlags::useFabricInterop(); } +bool NativeReactNativeFeatureFlags::useLISAlgorithmInDifferentiator( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator(); +} + bool NativeReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 081db38ca2fd..18ab3a19fd2f 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0aeea7c4fa2a8aa4180c83bbd0746250>> + * @generated SignedSource<<8368d761c6a95fcdbf804681a2bf65d3>> */ /** @@ -196,6 +196,8 @@ class NativeReactNativeFeatureFlags bool useFabricInterop(jsi::Runtime& runtime); + bool useLISAlgorithmInDifferentiator(jsi::Runtime& runtime); + bool useNativeViewConfigsInBridgelessMode(jsi::Runtime& runtime); bool useNestedScrollViewAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp index 89f27bf71f9a..10b6e24a12c1 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp @@ -12,6 +12,7 @@ #include #include "internal/CullingContext.h" #include "internal/DiffMap.h" +#include "internal/LongestIncreasingSubsequence.h" #include "internal/ShadowViewNodePair.h" #include "internal/sliceChildShadowNodeViewPairs.h" @@ -1048,13 +1049,275 @@ static void calculateShadowViewMutations( oldCullingContext, newCullingContextCopy); } + } else if (ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator()) { + // LIS-based Stage 4: find the Longest Increasing Subsequence of + // new-list positions among old children to minimize REMOVE/INSERT + // mutations. Items in the LIS maintain their relative order and + // don't need REMOVE+INSERT — only items outside the LIS are moved. + auto remainingOldCount = oldChildPairs.size() - lastIndexAfterFirstStage; + auto remainingNewCount = newChildPairs.size() - lastIndexAfterFirstStage; + + // Build newRemainingPairs (required by updateMatchedPairSubtrees for + // flattening logic) and tag→index map for O(1) lookups in Step 1. + auto newRemainingPairs = + DiffMap(remainingNewCount); + auto newTagToIndex = DiffMap(remainingNewCount); + for (size_t i = lastIndexAfterFirstStage; i < newChildPairs.size(); i++) { + auto& newChildPair = *newChildPairs[i]; + newRemainingPairs.insert({newChildPair.shadowView.tag, &newChildPair}); + newTagToIndex.insert({newChildPair.shadowView.tag, i}); + } + + // Step 1: Map old children to their positions in the new list. + std::vector oldToNewPos(remainingOldCount); + std::vector oldExistsInNew(remainingOldCount, false); + + for (size_t i = 0; i < remainingOldCount; i++) { + auto oldIdx = lastIndexAfterFirstStage + i; + Tag oldTag = oldChildPairs[oldIdx]->shadowView.tag; + auto it = newTagToIndex.find(oldTag); + if (it != newTagToIndex.end()) { + oldToNewPos[i] = it->second; + oldExistsInNew[i] = true; + } + } + + // Step 2: Compute LIS of new-list positions. + auto inLIS = longestIncreasingSubsequence(oldToNewPos, oldExistsInNew); + + auto deletionCandidatePairs = std::vector{}; + deletionCandidatePairs.reserve(remainingOldCount); + + // New-child-indexed flag: true = LIS match that stays in place. + auto isLISMatch = std::vector(remainingNewCount, false); + + // Step 4: Process old children. + // CRITICAL: check newRemainingPairs at runtime (not the pre-computed + // oldExistsInNew). The flattener erases entries from newRemainingPairs + // as it consumes them during flatten/unflatten — using the pre-computed + // snapshot would cause double-processing of flattened nodes. + react_native_assert(inLIS.size() == remainingOldCount); + for (size_t i = 0; i < remainingOldCount; i++) { + auto oldIdx = lastIndexAfterFirstStage + i; + auto& oldChildPair = *oldChildPairs[oldIdx]; + Tag oldTag = oldChildPair.shadowView.tag; + + auto newIt = newRemainingPairs.find(oldTag); + if (newIt == newRemainingPairs.end()) { + // Not in new list or consumed by flattening -> REMOVE. + if (!oldChildPair.isConcreteView) { + continue; + } + + DEBUG_LOGS({ + LOG(ERROR) << "Differ LIS Branch: Removing deleted tag: " + << oldChildPair << " with parent: [" << parentTag << "]"; + }); + + // Edge case: complex (un)flattening — node exists in other tree. + if (oldChildPair.inOtherTree() && + oldChildPair.otherTreePair->isConcreteView) { + const ShadowView& otherTreeView = + oldChildPair.otherTreePair->shadowView; + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( + parentTag, + otherTreeView, + static_cast(oldChildPair.mountIndex))); + continue; + } + + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( + parentTag, + oldChildPair.shadowView, + static_cast(oldChildPair.mountIndex))); + deletionCandidatePairs.push_back(&oldChildPair); + + } else if (inLIS[i]) { + // In LIS -> stays in place, just UPDATE + subtree recursion. + auto& newChildPair = *newIt->second; + + DEBUG_LOGS({ + LOG(ERROR) << "Differ LIS Branch: Matched in-order (LIS) at old " + << oldIdx << ": " << oldChildPair << " with parent: [" + << parentTag << "]"; + }); + + // For LIS matches with concrete-ness changes, we must use + // (true, false) to avoid generating INSERT in updateMatchedPair. + // INSERT mutations must be in new-child order (Step 5), not + // old-child order (Step 4). Generating INSERT here would put it + // out of order relative to Step 5's INSERTs. + bool concreteChanged = + oldChildPair.isConcreteView != newChildPair.isConcreteView; + + updateMatchedPair( + mutationContainer, + true, + !concreteChanged, + parentTag, + oldChildPair, + newChildPair); + + updateMatchedPairSubtrees( + scope, + mutationContainer, + newRemainingPairs, + oldChildPairs, + parentTag, + oldChildPair, + newChildPair, + oldCullingContext, + newCullingContext); + + if (!concreteChanged) { + // Check if this LIS match was consumed by flattening. + // updateMatchedPairSubtrees may erase entries from + // newRemainingPairs during flatten/unflatten transitions. + // If consumed, the node was reparented elsewhere and needs + // REMOVE from this parent. + if (newRemainingPairs.find(oldTag) != newRemainingPairs.end()) { + isLISMatch[oldToNewPos[i] - lastIndexAfterFirstStage] = true; + } else if (oldChildPair.isConcreteView) { + if (oldChildPair.inOtherTree() && + oldChildPair.otherTreePair->isConcreteView) { + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( + parentTag, + oldChildPair.otherTreePair->shadowView, + static_cast(oldChildPair.mountIndex))); + } else { + mutationContainer.removeMutations.push_back( + ShadowViewMutation::RemoveMutation( + parentTag, + oldChildPair.shadowView, + static_cast(oldChildPair.mountIndex))); + deletionCandidatePairs.push_back(&oldChildPair); + } + } + } + // concreteChanged: not in isLISMatch, Step 5 handles INSERT. + + } else { + // In new list but NOT in LIS -> REMOVE from old position. + // Will be re-inserted at new position in Step 5. + auto& newChildPair = *newIt->second; + + DEBUG_LOGS({ + LOG(ERROR) + << "Differ LIS Branch: Matched out-of-order (not in LIS) at old " + << oldIdx << ": " << oldChildPair << " with parent: [" + << parentTag << "]"; + }); + + updateMatchedPair( + mutationContainer, + true, + false, + parentTag, + oldChildPair, + newChildPair); + + updateMatchedPairSubtrees( + scope, + mutationContainer, + newRemainingPairs, + oldChildPairs, + parentTag, + oldChildPair, + newChildPair, + oldCullingContext, + newCullingContext); + } + } + + // Step 5: Process new children — INSERT + CREATE. + // Generate INSERT for every non-LIS-matched concrete new child. + // Only generate CREATE for genuinely new children (not in other tree + // from flattening). + for (size_t i = lastIndexAfterFirstStage; i < newChildPairs.size(); i++) { + auto& newChildPair = *newChildPairs[i]; + + if (!newChildPair.isConcreteView) { + continue; + } + + // LIS matches stay in place — no INSERT needed. + if (isLISMatch[i - lastIndexAfterFirstStage]) { + continue; + } + + DEBUG_LOGS({ + LOG(ERROR) << "Differ LIS Branch: Inserting tag: " << newChildPair + << " with parent: [" << parentTag << "]" + << (newChildPair.inOtherTree() ? " (in other tree)" : ""); + }); + + mutationContainer.insertMutations.push_back( + ShadowViewMutation::InsertMutation( + parentTag, + newChildPair.shadowView, + static_cast(newChildPair.mountIndex))); + + // Only CREATE genuinely new children (not matched by flattening). + if (!newChildPair.inOtherTree()) { + mutationContainer.createMutations.push_back( + ShadowViewMutation::CreateMutation(newChildPair.shadowView)); + + auto newCullingContextCopy = + newCullingContext.adjustCullingContextIfNeeded(newChildPair); + + ViewNodePairScope innerScope{}; + calculateShadowViewMutations( + innerScope, + mutationContainer.downwardMutations, + newChildPair.shadowView.tag, + {}, + sliceChildShadowNodeViewPairsFromViewNodePair( + newChildPair, innerScope, false, newCullingContextCopy), + oldCullingContext, + newCullingContextCopy); + } + } + + // Step 6: Generate DELETE for deletion candidates. + for (const auto* deletionCandidatePtr : deletionCandidatePairs) { + const auto& oldChildPair = *deletionCandidatePtr; + + DEBUG_LOGS({ + LOG(ERROR) << "Differ LIS Branch: Deleting removed tag: " + << oldChildPair << " with parent: [" << parentTag << "]"; + }); + + if (!oldChildPair.inOtherTree() && oldChildPair.isConcreteView) { + mutationContainer.deleteMutations.push_back( + ShadowViewMutation::DeleteMutation(oldChildPair.shadowView)); + auto oldCullingContextCopy = + oldCullingContext.adjustCullingContextIfNeeded(oldChildPair); + + ViewNodePairScope innerScope{}; + auto grandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( + oldChildPair, innerScope, false, oldCullingContextCopy); + calculateShadowViewMutations( + innerScope, + mutationContainer.destructiveDownwardMutations, + oldChildPair.shadowView.tag, + std::move(grandChildPairs), + {}, + oldCullingContextCopy, + newCullingContext); + } + } } else { + // Existing greedy Stage 4 algorithm. // Collect map of tags in the new list - auto remainingCount = newChildPairs.size() - index; + auto remainingCount = newChildPairs.size() - lastIndexAfterFirstStage; auto newRemainingPairs = DiffMap(remainingCount); auto newInsertedPairs = DiffMap(remainingCount); auto deletionCandidatePairs = DiffMap{}; - for (; index < newChildPairs.size(); index++) { + for (index = lastIndexAfterFirstStage; index < newChildPairs.size(); + index++) { auto& newChildPair = *newChildPairs[index]; newRemainingPairs.insert({newChildPair.shadowView.tag, &newChildPair}); } diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/internal/LongestIncreasingSubsequence.h b/packages/react-native/ReactCommon/react/renderer/mounting/internal/LongestIncreasingSubsequence.h new file mode 100644 index 000000000000..1f31ead210cf --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/mounting/internal/LongestIncreasingSubsequence.h @@ -0,0 +1,95 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +/** + * Computes the Longest Increasing Subsequence (LIS) of the given + * sequence of values using O(n log n) patience sorting. + * + * Returns a vector of the same size as `values`, where + * result[i] == true means values[i] is part of the LIS. + * + * Only elements where include[i] == true are considered; + * elements with include[i] == false are ignored and will always + * be false in the result. + */ +inline std::vector longestIncreasingSubsequence( + const std::vector &values, + const std::vector &include) +{ + react_native_assert(values.size() == include.size()); + + size_t n = values.size(); + std::vector inLIS(n, false); + + if (n == 0) { + return inLIS; + } + + // Collect indices of included elements. + std::vector indices; + indices.reserve(n); + for (size_t i = 0; i < n; i++) { + if (include[i]) { + indices.push_back(i); + } + } + + if (indices.empty()) { + return inLIS; + } + + // tails[i] = smallest tail value of all increasing subsequences + // of length i+1. + std::vector tails; + // tailIndices[i] = index into `indices` whose value is tails[i]. + std::vector tailIndices; + // predecessor[k] = index into `indices` of the predecessor of + // indices[k] in the LIS, or -1 if none. + std::vector predecessor(indices.size(), -1); + + tails.reserve(indices.size()); + tailIndices.reserve(indices.size()); + + for (size_t k = 0; k < indices.size(); k++) { + size_t val = values[indices[k]]; + + // Binary search for the first element in tails >= val. + auto it = std::lower_bound(tails.begin(), tails.end(), val); + auto pos = static_cast(it - tails.begin()); + + if (it == tails.end()) { + tails.push_back(val); + tailIndices.push_back(k); + } else { + *it = val; + tailIndices[pos] = k; + } + + if (pos > 0) { + predecessor[k] = static_cast(tailIndices[pos - 1]); + } + } + + // Reconstruct LIS by tracing predecessors from the last element. + int current = static_cast(tailIndices.back()); + while (current >= 0) { + inLIS[indices[static_cast(current)]] = true; + current = predecessor[static_cast(current)]; + } + + return inLIS; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/tests/LongestIncreasingSubsequenceTest.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/tests/LongestIncreasingSubsequenceTest.cpp new file mode 100644 index 000000000000..428543348f7e --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/mounting/tests/LongestIncreasingSubsequenceTest.cpp @@ -0,0 +1,154 @@ +/* + * 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 + +#include "internal/LongestIncreasingSubsequence.h" + +namespace facebook::react { + +TEST(LongestIncreasingSubsequenceTest, emptyInput) { + auto result = longestIncreasingSubsequence({}, {}); + EXPECT_TRUE(result.empty()); +} + +TEST(LongestIncreasingSubsequenceTest, singleElement) { + auto result = longestIncreasingSubsequence({5}, {true}); + ASSERT_EQ(result.size(), 1u); + EXPECT_TRUE(result[0]); +} + +TEST(LongestIncreasingSubsequenceTest, alreadySorted) { + // [0, 1, 2, 3, 4] — entire sequence is the LIS. + std::vector values = {0, 1, 2, 3, 4}; + std::vector include = {true, true, true, true, true}; + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 5u); + for (size_t i = 0; i < 5; i++) { + EXPECT_TRUE(result[i]) << "index " << i << " should be in LIS"; + } +} + +TEST(LongestIncreasingSubsequenceTest, reverseSorted) { + // [4, 3, 2, 1, 0] — LIS length is 1. + std::vector values = {4, 3, 2, 1, 0}; + std::vector include = {true, true, true, true, true}; + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 5u); + int count = 0; + for (size_t i = 0; i < 5; i++) { + if (result[i]) { + count++; + } + } + EXPECT_EQ(count, 1); +} + +TEST(LongestIncreasingSubsequenceTest, moveLastToFront) { + // Old: [A, B, C, D, E] mapped to new positions [1, 2, 3, 4, 0] + // LIS should be [1, 2, 3, 4] (indices 0-3), leaving index 4 out. + std::vector values = {1, 2, 3, 4, 0}; + std::vector include = {true, true, true, true, true}; + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 5u); + EXPECT_TRUE(result[0]); // value 1 + EXPECT_TRUE(result[1]); // value 2 + EXPECT_TRUE(result[2]); // value 3 + EXPECT_TRUE(result[3]); // value 4 + EXPECT_FALSE(result[4]); // value 0 +} + +TEST(LongestIncreasingSubsequenceTest, moveFirstToLast) { + // Old: [A, B, C, D, E] mapped to new positions [4, 0, 1, 2, 3] + // LIS should be [0, 1, 2, 3] (indices 1-4), leaving index 0 out. + std::vector values = {4, 0, 1, 2, 3}; + std::vector include = {true, true, true, true, true}; + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 5u); + EXPECT_FALSE(result[0]); // value 4 + EXPECT_TRUE(result[1]); // value 0 + EXPECT_TRUE(result[2]); // value 1 + EXPECT_TRUE(result[3]); // value 2 + EXPECT_TRUE(result[4]); // value 3 +} + +TEST(LongestIncreasingSubsequenceTest, withExcludedElements) { + // values = [1, _, 3, _, 0] where _ are excluded (deleted from new list) + std::vector values = {1, 999, 3, 999, 0}; + std::vector include = {true, false, true, false, true}; + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 5u); + EXPECT_FALSE(result[1]); // excluded + EXPECT_FALSE(result[3]); // excluded + + // Among included: [1, 3, 0]. LIS is [1, 3] (length 2). + EXPECT_TRUE(result[0]); // value 1 + EXPECT_TRUE(result[2]); // value 3 + EXPECT_FALSE(result[4]); // value 0 +} + +TEST(LongestIncreasingSubsequenceTest, allExcluded) { + std::vector values = {3, 1, 2}; + std::vector include = {false, false, false}; + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 3u); + EXPECT_FALSE(result[0]); + EXPECT_FALSE(result[1]); + EXPECT_FALSE(result[2]); +} + +TEST(LongestIncreasingSubsequenceTest, interleaved) { + // [3, 1, 4, 1, 5, 9, 2, 6] + // One valid LIS: [1, 4, 5, 9] or [1, 2, 6] etc. Length should be 4. + std::vector values = {3, 1, 4, 1, 5, 9, 2, 6}; + std::vector include(8, true); + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 8u); + int count = 0; + size_t prev = 0; + bool first = true; + for (size_t i = 0; i < 8; i++) { + if (result[i]) { + count++; + if (!first) { + EXPECT_GT(values[i], prev) << "LIS must be strictly increasing"; + } + prev = values[i]; + first = false; + } + } + EXPECT_EQ(count, 4); +} + +TEST(LongestIncreasingSubsequenceTest, swapTwoElements) { + // Old: [A, B, C, D] → New: [A, C, B, D] → positions [0, 2, 1, 3] + // LIS: [0, 1, 3] or [0, 2, 3] — length 3, one element out. + std::vector values = {0, 2, 1, 3}; + std::vector include = {true, true, true, true}; + auto result = longestIncreasingSubsequence(values, include); + + ASSERT_EQ(result.size(), 4u); + int count = 0; + for (size_t i = 0; i < 4; i++) { + if (result[i]) { + count++; + } + } + EXPECT_EQ(count, 3); + // First and last should always be in LIS. + EXPECT_TRUE(result[0]); // value 0 + EXPECT_TRUE(result[3]); // value 3 +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp index fb22e2372cc8..c7b501b91c25 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp @@ -13,9 +13,14 @@ #include #include #include +#include +#include +#include #include #include +#include +#include #include #include #include @@ -346,7 +351,35 @@ static void testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( using namespace facebook::react; -TEST( +namespace { + +class LISFeatureFlagsOverride + : public facebook::react::ReactNativeFeatureFlagsDefaults { + public: + bool useLISAlgorithmInDifferentiator() override { + return true; + } +}; + +} // namespace + +// Parametrized lifecycle tests: each test runs with both the greedy +// algorithm (false) and the LIS algorithm (true). +class ShadowTreeLifecycleTest : public ::testing::TestWithParam { + protected: + void SetUp() override { + if (GetParam()) { + facebook::react::ReactNativeFeatureFlags::override( + std::make_unique()); + } + } + + void TearDown() override { + facebook::react::ReactNativeFeatureFlags::dangerouslyReset(); + } +}; + +TEST_P( ShadowTreeLifecycleTest, stableBiggerTreeFewerIterationsOptimizedMovesFlattener) { testShadowNodeTreeLifeCycle( @@ -356,7 +389,7 @@ TEST( /* stages */ 32); } -TEST( +TEST_P( ShadowTreeLifecycleTest, stableBiggerTreeFewerIterationsOptimizedMovesFlattener2) { testShadowNodeTreeLifeCycle( @@ -366,7 +399,7 @@ TEST( /* stages */ 32); } -TEST( +TEST_P( ShadowTreeLifecycleTest, stableSmallerTreeMoreIterationsOptimizedMovesFlattener) { testShadowNodeTreeLifeCycle( @@ -376,7 +409,7 @@ TEST( /* stages */ 32); } -TEST( +TEST_P( ShadowTreeLifecycleTest, unstableSmallerTreeFewerIterationsExtensiveFlatteningUnflattening) { testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( @@ -386,7 +419,7 @@ TEST( /* stages */ 32); } -TEST( +TEST_P( ShadowTreeLifecycleTest, unstableBiggerTreeFewerIterationsExtensiveFlatteningUnflattening) { testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( @@ -396,7 +429,7 @@ TEST( /* stages */ 32); } -TEST( +TEST_P( ShadowTreeLifecycleTest, unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening) { testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( @@ -407,20 +440,18 @@ TEST( } // failing test case found 4-25-2021 -// TODO: T213669056 -// TEST( -// ShadowTreeLifecycleTest, -// unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening_1167342011) -// { -// testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( -// /* seed */ 1167342011, -// /* size */ 32, -// /* repeats */ 512, -// /* stages */ 32); -// } +TEST_P( + ShadowTreeLifecycleTest, + unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening_1167342011) { + testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening( + /* seed */ 1167342011, + /* size */ 32, + /* repeats */ 512, + /* stages */ 32); +} // You may uncomment this - locally only! - to generate failing seeds. -// TEST( +// TEST_P( // ShadowTreeLifecycleTest, // unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflatteningManyRandom) // { @@ -435,3 +466,268 @@ TEST( // /* stages */ 32); // } // } + +// Demonstrates that the LIS algorithm produces fewer mutations than the +// greedy algorithm for a simple child reorder: [A,B,C,D,E] → [B,C,D,E,A]. +// The greedy two-pointer walk encounters A vs B mismatch and generates +// excessive REMOVE+INSERT pairs. The LIS algorithm identifies that B,C,D,E +// are already in increasing order and only moves A. +TEST_P(ShadowTreeLifecycleTest, moveFirstChildToLast) { + auto builder = simpleComponentBuilder(); + + auto makeProps = [](const std::string& id) { + auto props = std::make_shared(); + props->nativeId = id; + return props; + }; + + // clang-format off + auto rootElement = + Element() + .tag(1) + .children({ + Element().tag(2).props(makeProps("A")), + Element().tag(3).props(makeProps("B")), + Element().tag(4).props(makeProps("C")), + Element().tag(5).props(makeProps("D")), + Element().tag(6).props(makeProps("E")), + }); + // clang-format on + + auto rootNode = builder.build(rootElement); + + // Clone root with children reordered to [B, C, D, E, A]. + auto children = rootNode->getChildren(); + auto reorderedChildren = + std::make_shared>>(); + for (size_t i = 1; i < children.size(); i++) { + reorderedChildren->push_back(children[i]); + } + reorderedChildren->push_back(children[0]); + + auto reorderedRootNode = std::static_pointer_cast( + rootNode->ShadowNode::clone( + ShadowNodeFragment{ + .props = ShadowNodeFragment::propsPlaceholder(), + .children = reorderedChildren})); + + auto expected = + buildStubViewTreeWithoutUsingDifferentiator(*reorderedRootNode); + auto mutations = calculateShadowViewMutations(*rootNode, *reorderedRootNode); + + auto isMoveOp = [](const ShadowViewMutation& m) { + return m.type == ShadowViewMutation::Remove || + m.type == ShadowViewMutation::Insert; + }; + + if (ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator()) { + // LIS: 1 REMOVE + 1 INSERT = 2 move ops. + EXPECT_EQ(std::count_if(mutations.begin(), mutations.end(), isMoveOp), 2); + } else { + // Greedy: 4 REMOVE + 4 INSERT = 8 move ops. + EXPECT_EQ(std::count_if(mutations.begin(), mutations.end(), isMoveOp), 8); + } + + auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode); + viewTree.mutate(mutations); + EXPECT_EQ(viewTree, expected); +} + +// Exercises the LIS path where concreteChanged=true AND flattening +// consumption occur simultaneously. Z(tag=7) is reordered to break head/tail +// matching, forcing A, W, and C into the LIS path. W(tag=3) loses testId +// (concrete→non-concrete, concreteChanged=true) while remaining in the LIS. +// updateMatchedPairSubtrees triggers the flattener which promotes B(tag=6). +// The if(!concreteChanged) block that normally checks consumption is bypassed, +// so this test verifies the combination still produces correct mutations. +TEST_P(ShadowTreeLifecycleTest, concreteChangedWithFlattening) { + auto builder = simpleComponentBuilder(); + + auto concreteProps = [](const std::string& id) { + auto props = std::make_shared(); + props->testId = id; + return props; + }; + + // clang-format off + // Old: Root -> [Z, A, W(concrete, child B), C] + // Old flattened: [Z(7), A(2), W(3), C(4)] + auto rootElement = + Element() + .tag(1) + .children({ + Element().tag(7).props(concreteProps("Z")), + Element().tag(2).props(concreteProps("A")), + Element().tag(3).props(concreteProps("W")) + .children({ + Element().tag(6).props(concreteProps("B")), + }), + Element().tag(4).props(concreteProps("C")), + }); + // clang-format on + + auto rootNode = builder.build(rootElement); + auto children = rootNode->getChildren(); + // children: [Z(7), A(2), W(3), C(4)] + + // W loses testId → non-concrete (flattened). B(tag=6) promoted. + auto flatW = children[2]->clone( + ShadowNodeFragment{.props = std::make_shared()}); + + // New: Root -> [A, W(flattened), C, Z] — Z moved to end. + // New flattened: [A(2), W(3,non-concrete), B(6), C(4), Z(7)] + // Z's move breaks head matching; A, W, C enter LIS path. + // Among old remaining [Z,A,W,C] mapped to new positions, + // LIS includes A,W,C (increasing order), W has concreteChanged=true. + auto newChildren = + std::make_shared>>(); + newChildren->push_back(children[1]); // A + newChildren->push_back(flatW); // W (flattened) + newChildren->push_back(children[3]); // C + newChildren->push_back(children[0]); // Z (moved to end) + + auto newRootNode = std::static_pointer_cast( + rootNode->ShadowNode::clone( + ShadowNodeFragment{ + .props = ShadowNodeFragment::propsPlaceholder(), + .children = newChildren})); + + auto expected = buildStubViewTreeWithoutUsingDifferentiator(*newRootNode); + auto mutations = calculateShadowViewMutations(*rootNode, *newRootNode); + auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode); + viewTree.mutate(mutations); + EXPECT_EQ(viewTree, expected); +} + +// Reverse: W(tag=3) gains testId (non-concrete→concrete, concreteChanged=true) +// while in the LIS path. B(tag=6) was promoted by flattening and now gets +// absorbed back into W via unflattening. Z reorder breaks head matching. +TEST_P(ShadowTreeLifecycleTest, concreteChangedWithUnflatteningInLIS) { + auto builder = simpleComponentBuilder(); + + auto concreteProps = [](const std::string& id) { + auto props = std::make_shared(); + props->testId = id; + return props; + }; + + // clang-format off + // Old: Root -> [Z, A, W(NON-concrete, child B(concrete)), C] + // Old flattened: [Z(7), A(2), W(3,non-concrete), B(6), C(4)] + auto rootElement = + Element() + .tag(1) + .children({ + Element().tag(7).props(concreteProps("Z")), + Element().tag(2).props(concreteProps("A")), + Element().tag(3) + .children({ + Element().tag(6).props(concreteProps("B")), + }), + Element().tag(4).props(concreteProps("C")), + }); + // clang-format on + + auto rootNode = builder.build(rootElement); + auto children = rootNode->getChildren(); + + // W gains testId → concrete (unflattened). B absorbed back into W. + auto concreteW = + children[2]->clone(ShadowNodeFragment{.props = concreteProps("W")}); + + // New: Root -> [A, W(concrete), C, Z] — Z moved to end. + // New flattened: [A(2), W(3), C(4), Z(7)] + auto newChildren = + std::make_shared>>(); + newChildren->push_back(children[1]); // A + newChildren->push_back(concreteW); // W (concrete) + newChildren->push_back(children[3]); // C + newChildren->push_back(children[0]); // Z (moved to end) + + auto newRootNode = std::static_pointer_cast( + rootNode->ShadowNode::clone( + ShadowNodeFragment{ + .props = ShadowNodeFragment::propsPlaceholder(), + .children = newChildren})); + + auto expected = buildStubViewTreeWithoutUsingDifferentiator(*newRootNode); + auto mutations = calculateShadowViewMutations(*rootNode, *newRootNode); + auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode); + viewTree.mutate(mutations); + EXPECT_EQ(viewTree, expected); +} + +// Tests that the LIS algorithm produces correct mutations when child +// reordering coincides with a view's flattening transition. When W(tag=3) +// becomes flattened (loses testId), its child B(tag=6) is promoted to the +// parent level in the flattened view. The LIS pre-computation maps old +// children to new positions, but the flattener may consume entries from +// newRemainingPairs during flatten/unflatten processing. This test verifies +// the runtime newRemainingPairs check correctly handles this interaction. +TEST_P(ShadowTreeLifecycleTest, reorderWithFlatteningTransition) { + auto builder = simpleComponentBuilder(); + + auto concreteProps = [](const std::string& id) { + auto props = std::make_shared(); + props->testId = id; + return props; + }; + + // clang-format off + // Old tree: Root -> [A, W(concrete with testId, child B), C, D] + auto rootElement = + Element() + .tag(1) + .children({ + Element().tag(2).props(concreteProps("A")), + Element().tag(3).props(concreteProps("W")) + .children({ + Element().tag(6).props(concreteProps("B")), + }), + Element().tag(4).props(concreteProps("C")), + Element().tag(5).props(concreteProps("D")), + }); + // clang-format on + + auto rootNode = builder.build(rootElement); + auto children = rootNode->getChildren(); + // children: [A(2), W(3), C(4), D(5)] + + // Clone W without testId to make it non-concrete (flattened). + // B(tag=6) will be promoted to the parent level. + auto flatW = children[1]->clone( + ShadowNodeFragment{.props = std::make_shared()}); + + // New tree: Root -> [C, D, A, W(flattened)] + // Reorder [A,W,C,D] -> [C,D,A,W(flattened)] + auto reorderedChildren = + std::make_shared>>(); + reorderedChildren->push_back(children[2]); // C + reorderedChildren->push_back(children[3]); // D + reorderedChildren->push_back(children[0]); // A + reorderedChildren->push_back(flatW); // W (flattened) + + auto newRootNode = std::static_pointer_cast( + rootNode->ShadowNode::clone( + ShadowNodeFragment{ + .props = ShadowNodeFragment::propsPlaceholder(), + .children = reorderedChildren})); + + auto expected = buildStubViewTreeWithoutUsingDifferentiator(*newRootNode); + auto mutations = calculateShadowViewMutations(*rootNode, *newRootNode); + auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode); + viewTree.mutate(mutations); + EXPECT_EQ(viewTree, expected); +} + +INSTANTIATE_TEST_SUITE_P( + Greedy, + ShadowTreeLifecycleTest, + ::testing::Values(false), + [](const auto&) { return "Greedy"; }); + +INSTANTIATE_TEST_SUITE_P( + LIS, + ShadowTreeLifecycleTest, + ::testing::Values(true), + [](const auto&) { return "LIS"; }); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 1d6ffadb4af6..1f4108dc265d 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -901,6 +901,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + useLISAlgorithmInDifferentiator: { + defaultValue: false, + metadata: { + dateAdded: '2026-03-12', + description: + 'Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, useNativeViewConfigsInBridgelessMode: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 3deb4d238223..3d92ce834c9f 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5966ef11ee71a38059decda1c529fd6f>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -128,6 +128,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ updateRuntimeShadowNodeReferencesOnCommitThread: Getter, useAlwaysAvailableJSErrorHandling: Getter, useFabricInterop: Getter, + useLISAlgorithmInDifferentiator: Getter, useNativeViewConfigsInBridgelessMode: Getter, useNestedScrollViewAndroid: Getter, useSharedAnimatedBackend: Getter, @@ -529,6 +530,10 @@ export const useAlwaysAvailableJSErrorHandling: Getter = createNativeFl * Should this application enable the Fabric Interop Layer for Android? If yes, the application will behave so that it can accept non-Fabric components and render them on Fabric. This toggle is controlling extra logic such as custom event dispatching that are needed for the Fabric Interop Layer to work correctly. */ export const useFabricInterop: Getter = createNativeFlagGetter('useFabricInterop', true); +/** + * Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation. + */ +export const useLISAlgorithmInDifferentiator: Getter = createNativeFlagGetter('useLISAlgorithmInDifferentiator', false); /** * When enabled, the native view configs are used in bridgeless mode. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index ea68b2cf9eb3..d03933b754e6 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<85ddaee522fc3be8546aef21cf236e9a>> + * @generated SignedSource<<971e04eed130adab56e3306a6a26ff1c>> * @flow strict * @noformat */ @@ -105,6 +105,7 @@ export interface Spec extends TurboModule { +updateRuntimeShadowNodeReferencesOnCommitThread?: () => boolean; +useAlwaysAvailableJSErrorHandling?: () => boolean; +useFabricInterop?: () => boolean; + +useLISAlgorithmInDifferentiator?: () => boolean; +useNativeViewConfigsInBridgelessMode?: () => boolean; +useNestedScrollViewAndroid?: () => boolean; +useSharedAnimatedBackend?: () => boolean; diff --git a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js index c45a6551484f..49ebf42cb3d0 100644 --- a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js +++ b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @fantom_flags enableFabricCommitBranching:* + * @fantom_flags enableFabricCommitBranching:* useLISAlgorithmInDifferentiator:* * @flow strict-local * @format */ @@ -14,6 +14,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import type {HostInstance} from 'react-native'; import ensureInstance from '../../../__tests__/utilities/ensureInstance'; +import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags'; import * as Fantom from '@react-native/fantom'; import * as React from 'react'; import {View} from 'react-native'; @@ -86,6 +87,80 @@ describe('ViewFlattening', () => { ]); }); + /** + * Test worst-case reordering: moving the first child to the end. + * + * P -> [A,B,C,D,E] ==> P -> [B,C,D,E,A] + * + * The greedy two-pointer encounters A vs B mismatch at the first + * position and generates excessive REMOVE+INSERT pairs: + * 4 removes + 4 inserts = 8 mutations. + * + * The LIS algorithm identifies [B,C,D,E] as the longest increasing + * subsequence — those children stay in place, and only A needs to + * move: 1 remove + 1 insert = 2 mutations. + */ + test('reordering: move first child to last (worst case for greedy)', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + + + + + , + ); + }); + + root.takeMountingManagerLogs(); + + Fantom.runTask(() => { + root.render( + + + + + + + , + ); + }); + + expect(root.getRenderedOutput().toJSX()).toEqual( + + + + + + + , + ); + + const logs = root.takeMountingManagerLogs(); + if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) { + // LIS = [B,C,D,E], only A moves: 1 remove + 1 insert = 2 mutations + expect(logs).toEqual([ + 'Remove {type: "View", parentNativeID: "P", index: 0, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "P", index: 4, nativeID: "A"}', + ]); + } else { + // Greedy: 4 removes + 4 inserts = 8 mutations + expect(logs).toEqual([ + 'Remove {type: "View", parentNativeID: "P", index: 4, nativeID: "E"}', + 'Remove {type: "View", parentNativeID: "P", index: 3, nativeID: "D"}', + 'Remove {type: "View", parentNativeID: "P", index: 2, nativeID: "C"}', + 'Remove {type: "View", parentNativeID: "P", index: 1, nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "P", index: 0, nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "P", index: 1, nativeID: "C"}', + 'Insert {type: "View", parentNativeID: "P", index: 2, nativeID: "D"}', + 'Insert {type: "View", parentNativeID: "P", index: 3, nativeID: "E"}', + ]); + } + }); + /** * Test reparenting mutation instruction generation. * We cannot practically handle all possible use-cases here. @@ -146,13 +221,23 @@ describe('ViewFlattening', () => { , ); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "View", nativeID: "A"}', - 'Remove {type: "View", parentNativeID: "G", index: 0, nativeID: "A"}', - 'Create {type: "View", nativeID: "H"}', - 'Insert {type: "View", parentNativeID: "G", index: 0, nativeID: "H"}', - 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}', - ]); + if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "View", nativeID: "A"}', + 'Remove {type: "View", parentNativeID: "G", index: 0, nativeID: "A"}', + 'Create {type: "View", nativeID: "H"}', + 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "G", index: 0, nativeID: "H"}', + ]); + } else { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "View", nativeID: "A"}', + 'Remove {type: "View", parentNativeID: "G", index: 0, nativeID: "A"}', + 'Create {type: "View", nativeID: "H"}', + 'Insert {type: "View", parentNativeID: "G", index: 0, nativeID: "H"}', + 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}', + ]); + } // The view is reparented 1 level down with a different sibling // Root -> G* -> H* -> I* -> J -> [B*, A*] [nodes with * are _not_ flattened] @@ -182,14 +267,25 @@ describe('ViewFlattening', () => { , ); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Remove {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}', - 'Create {type: "View", nativeID: "I"}', - 'Create {type: "View", nativeID: "B"}', - 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "I"}', - 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}', - 'Insert {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}', - ]); + if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Remove {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}', + 'Create {type: "View", nativeID: "I"}', + 'Create {type: "View", nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "I"}', + ]); + } else { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Remove {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}', + 'Create {type: "View", nativeID: "I"}', + 'Create {type: "View", nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "I"}', + 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}', + ]); + } // The view is reparented 1 level further down with its order with the sibling // swapped @@ -222,14 +318,25 @@ describe('ViewFlattening', () => { , ); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Remove {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}', - 'Remove {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}', - 'Create {type: "View", nativeID: "J"}', - 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "J"}', - 'Insert {type: "View", parentNativeID: "J", index: 0, nativeID: "A"}', - 'Insert {type: "View", parentNativeID: "J", index: 1, nativeID: "B"}', - ]); + if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Remove {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}', + 'Remove {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}', + 'Create {type: "View", nativeID: "J"}', + 'Insert {type: "View", parentNativeID: "J", index: 0, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "J", index: 1, nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "J"}', + ]); + } else { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Remove {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}', + 'Remove {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}', + 'Create {type: "View", nativeID: "J"}', + 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "J"}', + 'Insert {type: "View", parentNativeID: "J", index: 0, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "J", index: 1, nativeID: "B"}', + ]); + } }); test('parent-child switching from unflattened-flattened to flattened-unflattened', () => { @@ -340,15 +447,28 @@ describe('ViewFlattening', () => { , ); }); - expect(root.takeMountingManagerLogs()).toEqual([ - 'Update {type: "View", nativeID: "child"}', - 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}', - 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}', - 'Delete {type: "View", nativeID: (N/A)}', - 'Create {type: "View", nativeID: (N/A)}', - 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}', - 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}', - ]); + + if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "View", nativeID: "child"}', + 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}', + 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}', + 'Delete {type: "View", nativeID: (N/A)}', + 'Create {type: "View", nativeID: (N/A)}', + 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}', + 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}', + ]); + } else { + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "View", nativeID: "child"}', + 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}', + 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}', + 'Delete {type: "View", nativeID: (N/A)}', + 'Create {type: "View", nativeID: (N/A)}', + 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}', + 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}', + ]); + } }); test('#51378: view with rgba(255,255,255,127/256) background color is not flattened', () => {