From 5e63f34ab0d0d5a5b2ea248eea86980df7be1a7f Mon Sep 17 00:00:00 2001 From: Liron Yahdav Date: Wed, 19 Feb 2025 18:45:23 -0800 Subject: [PATCH] Throw exception with name of TurboModule instead of deadlock during sync rendering (#49509) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49509 Currently if we hit a deadlock in sync rendering due to a TurboModule initialization that requires main queue setup we don't get any information about which TurboModule caused the issue. To help us know which TurboModules we need to fix, this instead will crash with the name of the TurboModule. Changelog: [Internal] Reviewed By: sammy-SC Differential Revision: D69805065 fbshipit-source-id: f75df44f9a603a5f53a008382d32b2b5285c1162 --- .../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 | 38 ++++++++++++++----- .../ReactNativeFeatureFlagsAccessor.h | 6 ++- .../ReactNativeFeatureFlagsDefaults.h | 6 ++- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +++++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../ios/React-NativeModulesApple.podspec | 3 +- .../ios/ReactCommon/RCTTurboModuleManager.mm | 32 +++++++++++++++- .../NativeReactNativeFeatureFlags.cpp | 7 +++- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactCommon/RuntimeExecutor.cpp | 17 +++++++++ .../ReactCommon/RuntimeExecutor.h | 14 +++++++ .../ReactNativeFeatureFlags.config.js | 11 ++++++ .../featureflags/ReactNativeFeatureFlags.js | 7 +++- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 24 files changed, 210 insertions(+), 31 deletions(-) create mode 100644 packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.cpp 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 80420f09774..9ea7bc27cef 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<<6e8144a48ab65f3b5ad8dd601cee2114>> + * @generated SignedSource<<99417ea22d1bf969d71388c600d42e85>> */ /** @@ -226,6 +226,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun lazyAnimationCallbacks(): Boolean = accessor.lazyAnimationCallbacks() + /** + * Throw an exception instead of deadlocking when a TurboModule that requires main queue setup is initialized during a synchronous render on iOS. + */ + @JvmStatic + public fun throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(): Boolean = accessor.throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() + /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ 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 5d6b275337e..5b8b2fe06c3 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<<3ede8912be1a2874050bab4c88a0a416>> */ /** @@ -53,6 +53,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null private var lazyAnimationCallbacksCache: Boolean? = null + private var throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOSCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null private var useEditTextStockAndroidFocusBehaviorCache: Boolean? = null @@ -360,6 +361,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(): Boolean { + var cached = throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOSCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() + throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOSCache = cached + } + return cached + } + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean { var cached = traceTurboModulePromiseRejectionsOnAndroidCache 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 6c0b4b1ba40..a38007ebfec 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<<33adb23bfdb99c531c674f4c6d09ddbd>> */ /** @@ -94,6 +94,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun lazyAnimationCallbacks(): Boolean + @DoNotStrip @JvmStatic public external fun throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(): Boolean + @DoNotStrip @JvmStatic public external fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean @DoNotStrip @JvmStatic public external fun useAlwaysAvailableJSErrorHandling(): 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 0ce19bf7171..ee69db5cab0 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<<5315bd93234b8c00b7d029081231eeeb>> + * @generated SignedSource<<94ffd31d788dae979b92c7a347f2ad40>> */ /** @@ -89,6 +89,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun lazyAnimationCallbacks(): Boolean = false + override fun throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(): Boolean = false + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean = false override fun useAlwaysAvailableJSErrorHandling(): 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 5c212ccae04..aca0cdd4d21 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<> + * @generated SignedSource<<15176fdf014740aa34039cfd82943ae2>> */ /** @@ -57,6 +57,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null private var lazyAnimationCallbacksCache: Boolean? = null + private var throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOSCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null private var useEditTextStockAndroidFocusBehaviorCache: Boolean? = null @@ -397,6 +398,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(): Boolean { + var cached = throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOSCache + if (cached == null) { + cached = currentProvider.throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() + accessedFeatureFlags.add("throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS") + throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOSCache = cached + } + return cached + } + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean { var cached = traceTurboModulePromiseRejectionsOnAndroidCache 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 24d527c0899..6634920dac8 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<<51121a77716b7748f993496eff5f6bfb>> + * @generated SignedSource<<98ad5281b51418af43620a89a40a3c45>> */ /** @@ -89,6 +89,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun lazyAnimationCallbacks(): Boolean + @DoNotStrip public fun throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(): Boolean + @DoNotStrip public fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean @DoNotStrip public fun useAlwaysAvailableJSErrorHandling(): 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 8c5753cd10f..92f40898540 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -237,6 +237,12 @@ class ReactNativeFeatureFlagsProviderHolder return method(javaProvider_); } + bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS"); + return method(javaProvider_); + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("traceTurboModulePromiseRejectionsOnAndroid"); @@ -460,6 +466,11 @@ bool JReactNativeFeatureFlagsCxxInterop::lazyAnimationCallbacks( return ReactNativeFeatureFlags::lazyAnimationCallbacks(); } +bool JReactNativeFeatureFlagsCxxInterop::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(); +} + bool JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid(); @@ -635,6 +646,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "lazyAnimationCallbacks", JReactNativeFeatureFlagsCxxInterop::lazyAnimationCallbacks), + makeNativeMethod( + "throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS", + JReactNativeFeatureFlagsCxxInterop::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS), makeNativeMethod( "traceTurboModulePromiseRejectionsOnAndroid", JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid), 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 8e6ca51f366..f768b70716f 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<<0bf1c23eda1c41a60eaae19e87c541e9>> + * @generated SignedSource<> */ /** @@ -129,6 +129,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool lazyAnimationCallbacks( facebook::jni::alias_ref); + static bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS( + facebook::jni::alias_ref); + static bool traceTurboModulePromiseRejectionsOnAndroid( 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 826a0e3f369..d17a47dba8a 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<<0a3119b99e455127d8a404963a23eb96>> + * @generated SignedSource<<5ea68e0bf3cbf7822d7a3e80f7dffc72>> */ /** @@ -158,6 +158,10 @@ bool ReactNativeFeatureFlags::lazyAnimationCallbacks() { return getAccessor().lazyAnimationCallbacks(); } +bool ReactNativeFeatureFlags::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() { + return getAccessor().throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(); +} + bool ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid() { return getAccessor().traceTurboModulePromiseRejectionsOnAndroid(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 092ca0e553a..ff9e9726f69 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<<9be3692ca63703f32de54e24d4ceb6e9>> */ /** @@ -204,6 +204,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool lazyAnimationCallbacks(); + /** + * Throw an exception instead of deadlocking when a TurboModule that requires main queue setup is initialized during a synchronous render on iOS. + */ + RN_EXPORT static bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(); + /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 2fccefca4d4..67b9548d4f8 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<<41f3b407b98e268ba80f8cd8bb960321>> + * @generated SignedSource<<12be545f9fb06b76cb6c521e1d381bf1>> */ /** @@ -623,6 +623,24 @@ bool ReactNativeFeatureFlagsAccessor::lazyAnimationCallbacks() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() { + auto flagValue = throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS_.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(33, "throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS"); + + flagValue = currentProvider_->throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(); + throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid() { auto flagValue = traceTurboModulePromiseRejectionsOnAndroid_.load(); @@ -632,7 +650,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(33, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(34, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -650,7 +668,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(34, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(35, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::useEditTextStockAndroidFocusBehavior() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "useEditTextStockAndroidFocusBehavior"); + markFlagAsAccessed(36, "useEditTextStockAndroidFocusBehavior"); flagValue = currentProvider_->useEditTextStockAndroidFocusBehavior(); useEditTextStockAndroidFocusBehavior_ = flagValue; @@ -686,7 +704,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(36, "useFabricInterop"); + markFlagAsAccessed(37, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -704,7 +722,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(37, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(38, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "useOptimizedEventBatchingOnAndroid"); + markFlagAsAccessed(39, "useOptimizedEventBatchingOnAndroid"); flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid(); useOptimizedEventBatchingOnAndroid_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "useRawPropsJsiValue"); + markFlagAsAccessed(40, "useRawPropsJsiValue"); flagValue = currentProvider_->useRawPropsJsiValue(); useRawPropsJsiValue_ = flagValue; @@ -758,7 +776,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(40, "useTurboModuleInterop"); + markFlagAsAccessed(41, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -776,7 +794,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(41, "useTurboModules"); + markFlagAsAccessed(42, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 80fcc077ae2..013db19ef7a 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<<65caac2e749261ca919780dff302ef57>> */ /** @@ -65,6 +65,7 @@ class ReactNativeFeatureFlagsAccessor { bool fuseboxEnabledRelease(); bool fuseboxNetworkInspectionEnabled(); bool lazyAnimationCallbacks(); + bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(); bool traceTurboModulePromiseRejectionsOnAndroid(); bool useAlwaysAvailableJSErrorHandling(); bool useEditTextStockAndroidFocusBehavior(); @@ -85,7 +86,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 42> accessedFeatureFlags_; + std::array, 43> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> disableMountItemReorderingAndroid_; @@ -120,6 +121,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> fuseboxEnabledRelease_; std::atomic> fuseboxNetworkInspectionEnabled_; std::atomic> lazyAnimationCallbacks_; + std::atomic> throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS_; std::atomic> traceTurboModulePromiseRejectionsOnAndroid_; std::atomic> useAlwaysAvailableJSErrorHandling_; std::atomic> useEditTextStockAndroidFocusBehavior_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 678b9169300..68e6ce11ef7 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<<1ade51e6e5c82275c5c01b72df630eee>> + * @generated SignedSource<> */ /** @@ -159,6 +159,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() override { + return false; + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index d06ebfc72d0..afb4799d24a 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<<95aa462c1a48b2299c2a82adf74c58df>> + * @generated SignedSource<> */ /** @@ -342,6 +342,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::lazyAnimationCallbacks(); } + bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() override { + auto value = values_["throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(); + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { auto value = values_["traceTurboModulePromiseRejectionsOnAndroid"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index cd78fcef43c..3a88a248a7c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<716d5779133c4ee8c9d20e4a71065d59>> */ /** @@ -58,6 +58,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool fuseboxEnabledRelease() = 0; virtual bool fuseboxNetworkInspectionEnabled() = 0; virtual bool lazyAnimationCallbacks() = 0; + virtual bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS() = 0; virtual bool traceTurboModulePromiseRejectionsOnAndroid() = 0; virtual bool useAlwaysAvailableJSErrorHandling() = 0; virtual bool useEditTextStockAndroidFocusBehavior() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec index e9c20fba642..bdda78a7e2e 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec @@ -34,7 +34,7 @@ Pod::Spec.new do |s| s.platforms = min_supported_versions s.source = source s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags - s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/RCT-Folly\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fast_float/include\" \"$(PODS_ROOT)/fmt/include\" \"$(PODS_ROOT)/Headers/Private/React-Core\"", + s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/RCT-Folly\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fast_float/include\" \"$(PODS_ROOT)/fmt/include\" \"$(PODS_ROOT)/Headers/Private/React-Core\" \"$(PODS_CONFIGURATION_BUILD_DIR)/React-debug/React_featureflags.framework/Headers\"", "USE_HEADERMAP" => "YES", "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(), "GCC_WARN_PEDANTIC" => "YES" } @@ -51,6 +51,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" s.dependency "React-cxxreact" s.dependency "React-jsi" + s.dependency "React-featureflags" s.dependency "React-runtimeexecutor" add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern') diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm index 4e873126642..d97d5cd3714 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm @@ -29,9 +29,11 @@ #import #import #import +#import #import #import #import +#import using namespace facebook; using namespace facebook::react; @@ -574,7 +576,35 @@ typedef struct { }; if ([self _requiresMainQueueSetup:moduleClass]) { - RCTUnsafeExecuteOnMainQueueSync(work); + if (ReactNativeFeatureFlags::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS()) { + static int32_t modulesSettingUpOnMainQueueCount = 0; + bool needsUnlock = NO; + + // This function can be recursive. If it's called recursively, we need to skip the lock. + // We can't use a recursive mutex instead because the lock is needed on multiple threads. + if (modulesSettingUpOnMainQueueCount == 0) { + if (!facebook::react::getMainThreadMutex()->try_lock()) { + NSString *reason = [NSString + stringWithFormat: + @"TurboModule %@ which requires main queue setup is initializing during sync rendering. This would have caused a deadlock. Please fix this by avoiding main queue setup or eager initializing this TurboModule.", + NSStringFromClass(moduleClass)]; + NSException *exception = [NSException exceptionWithName:@"UnsafeTurboModuleException" + reason:reason + userInfo:nil]; + @throw exception; + } + needsUnlock = true; + } + modulesSettingUpOnMainQueueCount++; + RCTUnsafeExecuteOnMainQueueSync(work); + modulesSettingUpOnMainQueueCount--; + if (needsUnlock) { + facebook::react::getMainThreadMutex()->unlock(); + } + } else { + RCTUnsafeExecuteOnMainQueueSync(work); + } + } else { work(); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index aef893b2de6..fd57f3c01b8 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<<9a4f0e72b7e6518347f65253c018220a>> + * @generated SignedSource<> */ /** @@ -209,6 +209,11 @@ bool NativeReactNativeFeatureFlags::lazyAnimationCallbacks( return ReactNativeFeatureFlags::lazyAnimationCallbacks(); } +bool NativeReactNativeFeatureFlags::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(); +} + bool NativeReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 958e00099df..055e6cfa93e 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<<16deece14930c9b259f6f7594127fe55>> + * @generated SignedSource<<805e70470028c1a98ef49e6aaf18fd03>> */ /** @@ -103,6 +103,8 @@ class NativeReactNativeFeatureFlags bool lazyAnimationCallbacks(jsi::Runtime& runtime); + bool throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS(jsi::Runtime& runtime); + bool traceTurboModulePromiseRejectionsOnAndroid(jsi::Runtime& runtime); bool useAlwaysAvailableJSErrorHandling(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.cpp b/packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.cpp new file mode 100644 index 00000000000..3ebc8e1760d --- /dev/null +++ b/packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.cpp @@ -0,0 +1,17 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RuntimeExecutor.h" + +namespace facebook::react { + +std::mutex* getMainThreadMutex() { + static std::mutex mainThreadMutex; + return &mainThreadMutex; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.h b/packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.h index fe151954d52..050b7940ce2 100644 --- a/packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.h +++ b/packages/react-native/ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.h @@ -14,6 +14,8 @@ namespace facebook::react { +std::mutex* getMainThreadMutex(); + /* * Takes a function and calls it with a reference to a Runtime. The function * will be called when it is safe to do so (i.e. it ensures non-concurrent @@ -31,6 +33,16 @@ using RuntimeExecutor = * Use this method when the caller needs to *be blocked* by executing the * `callback` and requires that the callback will be executed on the same * thread. + * Example order of events (when not a sync call in runtimeExecutor callback): + * - [UI thread] Lock all mutexes at start + * - [UI thread] mutex1.lock before callback + * - [JS thread] Set runtimePtr in runtimeExecutor callback + * - [JS thread] mutex1.unlock in runtimeExecutor callback + * - [UI thread] Call callback + * - [JS thread] mutex2.lock in runtimeExecutor callback + * - [UI thread] mutex2.unlock after callback + * - [UI thread] mutex3.lock after callback + * - [JS thread] mutex3.unlock in runtimeExecutor callback */ inline static void executeSynchronouslyOnSameThread_CAN_DEADLOCK( const RuntimeExecutor& runtimeExecutor, @@ -42,6 +54,7 @@ inline static void executeSynchronouslyOnSameThread_CAN_DEADLOCK( std::mutex mutex2; std::mutex mutex3; + getMainThreadMutex()->lock(); mutex1.lock(); mutex2.lock(); mutex3.lock(); @@ -70,6 +83,7 @@ inline static void executeSynchronouslyOnSameThread_CAN_DEADLOCK( callback(*runtimePtr); mutex2.unlock(); mutex3.lock(); + getMainThreadMutex()->unlock(); } template diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 98a1c661e01..2a6e552f9cf 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -398,6 +398,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS: { + defaultValue: false, + metadata: { + dateAdded: '2025-02-18', + description: + 'Throw an exception instead of deadlocking when a TurboModule that requires main queue setup is initialized during a synchronous render on iOS.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, traceTurboModulePromiseRejectionsOnAndroid: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 7810a833816..9378d65c35e 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<<4bdedb6bbe2199cb99e72e86bfffe372>> + * @generated SignedSource<> * @flow strict */ @@ -82,6 +82,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ fuseboxEnabledRelease: Getter, fuseboxNetworkInspectionEnabled: Getter, lazyAnimationCallbacks: Getter, + throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS: Getter, traceTurboModulePromiseRejectionsOnAndroid: Getter, useAlwaysAvailableJSErrorHandling: Getter, useEditTextStockAndroidFocusBehavior: Getter, @@ -304,6 +305,10 @@ export const fuseboxNetworkInspectionEnabled: Getter = createNativeFlag * Only enqueue Choreographer calls if there is an ongoing animation, instead of enqueueing every frame. */ export const lazyAnimationCallbacks: Getter = createNativeFlagGetter('lazyAnimationCallbacks', false); +/** + * Throw an exception instead of deadlocking when a TurboModule that requires main queue setup is initialized during a synchronous render on iOS. + */ +export const throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS: Getter = createNativeFlagGetter('throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS', false); /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 82bbaeec6c9..b9aa6ade897 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<<06a03507366a38dfc43868e94d75fcf1>> + * @generated SignedSource<> * @flow strict */ @@ -57,6 +57,7 @@ export interface Spec extends TurboModule { +fuseboxEnabledRelease?: () => boolean; +fuseboxNetworkInspectionEnabled?: () => boolean; +lazyAnimationCallbacks?: () => boolean; + +throwExceptionInsteadOfDeadlockOnTurboModuleSetupDuringSyncRenderIOS?: () => boolean; +traceTurboModulePromiseRejectionsOnAndroid?: () => boolean; +useAlwaysAvailableJSErrorHandling?: () => boolean; +useEditTextStockAndroidFocusBehavior?: () => boolean;