From 26d194f7ee08b5b37e02cf4812a5347d19d535cb Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Sun, 15 Feb 2026 23:29:54 -0800 Subject: [PATCH 1/3] Refactor how we promote shadow tree revisions as latest for JS consistency Summary: Changelog: [General][Changed] - Refactor how shadow tree revisions are promoted as latest for JS consistency Changes `LazyShadowTreeRevisionConsistencyManager::updateCurrentRevision` to pull the latest commited revision for a given surface id instead of accepting the new revision as a parameter. This way, the new revision is read from the same place for every update, which opens up the way to implement commit branching. This change will allow to always read from the JS tree revision, if it exists. Differential Revision: D88151490 --- .../react/renderer/uimanager/UIManager.cpp | 21 +++++----- ...zyShadowTreeRevisionConsistencyManager.cpp | 40 ++++++++----------- ...LazyShadowTreeRevisionConsistencyManager.h | 4 +- ...adowTreeRevisionConsistencyManagerTest.cpp | 12 +++--- 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index c1d12096c2fd..8dddd3ced432 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -188,8 +188,10 @@ void UIManager::completeSurface( ShadowTree::CommitOptions commitOptions) { TraceSection s("UIManager::completeSurface", "surfaceId", surfaceId); + ShadowTree::CommitStatus result; + shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { - auto result = shadowTree.commit( + result = shadowTree.commit( [&](const RootShadowNode& oldRootShadowNode) { return std::make_shared( oldRootShadowNode, @@ -199,18 +201,17 @@ void UIManager::completeSurface( }); }, commitOptions); + }); - if (result == ShadowTree::CommitStatus::Succeeded) { - // It's safe to update the visible revision of the shadow tree immediately - // after we commit a specific one. - lazyShadowTreeRevisionConsistencyManager_->updateCurrentRevision( - surfaceId, shadowTree.getCurrentRevision().rootShadowNode); + if (result == ShadowTree::CommitStatus::Succeeded) { + // It's safe to update the visible revision of the shadow tree immediately + // after we commit a specific one. + lazyShadowTreeRevisionConsistencyManager_->updateCurrentRevision(surfaceId); - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { - animationBackend_->clearRegistry(surfaceId); - } + if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + animationBackend_->clearRegistry(surfaceId); } - }); + } } void UIManager::setIsJSResponder( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp index 85cd9cd5158a..8afaad89b39b 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp @@ -15,22 +15,32 @@ LazyShadowTreeRevisionConsistencyManager:: ShadowTreeRegistry& shadowTreeRegistry) : shadowTreeRegistry_(shadowTreeRegistry) {} -void LazyShadowTreeRevisionConsistencyManager::updateCurrentRevision( - SurfaceId surfaceId, - RootShadowNode::Shared rootShadowNode) { +std::shared_ptr +LazyShadowTreeRevisionConsistencyManager::updateCurrentRevision( + SurfaceId surfaceId) { + // This method is only going to be called from JS, so we don't need to protect + // the access to the shadow tree registry as well. + // If this was multi-threaded, we would need to protect it to avoid capturing + // root shadow nodes concurrently. + RootShadowNode::Shared rootShadowNode; + shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { + rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + std::unique_lock lock(capturedRootShadowNodesForConsistencyMutex_); // We don't need to store the revision if we haven't locked. // We can resolve lazily when requested. if (lockCount > 0) { - capturedRootShadowNodesForConsistency_[surfaceId] = - std::move(rootShadowNode); + capturedRootShadowNodesForConsistency_[surfaceId] = rootShadowNode; } + + return rootShadowNode; } #pragma mark - ShadowTreeRevisionProvider -RootShadowNode::Shared +std::shared_ptr LazyShadowTreeRevisionConsistencyManager::getCurrentRevision( SurfaceId surfaceId) { { @@ -43,23 +53,7 @@ LazyShadowTreeRevisionConsistencyManager::getCurrentRevision( } } - // This method is only going to be called from JS, so we don't need to protect - // the access to the shadow tree registry as well. - // If this was multi-threaded, we would need to protect it to avoid capturing - // root shadow nodes concurrently. - RootShadowNode::Shared rootShadowNode; - shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { - rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; - }); - - { - std::unique_lock lock(capturedRootShadowNodesForConsistencyMutex_); - if (lockCount > 0) { - capturedRootShadowNodesForConsistency_[surfaceId] = rootShadowNode; - } - } - - return rootShadowNode; + return updateCurrentRevision(surfaceId); } #pragma mark - ConsistentShadowTreeRevisionProvider diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h index 96c7537ed28d..b63ec2977dbe 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h @@ -28,11 +28,11 @@ class LazyShadowTreeRevisionConsistencyManager : public ShadowTreeRevisionConsis public: explicit LazyShadowTreeRevisionConsistencyManager(ShadowTreeRegistry &shadowTreeRegistry); - void updateCurrentRevision(SurfaceId surfaceId, RootShadowNode::Shared rootShadowNode); + std::shared_ptr updateCurrentRevision(SurfaceId surfaceId); #pragma mark - ShadowTreeRevisionProvider - RootShadowNode::Shared getCurrentRevision(SurfaceId surfaceId) override; + std::shared_ptr getCurrentRevision(SurfaceId surfaceId) override; #pragma mark - ShadowTreeRevisionConsistencyManager diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp index 01d43d096c55..322172b3de1d 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp @@ -141,7 +141,7 @@ TEST_F( EXPECT_EQ(consistencyManager_.getCurrentRevision(0), nullptr); - consistencyManager_.updateCurrentRevision(0, newRootShadowNode); + consistencyManager_.updateCurrentRevision(0); EXPECT_NE(consistencyManager_.getCurrentRevision(0), nullptr); EXPECT_EQ( @@ -176,7 +176,7 @@ TEST_F( EXPECT_EQ(consistencyManager_.getCurrentRevision(0), nullptr); - consistencyManager_.updateCurrentRevision(0, newRootShadowNode); + consistencyManager_.updateCurrentRevision(0); EXPECT_EQ( consistencyManager_.getCurrentRevision(0).get(), newRootShadowNode.get()); @@ -192,7 +192,7 @@ TEST_F( {}); }); - consistencyManager_.updateCurrentRevision(0, newRootShadowNode2); + consistencyManager_.updateCurrentRevision(0); EXPECT_EQ( consistencyManager_.getCurrentRevision(0).get(), @@ -265,7 +265,7 @@ TEST_F( EXPECT_EQ( consistencyManager_.getCurrentRevision(0).get(), newRootShadowNode.get()); - consistencyManager_.updateCurrentRevision(0, newRootShadowNode2); + consistencyManager_.updateCurrentRevision(0); // Updated EXPECT_EQ( @@ -344,7 +344,9 @@ TEST_F(LazyShadowTreeRevisionConsistencyManagerTest, testUpdateToUnmounted) { EXPECT_EQ( consistencyManager_.getCurrentRevision(0).get(), newRootShadowNode.get()); - consistencyManager_.updateCurrentRevision(0, nullptr); + shadowTreeRegistry_.remove(0); + + consistencyManager_.updateCurrentRevision(0); // Updated EXPECT_EQ(consistencyManager_.getCurrentRevision(0).get(), nullptr); From 291f6a00599774ffca9f3311540d32dc0ece8b97 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Sun, 15 Feb 2026 23:29:54 -0800 Subject: [PATCH 2/3] Add feature flag for Fabric commit branching Summary: Changelog: [Internal] Adds a feature flag for the Fabric commit branching mechanism. Differential Revision: D88151491 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 ++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 136 ++++++++++-------- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 10 ++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 20 files changed, 194 insertions(+), 78 deletions(-) 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 eff943289b1f..ddf645513941 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<<67ef2bd30983c949f41e014297dabd6e>> + * @generated SignedSource<<65e8cae8a55213f4b08ec4d896a05c00>> */ /** @@ -162,6 +162,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableExclusivePropsUpdateAndroid(): Boolean = accessor.enableExclusivePropsUpdateAndroid() + /** + * Enables Fabric commit branching to fix starvation problems and atomic JS updates. + */ + @JvmStatic + public fun enableFabricCommitBranching(): Boolean = accessor.enableFabricCommitBranching() + /** * This feature flag enables logs for Fabric. */ 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 77ce14c422c9..deda8570f22d 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<> */ /** @@ -42,6 +42,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableEagerMainQueueModulesOnIOSCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null private var enableExclusivePropsUpdateAndroidCache: Boolean? = null + private var enableFabricCommitBranchingCache: Boolean? = null private var enableFabricLogsCache: Boolean? = null private var enableFabricRendererCache: Boolean? = null private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null @@ -299,6 +300,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableFabricCommitBranching(): Boolean { + var cached = enableFabricCommitBranchingCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableFabricCommitBranching() + enableFabricCommitBranchingCache = cached + } + return cached + } + override fun enableFabricLogs(): Boolean { var cached = enableFabricLogsCache 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 4e69d2adc4a2..e1fc5716b4ec 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<> */ /** @@ -72,6 +72,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableExclusivePropsUpdateAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun enableFabricCommitBranching(): Boolean + @DoNotStrip @JvmStatic public external fun enableFabricLogs(): Boolean @DoNotStrip @JvmStatic public external fun enableFabricRenderer(): 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 474b3da78d6a..c0249bf8683b 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<<62ddb63e95e2b2aff842c5d13210d742>> + * @generated SignedSource<<0139bc7a7b2ae5c9175f44de1ac80105>> */ /** @@ -67,6 +67,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableExclusivePropsUpdateAndroid(): Boolean = false + override fun enableFabricCommitBranching(): Boolean = false + override fun enableFabricLogs(): Boolean = false override fun enableFabricRenderer(): 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 2e6e246813a2..7ccdb793016f 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<<477ec1cf98790b5219ed37092ae958cc>> + * @generated SignedSource<> */ /** @@ -46,6 +46,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableEagerMainQueueModulesOnIOSCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null private var enableExclusivePropsUpdateAndroidCache: Boolean? = null + private var enableFabricCommitBranchingCache: Boolean? = null private var enableFabricLogsCache: Boolean? = null private var enableFabricRendererCache: Boolean? = null private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null @@ -325,6 +326,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enableFabricCommitBranching(): Boolean { + var cached = enableFabricCommitBranchingCache + if (cached == null) { + cached = currentProvider.enableFabricCommitBranching() + accessedFeatureFlags.add("enableFabricCommitBranching") + enableFabricCommitBranchingCache = cached + } + return cached + } + override fun enableFabricLogs(): Boolean { var cached = enableFabricLogsCache 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 2eed3c97abad..e1278f51261e 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<<4480d856ab56f2ecb216610b6e4ae31e>> + * @generated SignedSource<> */ /** @@ -67,6 +67,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableExclusivePropsUpdateAndroid(): Boolean + @DoNotStrip public fun enableFabricCommitBranching(): Boolean + @DoNotStrip public fun enableFabricLogs(): Boolean @DoNotStrip public fun enableFabricRenderer(): 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 91657979e429..d1aa05d33fa5 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<<176845e66b87994db4a9199df21740c0>> + * @generated SignedSource<<4386581c81476b6f7684a8766642db13>> */ /** @@ -171,6 +171,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enableFabricCommitBranching() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableFabricCommitBranching"); + return method(javaProvider_); + } + bool enableFabricLogs() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableFabricLogs"); @@ -633,6 +639,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableExclusivePropsUpdateAndroid( return ReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid(); } +bool JReactNativeFeatureFlagsCxxInterop::enableFabricCommitBranching( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableFabricCommitBranching(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableFabricLogs( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableFabricLogs(); @@ -1020,6 +1031,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableExclusivePropsUpdateAndroid", JReactNativeFeatureFlagsCxxInterop::enableExclusivePropsUpdateAndroid), + makeNativeMethod( + "enableFabricCommitBranching", + JReactNativeFeatureFlagsCxxInterop::enableFabricCommitBranching), makeNativeMethod( "enableFabricLogs", JReactNativeFeatureFlagsCxxInterop::enableFabricLogs), 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 651ee6f86476..3d39e84acb50 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<<4078cafe7570b1478adc0b278a25155a>> + * @generated SignedSource<<49c9151beb66235968403b1091de3f56>> */ /** @@ -96,6 +96,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableExclusivePropsUpdateAndroid( facebook::jni::alias_ref); + static bool enableFabricCommitBranching( + facebook::jni::alias_ref); + static bool enableFabricLogs( 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 932769fb954a..956ec09b2eb4 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<> */ /** @@ -114,6 +114,10 @@ bool ReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid() { return getAccessor().enableExclusivePropsUpdateAndroid(); } +bool ReactNativeFeatureFlags::enableFabricCommitBranching() { + return getAccessor().enableFabricCommitBranching(); +} + bool ReactNativeFeatureFlags::enableFabricLogs() { return getAccessor().enableFabricLogs(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 70ae43f8f378..056741e7925d 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<> + * @generated SignedSource<> */ /** @@ -149,6 +149,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableExclusivePropsUpdateAndroid(); + /** + * Enables Fabric commit branching to fix starvation problems and atomic JS updates. + */ + RN_EXPORT static bool enableFabricCommitBranching(); + /** * This feature flag enables logs for Fabric. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 4741bba3d359..315bdee96786 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<> */ /** @@ -425,6 +425,24 @@ bool ReactNativeFeatureFlagsAccessor::enableExclusivePropsUpdateAndroid() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranching() { + auto flagValue = enableFabricCommitBranching_.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(22, "enableFabricCommitBranching"); + + flagValue = currentProvider_->enableFabricCommitBranching(); + enableFabricCommitBranching_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { auto flagValue = enableFabricLogs_.load(); @@ -434,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(22, "enableFabricLogs"); + markFlagAsAccessed(23, "enableFabricLogs"); flagValue = currentProvider_->enableFabricLogs(); enableFabricLogs_ = flagValue; @@ -452,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricRenderer() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(23, "enableFabricRenderer"); + markFlagAsAccessed(24, "enableFabricRenderer"); flagValue = currentProvider_->enableFabricRenderer(); enableFabricRenderer_ = flagValue; @@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFontScaleChangesUpdatingLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(24, "enableFontScaleChangesUpdatingLayout"); + markFlagAsAccessed(25, "enableFontScaleChangesUpdatingLayout"); flagValue = currentProvider_->enableFontScaleChangesUpdatingLayout(); enableFontScaleChangesUpdatingLayout_ = flagValue; @@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSTextBaselineOffsetPerLine() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enableIOSTextBaselineOffsetPerLine"); + markFlagAsAccessed(26, "enableIOSTextBaselineOffsetPerLine"); flagValue = currentProvider_->enableIOSTextBaselineOffsetPerLine(); enableIOSTextBaselineOffsetPerLine_ = flagValue; @@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "enableIOSViewClipToPaddingBox"); + markFlagAsAccessed(27, "enableIOSViewClipToPaddingBox"); flagValue = currentProvider_->enableIOSViewClipToPaddingBox(); enableIOSViewClipToPaddingBox_ = flagValue; @@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImagePrefetchingAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "enableImagePrefetchingAndroid"); + markFlagAsAccessed(28, "enableImagePrefetchingAndroid"); flagValue = currentProvider_->enableImagePrefetchingAndroid(); enableImagePrefetchingAndroid_ = flagValue; @@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImagePrefetchingJNIBatchingAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "enableImagePrefetchingJNIBatchingAndroid"); + markFlagAsAccessed(29, "enableImagePrefetchingJNIBatchingAndroid"); flagValue = currentProvider_->enableImagePrefetchingJNIBatchingAndroid(); enableImagePrefetchingJNIBatchingAndroid_ = flagValue; @@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImagePrefetchingOnUiThreadAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "enableImagePrefetchingOnUiThreadAndroid"); + markFlagAsAccessed(30, "enableImagePrefetchingOnUiThreadAndroid"); flagValue = currentProvider_->enableImagePrefetchingOnUiThreadAndroid(); enableImagePrefetchingOnUiThreadAndroid_ = flagValue; @@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImmediateUpdateModeForContentOffsetC // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "enableImmediateUpdateModeForContentOffsetChanges"); + markFlagAsAccessed(31, "enableImmediateUpdateModeForContentOffsetChanges"); flagValue = currentProvider_->enableImmediateUpdateModeForContentOffsetChanges(); enableImmediateUpdateModeForContentOffsetChanges_ = flagValue; @@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeFocus() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "enableImperativeFocus"); + markFlagAsAccessed(32, "enableImperativeFocus"); flagValue = currentProvider_->enableImperativeFocus(); enableImperativeFocus_ = flagValue; @@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::enableInteropViewManagerClassLookUpOptimiz // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "enableInteropViewManagerClassLookUpOptimizationIOS"); + markFlagAsAccessed(33, "enableInteropViewManagerClassLookUpOptimizationIOS"); flagValue = currentProvider_->enableInteropViewManagerClassLookUpOptimizationIOS(); enableInteropViewManagerClassLookUpOptimizationIOS_ = flagValue; @@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIntersectionObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "enableIntersectionObserverByDefault"); + markFlagAsAccessed(34, "enableIntersectionObserverByDefault"); flagValue = currentProvider_->enableIntersectionObserverByDefault(); enableIntersectionObserverByDefault_ = flagValue; @@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::enableKeyEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "enableKeyEvents"); + markFlagAsAccessed(35, "enableKeyEvents"); flagValue = currentProvider_->enableKeyEvents(); enableKeyEvents_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "enableLayoutAnimationsOnAndroid"); + markFlagAsAccessed(36, "enableLayoutAnimationsOnAndroid"); flagValue = currentProvider_->enableLayoutAnimationsOnAndroid(); enableLayoutAnimationsOnAndroid_ = flagValue; @@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "enableLayoutAnimationsOnIOS"); + markFlagAsAccessed(37, "enableLayoutAnimationsOnIOS"); flagValue = currentProvider_->enableLayoutAnimationsOnIOS(); enableLayoutAnimationsOnIOS_ = flagValue; @@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMainQueueCoordinatorOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "enableMainQueueCoordinatorOnIOS"); + markFlagAsAccessed(38, "enableMainQueueCoordinatorOnIOS"); flagValue = currentProvider_->enableMainQueueCoordinatorOnIOS(); enableMainQueueCoordinatorOnIOS_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::enableModuleArgumentNSNullConversionIOS() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "enableModuleArgumentNSNullConversionIOS"); + markFlagAsAccessed(39, "enableModuleArgumentNSNullConversionIOS"); flagValue = currentProvider_->enableModuleArgumentNSNullConversionIOS(); enableModuleArgumentNSNullConversionIOS_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "enableNativeCSSParsing"); + markFlagAsAccessed(40, "enableNativeCSSParsing"); flagValue = currentProvider_->enableNativeCSSParsing(); enableNativeCSSParsing_ = flagValue; @@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNetworkEventReporting() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "enableNetworkEventReporting"); + markFlagAsAccessed(41, "enableNetworkEventReporting"); flagValue = currentProvider_->enableNetworkEventReporting(); enableNetworkEventReporting_ = flagValue; @@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(41, "enablePreparedTextLayout"); + markFlagAsAccessed(42, "enablePreparedTextLayout"); flagValue = currentProvider_->enablePreparedTextLayout(); enablePreparedTextLayout_ = flagValue; @@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(43, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSwiftUIBasedFilters() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "enableSwiftUIBasedFilters"); + markFlagAsAccessed(44, "enableSwiftUIBasedFilters"); flagValue = currentProvider_->enableSwiftUIBasedFilters(); enableSwiftUIBasedFilters_ = flagValue; @@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "enableViewCulling"); + markFlagAsAccessed(45, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "enableViewRecycling"); + markFlagAsAccessed(46, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -866,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForImage() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(46, "enableViewRecyclingForImage"); + markFlagAsAccessed(47, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForScrollView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(47, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(48, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -902,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(48, "enableViewRecyclingForText"); + markFlagAsAccessed(49, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -920,7 +938,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(49, "enableViewRecyclingForView"); + markFlagAsAccessed(50, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -938,7 +956,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewContainerStateExperimenta // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(50, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(51, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -956,7 +974,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewDebugFeatures() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(51, "enableVirtualViewDebugFeatures"); + markFlagAsAccessed(52, "enableVirtualViewDebugFeatures"); flagValue = currentProvider_->enableVirtualViewDebugFeatures(); enableVirtualViewDebugFeatures_ = flagValue; @@ -974,7 +992,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(52, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(53, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -992,7 +1010,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextClippingAndroid15useBoundsForWidth( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(53, "fixTextClippingAndroid15useBoundsForWidth"); + markFlagAsAccessed(54, "fixTextClippingAndroid15useBoundsForWidth"); flagValue = currentProvider_->fixTextClippingAndroid15useBoundsForWidth(); fixTextClippingAndroid15useBoundsForWidth_ = flagValue; @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(55, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "fuseboxEnabledRelease"); + markFlagAsAccessed(56, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "fuseboxNetworkInspectionEnabled"); + markFlagAsAccessed(57, "fuseboxNetworkInspectionEnabled"); flagValue = currentProvider_->fuseboxNetworkInspectionEnabled(); fuseboxNetworkInspectionEnabled_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "hideOffscreenVirtualViewsOnIOS"); + markFlagAsAccessed(58, "hideOffscreenVirtualViewsOnIOS"); flagValue = currentProvider_->hideOffscreenVirtualViewsOnIOS(); hideOffscreenVirtualViewsOnIOS_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(59, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "perfIssuesEnabled"); + markFlagAsAccessed(60, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "perfMonitorV2Enabled"); + markFlagAsAccessed(61, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1136,7 +1154,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "preparedTextCacheSize"); + markFlagAsAccessed(62, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(63, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(64, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(65, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(66, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(67, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(68, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(69, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(70, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "useFabricInterop"); + markFlagAsAccessed(71, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1316,7 +1334,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(71, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(72, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1334,7 +1352,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(72, "useNestedScrollViewAndroid"); + markFlagAsAccessed(73, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1352,7 +1370,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(73, "useSharedAnimatedBackend"); + markFlagAsAccessed(74, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1370,7 +1388,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(74, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(75, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1388,7 +1406,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(75, "useTurboModuleInterop"); + markFlagAsAccessed(76, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1406,7 +1424,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(76, "useTurboModules"); + markFlagAsAccessed(77, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1424,7 +1442,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(77, "viewCullingOutsetRatio"); + markFlagAsAccessed(78, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1442,7 +1460,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(78, "viewTransitionEnabled"); + markFlagAsAccessed(79, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1460,7 +1478,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(79, "virtualViewPrerenderRatio"); + markFlagAsAccessed(80, "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 a1133d323f2f..aa76f95a68f9 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<> + * @generated SignedSource<> */ /** @@ -54,6 +54,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableEagerMainQueueModulesOnIOS(); bool enableEagerRootViewAttachment(); bool enableExclusivePropsUpdateAndroid(); + bool enableFabricCommitBranching(); bool enableFabricLogs(); bool enableFabricRenderer(); bool enableFontScaleChangesUpdatingLayout(); @@ -123,7 +124,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 80> accessedFeatureFlags_; + std::array, 81> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -147,6 +148,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableEagerMainQueueModulesOnIOS_; std::atomic> enableEagerRootViewAttachment_; std::atomic> enableExclusivePropsUpdateAndroid_; + std::atomic> enableFabricCommitBranching_; std::atomic> enableFabricLogs_; std::atomic> enableFabricRenderer_; std::atomic> enableFontScaleChangesUpdatingLayout_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 6c304c6bf403..7ac1e669b7f9 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<<3e0a2c49ce52691f15ed127568a8bda8>> + * @generated SignedSource<<3cd7cb9afed3b596d9846a4701234677>> */ /** @@ -115,6 +115,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool enableFabricCommitBranching() override { + return false; + } + bool enableFabricLogs() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index e74c2bc6dffc..8094ea9aac1e 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<<071d8d1951fa9737eded3cd8a423eb8a>> + * @generated SignedSource<<86fa523d5b3be2d21f7b327de973e5bc>> */ /** @@ -243,6 +243,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableExclusivePropsUpdateAndroid(); } + bool enableFabricCommitBranching() override { + auto value = values_["enableFabricCommitBranching"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableFabricCommitBranching(); + } + bool enableFabricLogs() override { auto value = values_["enableFabricLogs"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 6e05fd21cdd9..9d238ad34905 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<<5678ce5959c4d862ea03b968a7fe07e1>> + * @generated SignedSource<> */ /** @@ -47,6 +47,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableEagerMainQueueModulesOnIOS() = 0; virtual bool enableEagerRootViewAttachment() = 0; virtual bool enableExclusivePropsUpdateAndroid() = 0; + virtual bool enableFabricCommitBranching() = 0; virtual bool enableFabricLogs() = 0; virtual bool enableFabricRenderer() = 0; virtual bool enableFontScaleChangesUpdatingLayout() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index a91509b843b9..5dff1c0eb96f 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<<54a1cd3bf2a43d8bcb92bc0a467a778c>> + * @generated SignedSource<> */ /** @@ -154,6 +154,11 @@ bool NativeReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid( return ReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid(); } +bool NativeReactNativeFeatureFlags::enableFabricCommitBranching( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableFabricCommitBranching(); +} + bool NativeReactNativeFeatureFlags::enableFabricLogs( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableFabricLogs(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 0b8e3a868645..43b161081893 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<<55bd70d1f64dcbcdcf9173e4f4b7aff6>> + * @generated SignedSource<> */ /** @@ -80,6 +80,8 @@ class NativeReactNativeFeatureFlags bool enableExclusivePropsUpdateAndroid(jsi::Runtime& runtime); + bool enableFabricCommitBranching(jsi::Runtime& runtime); + bool enableFabricLogs(jsi::Runtime& runtime); bool enableFabricRenderer(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index cf8a2b4ef3b2..0a43b9158c6a 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -286,6 +286,16 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enableFabricCommitBranching: { + defaultValue: false, + metadata: { + description: + 'Enables Fabric commit branching to fix starvation problems and atomic JS updates.', + expectedReleaseValue: true, + purpose: 'release', + }, + ossReleaseStage: 'none', + }, enableFabricLogs: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index fb09dbf38509..6497edf936f2 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<<1f591f0e6d65b58c811cb57d13f7f764>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -69,6 +69,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ enableEagerMainQueueModulesOnIOS: Getter, enableEagerRootViewAttachment: Getter, enableExclusivePropsUpdateAndroid: Getter, + enableFabricCommitBranching: Getter, enableFabricLogs: Getter, enableFabricRenderer: Getter, enableFontScaleChangesUpdatingLayout: Getter, @@ -281,6 +282,10 @@ export const enableEagerRootViewAttachment: Getter = createNativeFlagGe * When enabled, Android will disable Props 1.5 raw value merging when Props 2.0 is available. */ export const enableExclusivePropsUpdateAndroid: Getter = createNativeFlagGetter('enableExclusivePropsUpdateAndroid', false); +/** + * Enables Fabric commit branching to fix starvation problems and atomic JS updates. + */ +export const enableFabricCommitBranching: Getter = createNativeFlagGetter('enableFabricCommitBranching', false); /** * This feature flag enables logs for Fabric. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fa59ada0dee..44f08d416c96 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<> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -47,6 +47,7 @@ export interface Spec extends TurboModule { +enableEagerMainQueueModulesOnIOS?: () => boolean; +enableEagerRootViewAttachment?: () => boolean; +enableExclusivePropsUpdateAndroid?: () => boolean; + +enableFabricCommitBranching?: () => boolean; +enableFabricLogs?: () => boolean; +enableFabricRenderer?: () => boolean; +enableFontScaleChangesUpdatingLayout?: () => boolean; From ca6abd33fd171d23444da193d9b2ff990a502371 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Sun, 15 Feb 2026 23:40:07 -0800 Subject: [PATCH 3/3] Implement Fabric commit branching (behind a flag) (#54835) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/54835 Changelog: [General][Changed] - Changed how React changes are commited to the Shadow Tree (behind a feature flag) Introduces two branches for the Shadow Tree commits - React always commits to the JS revision, while other sources should commit to the main revision. Commits to the JS revision schedule a "merge" commit at the end of the event loop pass to upstream the React changes to the main revision. "Merge" means taking the JS revision and promoting it to become the new main, losing changes that happened between fork and merge. Preservation of these changes is the responsibility of the party that created them, and they should be reapplied using a commit hook. The main synchronization points here are: - react revision promotion/promoted revision merge - During merge, the promoted revision is read and reset (UI thread), while promotion itself updated the promoted revision (JS thread) - react commit/promoted revision merge - In case the promoted revision being merged is the same as the current React revision, the latter needs to be reset (UI thread), so that during the next JS commit it forks from the "main" branch (JS thread). - react commit/react revision promotion - Both access the `currentReactRevision`, in case of a main thread render, those might happen to run concurrently on JS/UI thread Reviewed By: javache, rubennorte Differential Revision: D88151489 --- .../__tests__/SyncOnCommit-itest.js | 2 +- .../NativeFantomTestSpecificMethods.cpp | 32 +++ .../NativeFantomTestSpecificMethods.h | 2 + .../react/renderer/mounting/ShadowTree.cpp | 133 +++++++-- .../react/renderer/mounting/ShadowTree.h | 35 ++- .../renderer/mounting/ShadowTreeDelegate.h | 11 + .../tests/StateReconciliationTest.cpp | 6 + .../react/renderer/scheduler/Scheduler.cpp | 15 ++ .../react/renderer/scheduler/Scheduler.h | 2 + .../react/renderer/uimanager/UIManager.cpp | 20 ++ .../react/renderer/uimanager/UIManager.h | 7 + .../renderer/uimanager/UIManagerDelegate.h | 10 + ...zyShadowTreeRevisionConsistencyManager.cpp | 4 +- ...LazyShadowTreeRevisionConsistencyManager.h | 1 - ...adowTreeRevisionConsistencyManagerTest.cpp | 6 + .../react/runtime/ReactHost.cpp | 3 + .../ShadowNodeReferenceCounter-itest.js | 1 + .../ShadowNodeRevisionGetter-itest.js | 1 + .../__tests__/ShadowTreeBranching-itest.js | 252 ++++++++++++++++++ .../__tests__/UIConsistency-itest.js | 1 + .../mounting/__tests__/Mounting-itest.js | 1 + .../specs/NativeFantomTestSpecificMethods.js | 2 + .../__tests__/ReactNativeElement-itest.js | 1 + 23 files changed, 521 insertions(+), 27 deletions(-) create mode 100644 packages/react-native/src/private/renderer/branching/__tests__/ShadowTreeBranching-itest.js diff --git a/packages/react-native/Libraries/ReactNative/__tests__/SyncOnCommit-itest.js b/packages/react-native/Libraries/ReactNative/__tests__/SyncOnCommit-itest.js index faada0aa8f35..cd0cfebe74ac 100644 --- a/packages/react-native/Libraries/ReactNative/__tests__/SyncOnCommit-itest.js +++ b/packages/react-native/Libraries/ReactNative/__tests__/SyncOnCommit-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 updateRuntimeShadowNodeReferencesOnCommit:true + * @fantom_flags updateRuntimeShadowNodeReferencesOnCommit:true enableFabricCommitBranching:* * @fantom_react_fb_flags passChildrenWhenCloningPersistedNodes:true * @flow strict-local * @format diff --git a/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.cpp b/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.cpp index 680ba595be46..a3ac0b0effee 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.cpp @@ -49,4 +49,36 @@ void NativeFantomTestSpecificMethods::takeFunctionAndNoop( jsi::Runtime& runtime, jsi::Function function) {} +void NativeFantomTestSpecificMethods::setRootNodeSize( + jsi::Runtime& runtime, + int surfaceId, + float width, + float height) { + auto& uiManager = getUIManagerFromRuntime(runtime); + + uiManager.getShadowTreeRegistry().visit( + surfaceId, [&](const ShadowTree& shadowTree) { + shadowTree.commit( + [&](const RootShadowNode& oldRootShadowNode) + -> RootShadowNode::Unshared { + PropsParserContext propsParserContext{ + surfaceId, *uiManager.getContextContainer()}; + + const auto& mainBranchRootProps = + oldRootShadowNode.getConcreteProps(); + const auto layoutContext = mainBranchRootProps.layoutContext; + auto layoutConstraints = mainBranchRootProps.layoutConstraints; + + layoutConstraints.maximumSize.width = width; + layoutConstraints.maximumSize.height = height; + layoutConstraints.minimumSize.width = width; + layoutConstraints.minimumSize.height = height; + + return oldRootShadowNode.clone( + propsParserContext, layoutConstraints, layoutContext); + }, + {.source = ShadowTreeCommitSource::Unknown}); + }); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.h b/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.h index a82fa5361227..aff0dbf4fc9f 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.h +++ b/packages/react-native/ReactCommon/react/nativemodule/fantomtestspecificmethods/NativeFantomTestSpecificMethods.h @@ -21,6 +21,8 @@ class NativeFantomTestSpecificMethods : public NativeFantomTestSpecificMethodsCx void takeFunctionAndNoop(jsi::Runtime &runtime, jsi::Function callback); + void setRootNodeSize(jsi::Runtime &runtime, int surfaceId, float width, float height); + private: std::shared_ptr fantomForcedCloneCommitHook_{}; }; diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index 58c5bbaa4e08..ec806e3be222 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -220,7 +220,7 @@ void ShadowTree::setCommitMode(CommitMode commitMode) const { auto revision = ShadowTreeRevision{}; { - ShadowTree::UniqueLock lock = uniqueCommitLock(); + ShadowTree::UniqueLock lock = uniqueRevisionLock(); if (commitMode_ == commitMode) { return; @@ -238,7 +238,7 @@ void ShadowTree::setCommitMode(CommitMode commitMode) const { } CommitMode ShadowTree::getCommitMode() const { - SharedLock lock = sharedCommitLock(); + SharedLock lock = sharedRevisionLock(); return commitMode_; } @@ -262,7 +262,7 @@ CommitStatus ShadowTree::commit( } { - std::unique_lock lock(commitMutexRecursive_); + std::unique_lock lock(revisionMutexRecursive_); return tryCommit(transaction, commitOptions); } } else { @@ -286,18 +286,32 @@ CommitStatus ShadowTree::tryCommit( const CommitOptions& commitOptions) const { TraceSection s("ShadowTree::commit"); + auto isReactBranch = ReactNativeFeatureFlags::enableFabricCommitBranching() && + commitOptions.source == CommitSource::React; + + // Commits on the JS branch are never synchronous. + react_native_assert(!isReactBranch || !commitOptions.mountSynchronously); + auto telemetry = TransactionTelemetry{}; telemetry.willCommit(); CommitMode commitMode; auto oldRevision = ShadowTreeRevision{}; + auto oldRevisionForStateProgression = ShadowTreeRevision{}; auto newRevision = ShadowTreeRevision{}; { // Reading `currentRevision_` in shared manner. - SharedLock lock = sharedCommitLock(); + SharedLock lock = sharedRevisionLock(); commitMode = commitMode_; - oldRevision = currentRevision_; + + if (isReactBranch && currentReactRevision_.has_value()) { + oldRevision = currentReactRevision_.value(); + } else { + oldRevision = currentRevision_; + } + + oldRevisionForStateProgression = currentRevision_; } const auto& oldRootShadowNode = oldRevision.rootShadowNode; @@ -308,8 +322,8 @@ CommitStatus ShadowTree::tryCommit( } if (commitOptions.enableStateReconciliation) { - auto updatedNewRootShadowNode = - progressState(*newRootShadowNode, *oldRootShadowNode); + auto updatedNewRootShadowNode = progressState( + *newRootShadowNode, *oldRevisionForStateProgression.rootShadowNode); if (updatedNewRootShadowNode) { newRootShadowNode = std::static_pointer_cast(updatedNewRootShadowNode); @@ -336,9 +350,10 @@ CommitStatus ShadowTree::tryCommit( { // Updating `currentRevision_` in unique manner if it hasn't changed. - UniqueLock lock = uniqueCommitLock(); + UniqueLock lock = uniqueRevisionLock( + /*defer*/ isReactBranch); - if (currentRevision_.number != oldRevision.number) { + if (!isReactBranch && currentRevision_.number != oldRevision.number) { return CommitStatus::Failed; } @@ -364,12 +379,21 @@ CommitStatus ShadowTree::tryCommit( .number = newRevisionNumber, .telemetry = telemetry}; - currentRevision_ = newRevision; + if (isReactBranch) { + // Lock the deferred lock to ensure that the new React revision is not + // reset during a scheduled merge. + std::visit([](auto& concreteLock) { concreteLock.lock(); }, lock); + currentReactRevision_ = newRevision; + } else { + currentRevision_ = newRevision; + } } emitLayoutEvents(affectedLayoutableNodes); - if (commitMode == CommitMode::Normal) { + if (isReactBranch) { + scheduleReactRevisionPromotion(); + } else if (commitMode == CommitMode::Normal) { mount(std::move(newRevision), commitOptions.mountSynchronously); } @@ -377,10 +401,15 @@ CommitStatus ShadowTree::tryCommit( } ShadowTreeRevision ShadowTree::getCurrentRevision() const { - SharedLock lock = sharedCommitLock(); + SharedLock lock = sharedRevisionLock(); return currentRevision_; } +std::optional ShadowTree::getCurrentReactRevision() const { + SharedLock lock = sharedRevisionLock(); + return currentReactRevision_; +} + void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously) const { mountingCoordinator_->push(std::move(revision)); @@ -388,6 +417,72 @@ void ShadowTree::mount(ShadowTreeRevision revision, bool mountSynchronously) mountingCoordinator_, mountSynchronously); } +void ShadowTree::mergeReactRevision() const { + ShadowTreeRevision promotedRevision; + + { + UniqueLock lock = uniqueRevisionLock(false); + + if (!reactRevisionToBePromoted_.has_value()) { + return; + } + + promotedRevision = + std::exchange(reactRevisionToBePromoted_, std::nullopt).value(); + } + + const auto mergedRevisionNumber = promotedRevision.number; + + this->commit( + [revision = std::move(promotedRevision)]( + const RootShadowNode& /*oldRootShadowNode*/) { + return std::make_shared( + *revision.rootShadowNode, ShadowNodeFragment{}); + }, + { + .enableStateReconciliation = true, + .mountSynchronously = true, + .source = CommitSource::ReactRevisionMerge, + }); + + { + UniqueLock commitLock = uniqueRevisionLock( + /*defer*/ false); + + // If the current react revision is the same as the one that was just + // merged, clear it. + if (currentReactRevision_.has_value() && + mergedRevisionNumber == currentReactRevision_.value().number) { + currentReactRevision_.reset(); + } + } +} + +void ShadowTree::promoteReactRevision() const { + ShadowTreeRevision currentReactRevision; + { + SharedLock lock = sharedRevisionLock(); + // Promotion happens at the end of the event loop tick, it's possible to + // have more than one promotion in a row. In this case, all but the first + // one should no-op. + if (!currentReactRevision_.has_value()) { + return; + } + currentReactRevision = currentReactRevision_.value(); + } + + { + UniqueLock lock = uniqueRevisionLock(false); + reactRevisionToBePromoted_ = std::move(currentReactRevision); + } + + delegate_.shadowTreeDidPromoteReactRevision(*this); +} + +void ShadowTree::scheduleReactRevisionPromotion() const { + delegate_.shadowTreeDidFinishReactCommit(*this); +} + void ShadowTree::commitEmptyTree() const { commit( [](const RootShadowNode& oldRootShadowNode) @@ -426,19 +521,21 @@ void ShadowTree::notifyDelegatesOfUpdates() const { delegate_.shadowTreeDidFinishTransaction(mountingCoordinator_, true); } -inline ShadowTree::UniqueLock ShadowTree::uniqueCommitLock() const { +inline ShadowTree::UniqueLock ShadowTree::uniqueRevisionLock(bool defer) const { if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) { - return std::unique_lock{commitMutexRecursive_}; + return defer ? std::unique_lock{revisionMutexRecursive_, std::defer_lock} + : std::unique_lock{revisionMutexRecursive_}; } else { - return std::unique_lock{commitMutex_}; + return defer ? std::unique_lock{revisionMutex_, std::defer_lock} + : std::unique_lock{revisionMutex_}; } } -inline ShadowTree::SharedLock ShadowTree::sharedCommitLock() const { +inline ShadowTree::SharedLock ShadowTree::sharedRevisionLock() const { if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) { - return std::unique_lock{commitMutexRecursive_}; + return std::unique_lock{revisionMutexRecursive_}; } else { - return std::shared_lock{commitMutex_}; + return std::shared_lock{revisionMutex_}; } } diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h index c1f51fc1a91e..b97680b8c25e 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h @@ -51,6 +51,7 @@ enum class ShadowTreeCommitSource { Unknown, React, AnimationEndSync, + ReactRevisionMerge, }; struct ShadowTreeCommitOptions { @@ -124,6 +125,12 @@ class ShadowTree final { */ ShadowTreeRevision getCurrentRevision() const; + /* + * Returns a `ShadowTreeRevision` representing the momentary state of + * the `ShadowTree` in the JS thread. + */ + std::optional getCurrentReactRevision() const; + /* * Commit an empty tree (a new `RootShadowNode` with no children). */ @@ -137,6 +144,18 @@ class ShadowTree final { std::shared_ptr getMountingCoordinator() const; + /** + * Promotes the current React revision to be merged into the main branch of the + * ShadowTree. + */ + void promoteReactRevision() const; + + /** + * Commits the currently promoted React revision to the "main" branch of the + * ShadowTree. No-op if the promoted React revision doesn't exist. + */ + void mergeReactRevision() const; + private: constexpr static ShadowTreeRevision::Number INITIAL_REVISION{0}; @@ -144,19 +163,23 @@ class ShadowTree final { void emitLayoutEvents(std::vector &affectedLayoutableNodes) const; + void scheduleReactRevisionPromotion() const; + const SurfaceId surfaceId_; const ShadowTreeDelegate &delegate_; - mutable std::shared_mutex commitMutex_; - mutable std::recursive_mutex commitMutexRecursive_; - mutable CommitMode commitMode_{CommitMode::Normal}; // Protected by `commitMutex_`. - mutable ShadowTreeRevision currentRevision_; // Protected by `commitMutex_`. + mutable std::shared_mutex revisionMutex_; + mutable std::recursive_mutex revisionMutexRecursive_; + mutable CommitMode commitMode_{CommitMode::Normal}; // Protected by `revisionMutex_`. + mutable ShadowTreeRevision currentRevision_; // Protected by `revisionMutex_`. + mutable std::optional currentReactRevision_; // Protected by `revisionMutex_`. + mutable std::optional reactRevisionToBePromoted_; // Protected by `revisionMutex_`. std::shared_ptr mountingCoordinator_; using UniqueLock = std::variant, std::unique_lock>; using SharedLock = std::variant, std::unique_lock>; - inline UniqueLock uniqueCommitLock() const; - inline SharedLock sharedCommitLock() const; + inline UniqueLock uniqueRevisionLock(bool defer = false) const; + inline SharedLock sharedRevisionLock() const; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h index 650d898a6f34..3dde9c5837c8 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h @@ -38,6 +38,17 @@ class ShadowTreeDelegate { std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; + /* + * Called right after Shadow Tree commits a new React revision of the tree. + */ + virtual void shadowTreeDidFinishReactCommit(const ShadowTree &shadowTree) const = 0; + + /* + * Called right after Shadow Tree promotes a React revision of the tree to + * be merged. + */ + virtual void shadowTreeDidPromoteReactRevision(const ShadowTree &shadowTree) const = 0; + virtual ~ShadowTreeDelegate() noexcept = default; }; diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp index 7290d76cc637..721a46700be8 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/tests/StateReconciliationTest.cpp @@ -37,6 +37,12 @@ class DummyShadowTreeDelegate : public ShadowTreeDelegate { void shadowTreeDidFinishTransaction( std::shared_ptr mountingCoordinator, bool /*mountSynchronously*/) const override {} + + void shadowTreeDidFinishReactCommit( + const ShadowTree& /*shadowTree*/) const override {} + + void shadowTreeDidPromoteReactRevision( + const ShadowTree& /*shadowTree*/) const override {} }; namespace { diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index bf0e3d93a043..af2a59f0a282 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -366,6 +366,21 @@ void Scheduler::uiManagerShouldRemoveEventListener( removeEventListener(listener); } +void Scheduler::uiManagerDidFinishReactCommit(const ShadowTree& shadowTree) { + auto surfaceId = shadowTree.getSurfaceId(); + runtimeScheduler_->scheduleRenderingUpdate( + surfaceId, [surfaceId, uiManager = uiManager_]() { + uiManager->getShadowTreeRegistry().visit( + surfaceId, + [](const ShadowTree& tree) { tree.promoteReactRevision(); }); + }); +} + +void Scheduler::uiManagerDidPromoteReactRevision(const ShadowTree& shadowTree) { + // Replaced by scheduling on the UI thread in the following diff. + shadowTree.mergeReactRevision(); +} + void Scheduler::uiManagerDidStartSurface(const ShadowTree& shadowTree) { std::shared_lock lock(onSurfaceStartCallbackMutex_); if (onSurfaceStartCallback_) { diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index 80be380a48e1..080c5407c131 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -95,6 +95,8 @@ class Scheduler final : public UIManagerDelegate { void uiManagerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; void uiManagerShouldAddEventListener(std::shared_ptr listener) final; void uiManagerShouldRemoveEventListener(const std::shared_ptr &listener) final; + void uiManagerDidFinishReactCommit(const ShadowTree &shadowTree) override; + void uiManagerDidPromoteReactRevision(const ShadowTree &shadowTree) override; void uiManagerDidStartSurface(const ShadowTree &shadowTree) override; #pragma mark - ContextContainer diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 8dddd3ced432..1239c8a73f71 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -644,6 +644,20 @@ void UIManager::shadowTreeDidFinishTransaction( } } +void UIManager::shadowTreeDidFinishReactCommit( + const ShadowTree& shadowTree) const { + if (delegate_ != nullptr) { + delegate_->uiManagerDidFinishReactCommit(shadowTree); + } +} + +void UIManager::shadowTreeDidPromoteReactRevision( + const ShadowTree& shadowTree) const { + if (delegate_ != nullptr) { + delegate_->uiManagerDidPromoteReactRevision(shadowTree); + } +} + void UIManager::reportMount(SurfaceId surfaceId) const { TraceSection s("UIManager::reportMount"); @@ -718,6 +732,12 @@ void UIManager::synchronouslyUpdateViewOnUIThread( } } +#pragma mark ContextContainer + +std::shared_ptr UIManager::getContextContainer() const { + return contextContainer_; +} + #pragma mark - Add & Remove event listener void UIManager::addEventListener( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index eac4b5d9a49a..bb287fe40f2d 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -135,6 +135,10 @@ class UIManager final : public ShadowTreeDelegate { const RootShadowNode::Unshared &newRootShadowNode, const ShadowTree::CommitOptions &commitOptions) const override; + void shadowTreeDidFinishReactCommit(const ShadowTree &shadowTree) const override; + + void shadowTreeDidPromoteReactRevision(const ShadowTree &shadowTree) const override; + std::shared_ptr createNode( Tag tag, const std::string &componentName, @@ -201,6 +205,9 @@ class UIManager final : public ShadowTreeDelegate { void updateShadowTree(std::unordered_map &&tagToProps); +#pragma mark - ContextContainer + std::shared_ptr getContextContainer() const; + #pragma mark - Add & Remove event listener void addEventListener(std::shared_ptr listener); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h index 32df5c92f109..92ee339f0929 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerDelegate.h @@ -84,6 +84,16 @@ class UIManagerDelegate { */ virtual void uiManagerDidStartSurface(const ShadowTree &shadowTree) = 0; + /* + * Called after a new React revision of the shadow tree is committed. + */ + virtual void uiManagerDidFinishReactCommit(const ShadowTree &shadowTree) = 0; + + /* + * Called after a React revision of the shadow tree is promoted to be merged. + */ + virtual void uiManagerDidPromoteReactRevision(const ShadowTree &shadowTree) = 0; + using OnSurfaceStartCallback = std::function; virtual void uiManagerShouldSetOnSurfaceStartCallback(OnSurfaceStartCallback &&callback) = 0; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp index 8afaad89b39b..cf11c7f9d28e 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp @@ -24,7 +24,9 @@ LazyShadowTreeRevisionConsistencyManager::updateCurrentRevision( // root shadow nodes concurrently. RootShadowNode::Shared rootShadowNode; shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { - rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + auto reactRevision = shadowTree.getCurrentReactRevision(); + rootShadowNode = + reactRevision.value_or(shadowTree.getCurrentRevision()).rootShadowNode; }); std::unique_lock lock(capturedRootShadowNodesForConsistencyMutex_); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h index b63ec2977dbe..c7ab5af7fac2 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h @@ -13,7 +13,6 @@ #include #include #include -#include namespace facebook::react { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp index 322172b3de1d..349a79f3d495 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/tests/LazyShadowTreeRevisionConsistencyManagerTest.cpp @@ -27,6 +27,12 @@ class FakeShadowTreeDelegate : public ShadowTreeDelegate { void shadowTreeDidFinishTransaction( std::shared_ptr mountingCoordinator, bool /*mountSynchronously*/) const override {} + + void shadowTreeDidFinishReactCommit( + const ShadowTree& /*shadowTree*/) const override {} + + void shadowTreeDidPromoteReactRevision( + const ShadowTree& /*shadowTree*/) const override {} }; class LazyShadowTreeRevisionConsistencyManagerTest : public ::testing::Test { diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index 7affd88dca04..74427d2a72b5 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -311,6 +311,9 @@ void ReactHost::destroyReactInstance() { reactInstanceData_->contextContainer->erase(RuntimeSchedulerKey); reactInstanceData_->mountingManager->setSchedulerTaskExecutor(nullptr); + reactInstanceData_->mountingManager = nullptr; + reactInstanceData_->contextContainer = nullptr; + reactInstanceData_->turboModuleProviders.clear(); reactInstance_ = nullptr; reactInstanceData_->messageQueueThread = nullptr; } diff --git a/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-itest.js b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-itest.js index 5bb74b146010..00112dd29008 100644 --- a/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-itest.js +++ b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-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 enableFabricCommitBranching:* * @flow strict-local * @format */ diff --git a/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js index 3475f0cb385b..f5bd1e3bfbd5 100644 --- a/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js +++ b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-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 enableFabricCommitBranching:* * @flow strict-local * @format */ diff --git a/packages/react-native/src/private/renderer/branching/__tests__/ShadowTreeBranching-itest.js b/packages/react-native/src/private/renderer/branching/__tests__/ShadowTreeBranching-itest.js new file mode 100644 index 000000000000..d37fa968119a --- /dev/null +++ b/packages/react-native/src/private/renderer/branching/__tests__/ShadowTreeBranching-itest.js @@ -0,0 +1,252 @@ +/** + * 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. + * + * @fantom_flags enableFabricCommitBranching:true + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; + +import ensureInstance from '../../../__tests__/utilities/ensureInstance'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {View} from 'react-native'; +import NativeFantomTestSpecificMethods from 'react-native/src/private/testing/fantom/specs/NativeFantomTestSpecificMethods'; +import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; + +describe('ShadowTreeBranching', () => { + function LayoutEffectObserver({onCommit}: {onCommit: () => void}) { + React.useLayoutEffect(() => { + onCommit(); + }); + return null; + } + + it('should not mount React revision before merging them', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "View", nativeID: "view"}', + 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "view"}', + ]); + + let updatesBeforeEndOfTask: $ReadOnlyArray = []; + + Fantom.runTask(() => { + root.render( + <> + + { + updatesBeforeEndOfTask = root.takeMountingManagerLogs(); + }} + /> + , + ); + }); + + const updatesAfterEndOfTask = root.takeMountingManagerLogs(); + + // The intermediate React revision should not be mounted + expect(updatesBeforeEndOfTask).toEqual([]); + expect(updatesAfterEndOfTask).toEqual([ + 'Update {type: "View", nativeID: "view"}', + ]); + }); + + it('should not skip intermediate React commits', () => { + const root = Fantom.createRoot(); + + let updatesBeforeEndOfTask: $ReadOnlyArray = []; + + // When rendered with `shouldTriggerIntermediate=false`, it renders two views + // both with blue background. When rendered with `shouldTriggerIntermediate=true`, + // it renders two views, the first one with red background and the second one + // with blue background. The layout effect will update the second view to red. + // The intermediate React revision with different background colors should not + // be committed. + function AutoUpdateComponent({ + shouldTriggerIntermediate, + }: { + shouldTriggerIntermediate: boolean, + }) { + const [intermediateRender, setIntermediateRender] = React.useState(false); + + return ( + + + + { + if (shouldTriggerIntermediate) { + if (!intermediateRender) { + setIntermediateRender(true); + return; + } + + updatesBeforeEndOfTask = root.takeMountingManagerLogs(); + } + }} + /> + + ); + } + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "View", nativeID: "view"}', + 'Create {type: "View", nativeID: "A"}', + 'Create {type: "View", nativeID: "B"}', + 'Insert {type: "View", parentNativeID: "view", index: 0, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "view", index: 1, nativeID: "B"}', + 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "view"}', + ]); + + Fantom.runTask(() => { + root.render(); + }); + + const updatesAfterEndOfTask = root.takeMountingManagerLogs(); + + // The intermediate React revision should not be committed + expect(updatesBeforeEndOfTask).toEqual([]); + expect(updatesAfterEndOfTask).toEqual([ + 'Update {type: "View", nativeID: "A"}', + 'Update {type: "View", nativeID: "B"}', + ]); + }); + + it('should provide up-to-date data when read from JS', () => { + const root = Fantom.createRoot(); + + const viewRef = React.createRef(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "View", nativeID: "view"}', + 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "view"}', + ]); + + let dimensions: [number, number] = [0, 0]; + let mountingManagerLogs: Array = []; + + Fantom.runTask(() => { + root.render( + <> + + { + mountingManagerLogs = root.takeMountingManagerLogs(); + + // The React revision is not merged yet but the new dimensions + // should be visible from JS + const viewNode = ensureInstance( + viewRef.current, + ReactNativeElement, + ); + const rect = viewNode.getBoundingClientRect(); + dimensions = [rect.width, rect.height]; + }} + /> + , + ); + }); + + expect(mountingManagerLogs).toEqual([]); + expect(dimensions).toEqual([50, 50]); + }); + + it('should not mount intermediate React tree when committing from another source', () => { + const root = Fantom.createRoot(); + + const viewRef = React.createRef(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Create {type: "View", nativeID: "view"}', + 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "view"}', + ]); + + let mountingManagerLogsBeforeEndOfTask: Array = []; + + Fantom.runTask(() => { + root.render( + <> + + { + NativeFantomTestSpecificMethods.setRootNodeSize( + root.getRootTag() as $FlowFixMe, + 150, + 150, + ); + + mountingManagerLogsBeforeEndOfTask = + root.takeMountingManagerLogs(); + }} + /> + , + ); + }); + + // Commit made from outside of React - it should not mount intermediate React tree + expect(mountingManagerLogsBeforeEndOfTask).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + ]); + + // Commit made from React - it should mount the final React tree + // NOTE: The RootView update should not be included here, fixed + // in a following diff + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "RootView", nativeID: (root)}', + 'Update {type: "View", nativeID: "view"}', + ]); + }); +}); diff --git a/packages/react-native/src/private/renderer/consistency/__tests__/UIConsistency-itest.js b/packages/react-native/src/private/renderer/consistency/__tests__/UIConsistency-itest.js index 526d48731963..31a945eecde7 100644 --- a/packages/react-native/src/private/renderer/consistency/__tests__/UIConsistency-itest.js +++ b/packages/react-native/src/private/renderer/consistency/__tests__/UIConsistency-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 enableFabricCommitBranching:* * @flow strict-local * @format */ 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 16e2474401b8..faea08c6d4a2 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,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 enableFabricCommitBranching:* * @flow strict-local * @format */ diff --git a/packages/react-native/src/private/testing/fantom/specs/NativeFantomTestSpecificMethods.js b/packages/react-native/src/private/testing/fantom/specs/NativeFantomTestSpecificMethods.js index 6277b704cdc9..f10f0a8fecb7 100644 --- a/packages/react-native/src/private/testing/fantom/specs/NativeFantomTestSpecificMethods.js +++ b/packages/react-native/src/private/testing/fantom/specs/NativeFantomTestSpecificMethods.js @@ -9,6 +9,7 @@ */ import type {TurboModule} from '../../../../../Libraries/TurboModule/RCTExport'; +import type {Float, Int32} from '../../../../../Libraries/Types/CodegenTypes'; import * as TurboModuleRegistry from '../../../../../Libraries/TurboModule/TurboModuleRegistry'; @@ -24,6 +25,7 @@ import * as TurboModuleRegistry from '../../../../../Libraries/TurboModule/Turbo export interface Spec extends TurboModule { +registerForcedCloneCommitHook: () => void; +takeFunctionAndNoop: (fn: () => void) => void; + +setRootNodeSize: (surfaceId: Int32, width: Float, height: Float) => void; } export default (TurboModuleRegistry.getEnforcing( diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js index 1f650b95a3c4..196348fee7c1 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-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 enableFabricCommitBranching:* * @flow strict-local * @format */