Skip to content

Commit 73e373a

Browse files
RSNarafacebook-github-bot
authored andcommitted
Introduce main queue coordinator (facebook#51425)
Summary: Pull Request resolved: facebook#51425 # Problem React native's new architecture will allow components to do sync render/events. That means they'll makes synchronous dispatches from main thread to the js thread, to capture the runtime so that they can execute js on the main thread. But, the js thread already as a bunch of synchronous calls to the main thread. So, if any of those js -> ui sync calls happen concurrently with a synchronous render, the application will deadlock. This diff is an attempt to mitigate all those deadlocks. ## Context How js execution from the main thread works: * Main thread puts a block on the js thread, to capture the js runtime. Main thread is put to sleep. * Js thread executes "runtime capture block". The runtime is captured for the main thread. The js thread is put to sleep. * Main thread wakes up, noticing that the runtime is captured. It executes its js code with the captured runtime. Then, it releases the runtime, which wakes up the js thread. Both the main and js thread move on to other tasks. How synchronous js -> main thread calls work: * Js thread puts a ui block on the main queue. * Js thread goes to sleep until that ui block executes on the main thread. ## Deadlock facebook#1 **Main thread**: execute js now: * Main thread puts a block on the js queue, to capture the runtime. * Main thread then then goes to sleep, waiting for runtime to be captured **JS thread**: execute ui code synchronously: * Js thread schedules a block on the ui thread * Js thread then goes to sleep, waiting for that block to execute. **Result:** The application deadlocks | {F1978009555} | {F1978009612} | ![image](https://github.com/user-attachments/assets/325a62f4-d5b7-492d-a114-efb738556239) ## Deadlock facebook#2 **JS thread**: execute ui code synchronously: * Js thread schedules a block on the ui thread * Js thread then goes to sleep waiting for that block to execute. **Main thread**: execute js now: * Main thread puts a block on the js queue, to capture the runtime. * Main thread then then goes to sleep, waiting for runtime to be captured **Result:** The application deadlocks | {F1978009690} | {F1978009701} | ![image](https://github.com/user-attachments/assets/13a6ea17-a55d-453d-9291-d1c8007ecffa) # Changes This diff attempts to fix those deadlocks. How: * In "execute ui code synchronously" (js thread): * Before going to sleep, the js thread schedules the ui work on the main queue, **and** it posts the ui work to "execute js now". * In "execute js now" (main thread): * This diff makes "execute js now" stateful: it keeps a "pending ui block." * Before capturing the runtime, the "execute js now" executes "pending ui work", if it exists. * While sleeping waiting for runtime capture, "execute js now" can wake up, and execute "pending ui work." It goes back to sleep afterwards, waiting for runtime capture. ## Mitigation: Deadlock facebook#1 **Main thread**: execute js now: * Main thread puts a block on the js queue, to capture the runtime. * Main thread then then goes to sleep, waiting for runtime capture **JS Thread**: execute ui code synchronously: * Js thread puts its ui block on the ui queue. * ***New***: Js thread also posts that ui block to "execute js now". Main thread was sleeping waiting for runtime to be captured. It now wakes up. * Js thread goes to sleep. The main thread wakes up in "execute js now": * Main thread sees that a "pending ui block" is posted. It executes the "pending ui block." The block, also scheduled on the main thread, noops henceforth. * Main thread goes back to sleep, waiting for runtime capture. * The js thread wakes up, moves on to the next task. **Result:** The runtime is captured by the main thread. | {F1978010383} | {F1978010363} | {F1978010371} | {F1978010379} | ![image](https://github.com/user-attachments/assets/f53cb10c-7801-46be-934a-96af7d5f5fab) ## Mitigation: Deadlock facebook#2 **JS Thread**: execute ui code synchronously: * Js thread puts its ui block on the ui queue. * ***New***: Js thread also posts that ui block to "execute js now". Main thread was sleeping waiting for runtime to be captured. It now wakes up. * Js thread goes to sleep. **Main thread**: execute js now * Main thread sees that a "pending ui block" is posted. It executes the "pending ui block" immediately. The block, also scheduled on the main thread, noops henceforth. * Js thread wakes up and moves onto the next task. **Result:** Main thread captures the runtime. | {F1978010525} | {F1978010533} | {F1978010542} | ![image](https://github.com/user-attachments/assets/9e0ca5ef-fab6-4a26-bcca-d79d36624d5d) Differential Revision: D74769326
1 parent 0114c8a commit 73e373a

File tree

5 files changed

+212
-5
lines changed

5 files changed

+212
-5
lines changed

packages/react-native/React/Base/RCTUtils.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#import <CommonCrypto/CommonCrypto.h>
2020

2121
#import <React/RCTUtilsUIOverride.h>
22+
#import <ReactCommon/RuntimeExecutorSyncUIThreadUtils.h>
2223
#import "RCTAssert.h"
2324
#import "RCTLog.h"
2425

@@ -314,6 +315,11 @@ void RCTUnsafeExecuteOnMainQueueSyncWithError(dispatch_block_t block, NSString *
314315
return;
315316
}
316317

318+
if (facebook::react::ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
319+
facebook::react::unsafeExecuteOnMainThreadSync(block);
320+
return;
321+
}
322+
317323
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
318324
RCTLogError(@"RCTUnsafeExecuteOnMainQueueSync: %@", context);
319325
}
@@ -341,6 +347,11 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp
341347
return;
342348
}
343349

