diff --git a/packages/react-native/React/Base/RCTUtils.mm b/packages/react-native/React/Base/RCTUtils.mm index 16a08ac2bcb6be..dffe4dc800ee97 100644 --- a/packages/react-native/React/Base/RCTUtils.mm +++ b/packages/react-native/React/Base/RCTUtils.mm @@ -19,9 +19,12 @@ #import #import +#import #import "RCTAssert.h" #import "RCTLog.h" +using namespace facebook::react; + NSString *const RCTErrorUnspecified = @"EUNSPECIFIED"; // Returns the Path of Home directory @@ -314,7 +317,12 @@ void RCTUnsafeExecuteOnMainQueueSyncWithError(dispatch_block_t block, NSString * return; } - if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) { + if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) { + unsafeExecuteOnMainThreadSync(block); + return; + } + + if (ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) { RCTLogError(@"RCTUnsafeExecuteOnMainQueueSync: %@", context); } @@ -341,7 +349,12 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp return; } - if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) { + if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) { + unsafeExecuteOnMainThreadSync(block); + return; + } + + if (ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) { RCTLogError(@"RCTUnsafeExecuteOnMainQueueOnceSync: Sync dispatches to the main queue can deadlock React Native."); } diff --git a/packages/react-native/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec b/packages/react-native/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec index 2cf65a5db3a11a..8e2904d20bd0f3 100644 --- a/packages/react-native/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec +++ b/packages/react-native/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec @@ -44,4 +44,7 @@ Pod::Spec.new do |s| "DEFINES_MODULE" => "YES" } s.dependency "React-jsi", version + s.dependency "React-featureflags", version + add_dependency(s, "React-debug") + add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"]) end diff --git a/packages/react-native/ReactCommon/runtimeexecutor/platform/cxx/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h b/packages/react-native/ReactCommon/runtimeexecutor/platform/cxx/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h index fff9664f59fa7b..0baee4e2841a6e 100644 --- a/packages/react-native/ReactCommon/runtimeexecutor/platform/cxx/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h +++ b/packages/react-native/ReactCommon/runtimeexecutor/platform/cxx/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h @@ -24,7 +24,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK( template inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK( const RuntimeExecutor& runtimeExecutor, - std::function&& runtimeWork) { + std::function&& runtimeWork) { DataT data; executeSynchronouslyOnSameThread_CAN_DEADLOCK( diff --git a/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h b/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h index fff9664f59fa7b..12ab4d958e7a95 100644 --- a/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h +++ b/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h @@ -24,7 +24,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK( template inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK( const RuntimeExecutor& runtimeExecutor, - std::function&& runtimeWork) { + std::function&& runtimeWork) { DataT data; executeSynchronouslyOnSameThread_CAN_DEADLOCK( @@ -33,4 +33,7 @@ inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK( return data; } + +void unsafeExecuteOnMainThreadSync(std::function work); + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.mm b/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.mm index c2484f255952ae..34b335d7f92875 100644 --- a/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.mm +++ b/packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.mm @@ -5,11 +5,160 @@ * LICENSE file in the root directory of this source tree. */ +#import + #import -#include -#include +#import +#import +#import +#import +#import +#import +#import +#import +#import namespace facebook::react { + +namespace { +class UITask { + std::promise _isDone; + std::function _uiWork; + + public: + UITask(UITask &&other) = default; + UITask &operator=(UITask &&other) = default; + UITask(const UITask &) = delete; + UITask &operator=(const UITask &) = delete; + ~UITask() = default; + + UITask(std::function &&uiWork) : _uiWork(std::move(uiWork)) {} + + void operator()() + { + if (!_uiWork) { + return; + } + OnScopeExit onScopeExit(^{ + _uiWork = nullptr; + _isDone.set_value(); + }); + _uiWork(); + } + + std::future future() + { + return _isDone.get_future(); + } +}; + +// Protects access to g_uiTask +std::mutex &g_mutex() +{ + static std::mutex mutex; + return mutex; +} + +std::condition_variable &g_cv() +{ + static std::condition_variable cv; + return cv; +} + +std::mutex &g_ticket() +{ + static std::mutex ticket; + return ticket; +} + +std::optional &g_uiTask() +{ + static std::optional uiTaskQueue; + return uiTaskQueue; +} + +// Must be called holding g_mutex(); +bool hasUITask() +{ + return g_uiTask().has_value(); +} + +// Must be called holding g_mutex(); +UITask takeUITask() +{ + react_native_assert(hasUITask()); + auto uiTask = std::move(*g_uiTask()); + g_uiTask() = std::nullopt; + return uiTask; +} + +// Must be called holding g_mutex(); +UITask &postUITask(std::function &&uiWork) +{ + react_native_assert(!hasUITask()); + g_uiTask() = UITask(std::move(uiWork)); + g_cv().notify_one(); + return *g_uiTask(); +} + +bool g_isRunningUITask = false; +void runUITask(UITask &uiTask) +{ + react_native_assert([[NSThread currentThread] isMainThread]); + g_isRunningUITask = true; + OnScopeExit onScopeExit([]() { g_isRunningUITask = false; }); + uiTask(); +} + +/** + * This method is resilient to multiple javascript threads. + * This can happen when multiple react instances interleave. + * + * The extension from 1 js thread to n: All js threads race to + * get a ticket to post a ui task. The first one to get the ticket + * will post the ui task, and go to sleep. The cooridnator or + * main queue will execute that ui task, waking up the js thread + * and releasing that ticket. Another js thread will get the ticket. + * + * For simplicity, we will just use this algorithm for all bg threads. + * Not just the js thread. + */ +void saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK( + const RuntimeExecutor &runtimeExecutor, + std::function &&runtimeWork) +{ + react_native_assert([[NSThread currentThread] isMainThread] && !g_isRunningUITask); + + jsi::Runtime *runtime = nullptr; + std::promise runtimeWorkDone; + + runtimeExecutor([&runtime, runtimeWorkDoneFuture = runtimeWorkDone.get_future().share()](jsi::Runtime &rt) { + { + std::lock_guard lock(g_mutex()); + runtime = &rt; + g_cv().notify_one(); + } + + runtimeWorkDoneFuture.wait(); + }); + + while (true) { + std::unique_lock lock(g_mutex()); + g_cv().wait(lock, [&] { return runtime != nullptr || hasUITask(); }); + if (runtime != nullptr) { + break; + } + + auto uiTask = takeUITask(); + lock.unlock(); + runUITask(uiTask); + } + + OnScopeExit onScopeExit([&]() { runtimeWorkDone.set_value(); }); + // Calls into runtime scheduler, which takes care of error handling + runtimeWork(*runtime); +} + /* * Schedules `runtimeWork` to be executed on the same thread using the * `RuntimeExecutor`, and blocks on its completion. @@ -26,7 +175,7 @@ * - [JS thread] Signal runtime capture block is finished: * resolve(runtimeCaptureBlockDone); */ -void executeSynchronouslyOnSameThread_CAN_DEADLOCK( +void legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK( const RuntimeExecutor &runtimeExecutor, std::function &&runtimeWork) { @@ -58,4 +207,54 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK( runtimeCaptureBlockDone.get_future().wait(); } +} // namespace + +void executeSynchronouslyOnSameThread_CAN_DEADLOCK( + const RuntimeExecutor &runtimeExecutor, + std::function &&runtimeWork) +{ + if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) { + saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork)); + } else { + legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork)); + } +} + +/** + * This method is resilient to multiple javascript threads. + * This can happen when multiple react instances interleave. + * + * The extension from 1 js thread to n: All js threads race to + * get a ticket to post a ui task. The first one to get the ticket + * will post the ui task, and go to sleep. The cooridnator or + * main queue will execute that ui task, waking up the js thread + * and releasing that ticket. Another js thread will get the ticket. + * + * For simplicity, we will just use this method for all bg threads. + * Not just the js thread. + */ +void unsafeExecuteOnMainThreadSync(std::function work) +{ + std::lock_guard ticket(g_ticket()); + + std::future isDone; + { + std::lock_guard lock(g_mutex()); + isDone = postUITask(std::move(work)).future(); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + std::unique_lock lock(g_mutex()); + if (!hasUITask()) { + return; + } + + auto uiTask = takeUITask(); + lock.unlock(); + runUITask(uiTask); + }); + + isDone.wait(); +} + } // namespace facebook::react