350+
if (facebook::react::ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
351+
facebook::react::unsafeExecuteOnMainThreadSync(block);
352+
return;
353+
}
354+
344355
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
345356
RCTLogError(@"RCTUnsafeExecuteOnMainQueueOnceSync: Sync dispatches to the main queue can deadlock React Native.");
346357
}

packages/react-native/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,7 @@ Pod::Spec.new do |s|
4444
"DEFINES_MODULE" => "YES" }
4545

4646
s.dependency "React-jsi", version
47+
s.dependency "React-featureflags", version
48+
add_dependency(s, "React-debug")
49+
add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"])
4750
end

packages/react-native/ReactCommon/runtimeexecutor/platform/cxx/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2626
template <typename DataT>
2727
inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2828
const RuntimeExecutor& runtimeExecutor,
29-
std::function<DataT(jsi::Runtime& runtime)>&& runtimeWork) {
29+
std::function<DataT(jsi::Runtime&)>&& runtimeWork) {
3030
DataT data;
3131

3232
executeSynchronouslyOnSameThread_CAN_DEADLOCK(

packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2626
template <typename DataT>
2727
inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2828
const RuntimeExecutor& runtimeExecutor,
29-
std::function<DataT(jsi::Runtime& runtime)>&& runtimeWork) {
29+
std::function<DataT(jsi::Runtime&)>&& runtimeWork) {
3030
DataT data;
3131

3232
executeSynchronouslyOnSameThread_CAN_DEADLOCK(
@@ -35,4 +35,7 @@ inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
3535

3636
return data;
3737
}
38+
39+
void unsafeExecuteOnMainThreadSync(std::function<void()> runnable);
40+
3841
} // namespace facebook::react

packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.mm

Lines changed: 193 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,154 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
#import <Foundation/Foundation.h>
9+
810
#import <ReactCommon/RuntimeExecutorSyncUIThreadUtils.h>
9-
#include <future>
10-
#include <thread>
11+
#import <react/debug/react_native_assert.h>
12+
#import <react/featureflags/ReactNativeFeatureFlags.h>
13+
#import <react/utils/OnScopeExit.h>
14+
#import <algorithm>
15+
#import <functional>
16+
#import <future>
17+
#import <mutex>
18+
#import <optional>
19+
#import <thread>
1120

1221
namespace facebook::react {
22+
23+
namespace {
24+
class UITask {
25+
std::promise<void> _isDone;
26+
std::function<void()> _uiWork;
27+
28+
public:
29+
UITask(UITask &&other) = default;
30+
UITask &operator=(UITask &&other) = default;
31+
UITask(const UITask &) = delete;
32+
UITask &operator=(const UITask &) = delete;
33+
34+
UITask() = delete;
35+
UITask(std::function<void()> uiWork) : _uiWork(std::move(uiWork)) {}
36+
37+
void operator()()
38+
{
39+
if (!_uiWork) {
40+
return;
41+
}
42+
OnScopeExit onScopeExit(^{
43+
_uiWork = nullptr;
44+
_isDone.set_value();
45+
});
46+
_uiWork();
47+
}
48+
49+
std::future<void> future()
50+
{
51+
return _isDone.get_future();
52+
}
53+
};
54+
55+
std::mutex &g_mutex()
56+
{
57+
static std::mutex mutex;
58+
return mutex;
59+
}
60+
61+
std::condition_variable &g_cv()
62+
{
63+
static std::condition_variable cv;
64+
return cv;
65+
}
66+
67+
std::mutex &g_ticket()
68+
{
69+
static std::mutex ticket;
70+
return ticket;
71+
}
72+
73+
std::optional<UITask> &g_uiTask()
74+
{
75+
static std::optional<UITask> uiTaskQueue;
76+
return uiTaskQueue;
77+
}
78+
79+
// Must be called holding g_mutex();
80+
bool hasUITask()
81+
{
82+
return g_uiTask().has_value();
83+
}
84+
85+
// Must be called holding g_mutex();
86+
UITask takeUITask()
87+
{
88+
react_native_assert(hasUITask());
89+
auto uiTask = std::move(*g_uiTask());
90+
g_uiTask() = std::nullopt;
91+
return uiTask;
92+
}
93+
94+
// Must be called holding g_mutex();
95+
UITask &postUITask(std::function<void()> uiWork)
96+
{
97+
react_native_assert(!hasUITask());
98+
g_uiTask() = UITask(uiWork);
99+
g_cv().notify_one();
100+
return *g_uiTask();
101+
}
102+
103+
static bool g_isRunningUITask = false;
104+
void runUITask(UITask &uiTask)
105+
{
106+
g_isRunningUITask = true;
107+
OnScopeExit onScopeExit([]() { g_isRunningUITask = false; });
108+
uiTask();
109+
}
110+
111+
/**
112+
* This method is resilient to multiple javascript threads.
113+
* This can happen when multiple react instances interleave.
114+
*
115+
* The extension from 1 js thread to n: All js threads race to
116+
* get a ticket to post a ui task. The first one to get the ticket
117+
* will post the ui task, and go to sleep. The cooridnator or
118+
* main queue will execute that ui task, waking up the js thread
119+
* and releasing that ticket. Another js thread will get the ticket.
120+
*/
121+
void saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
122+
const RuntimeExecutor &runtimeExecutor,
123+
std::function<void(jsi::Runtime &runtime)> &&runtimeWork)
124+
{
125+
react_native_assert([[NSThread currentThread] isMainThread] && !g_isRunningUITask);
126+
127+
jsi::Runtime *runtime = nullptr;
128+
std::promise<void> runtimeWorkDone;
129+
130+
runtimeExecutor([&](jsi::Runtime &rt) {
131+
{
132+
std::lock_guard<std::mutex> lock(g_mutex());
133+
runtime = &rt;
134+
g_cv().notify_one();
135+
}
136+
137+
runtimeWorkDone.get_future().wait();
138+
});
139+
140+
while (true) {
141+
std::unique_lock<std::mutex> lock(g_mutex());
142+
g_cv().wait(lock, [&] { return runtime != nullptr || hasUITask(); });
143+
if (runtime != nullptr) {
144+
break;
145+
}
146+
147+
auto uiTask = takeUITask();
148+
lock.unlock();
149+
runUITask(uiTask);
150+
}
151+
152+
OnScopeExit onScopeExit([&]() { runtimeWorkDone.set_value(); });
153+
runtimeWork(*runtime);
154+
}
155+
13156
/*
14157
* Executes some `runtimeWork` on the same thread in a *synchronous* manner
15158
* using the `RuntimeExecutor`.
@@ -26,7 +169,7 @@
26169
* - [JS thread] Signal runtime capture block is finished:
27170
* resolve(runtimeCaptureBlockDone);
28171
*/
29-
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
172+
void legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
30173
const RuntimeExecutor &runtimeExecutor,
31174
std::function<void(jsi::Runtime &)> &&runtimeWork)
32175
{
@@ -55,4 +198,51 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
55198
runtimeCaptureBlockDone.get_future().wait();
56199
}
57200

201+
} // namespace
202+
203+
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
204+
const RuntimeExecutor &runtimeExecutor,
205+
std::function<void(jsi::Runtime &)> &&runtimeWork)
206+
{
207+
if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
208+
saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork));
209+
} else {
210+
legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork));
211+
}
212+
}
213+
214+
/**
215+
* This method is resilient to multiple javascript threads.
216+
* This can happen when multiple react instances interleave.
217+
*
218+
* The extension from 1 js thread to n: All js threads race to
219+
* get a ticket to post a ui task. The first one to get the ticket
220+
* will post the ui task, and go to sleep. The cooridnator or
221+
* main queue will execute that ui task, waking up the js thread
222+
* and releasing that ticket. Another js thread will get the ticket.
223+
*/
224+
void unsafeExecuteOnMainThreadSync(std::function<void()> work)
225+
{
226+
std::lock_guard<std::mutex> ticket(g_ticket());
227+
228+
std::future<void> isDone;
229+
{
230+
std::lock_guard<std::mutex> lock(g_mutex());
231+
isDone = postUITask(std::move(work)).future();
232+
}
233+
234+
dispatch_async(dispatch_get_main_queue(), ^{
235+
std::unique_lock<std::mutex> lock(g_mutex());
236+
if (!hasUITask()) {
237+
return;
238+
}
239+
240+
auto uiTask = takeUITask();
241+
lock.unlock();
242+
runUITask(uiTask);
243+
});
244+
245+
isDone.wait();
246+
}
247+
58248
} // namespace facebook::react

0 commit comments

Comments
 (0)