Skip to content

Commit 167f7f2

Browse files
RSNarafacebook-github-bot
authored andcommitted
RuntimeExecutor: Fix sync js execution from main thread
Summary: # Problem Sync rendering/events (i.e: execute js now below), when used in conjunction with js -> ui sync calls, can deadlock react native: ## Deadlock #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. * **The application deadlocks** | {F1978009555} | {F1978009612} | ## Deadlock #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 * **The application deadlocks.** | {F1978009690} | {F1978009701} | # Changes This diff attempts to fix those deadlocks. How: * This diff introduces a stateful "execute js now" coordinator. * In "execute ui code synchronously" (js thread): * Before going to sleep, the js thread posts its ui work to the "execute js now" coordinator. * In "execute js now" (main thread): * Before trying to capture the runtime, the main thread executes "pending ui work", if it exists. * While the main thread is sleeping, waiting for runtime capture, it can be woken up, and asked to execute "pending ui work." ## Mitigation: Deadlock #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 this block on the ui thread. * ***New***: Js thread also assigns this block to the coordinator. *And wakes up the main thread*. * Js thread goes to sleep. The main thread wakes up: * Main thread **executes** the ui block assigned to the coordinator. **This cancels the ui block scheduled on the main queue.** * Main thread goes back to sleep. * The js thread wakes up, moves on to the next task. The runtime is captured by the main thread. | {F1978010383} | {F1978010363} | {F1978010371} | {F1978010379} ## Mitigation: Deadlock #2 **JS Thread**: execute ui code synchronously: * Js thread schedules this block on the ui thread. * ***New***: Js thread also assigns this block to the coordinator. *And wakes up the main thread*. * Js thread goes to sleep. **Main thread**: execute js now * Main thread executes the ui block immediately. (This cancels the ui block on the main queue). * Js thread wakes up and moves onto the next task. Main thread captures the runtime. | {F1978010525} | {F1978010533} | {F1978010542} | Differential Revision: D74769326
1 parent 8041df8 commit 167f7f2

File tree

7 files changed

+223
-143
lines changed

7 files changed

+223
-143
lines changed

packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
- (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy
7272
bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator
7373
delegate:(id<RCTTurboModuleManagerDelegate>)delegate
74-
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker;
74+
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
75+
isJSThread:(std::function<bool()>)isJSThread;
7576

7677
- (void)installJSBindings:(facebook::jsi::Runtime &)runtime;
7778

packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm

+37-8
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
#import <React/RCTUtils.h>
3030
#import <ReactCommon/CxxTurboModuleUtils.h>
3131
#import <ReactCommon/RCTTurboModuleWithJSIBindings.h>
32+
#import <ReactCommon/RuntimeExecutorUtils.h>
3233
#import <ReactCommon/TurboCxxModule.h>
3334
#import <ReactCommon/TurboModulePerfLogger.h>
3435
#import <ReactCommon/TurboModuleUtils.h>
3536
#import <react/featureflags/ReactNativeFeatureFlags.h>
37+
#import <react/utils/OnScopeExit.h>
3638

3739
using namespace facebook;
3840
using namespace facebook::react;
@@ -110,6 +112,27 @@ bool isCreatingModule() const
110112
}
111113
};
112114

115+
void RCTExecuteOnMainQueueSyncFromJS(dispatch_block_t work)
116+
{
117+
__block auto uiWorkDone = std::make_shared<std::mutex>();
118+
__block auto uiWork = work;
119+
auto wrapper = ^{
120+
if (!uiWork) {
121+
return;
122+
}
123+
OnScopeExit onScopeExit(^{
124+
uiWork = nil;
125+
uiWorkDone->lock();
126+
});
127+
uiWork();
128+
};
129+
130+
uiWorkDone->lock();
131+
RCTExecuteOnMainQueue(wrapper);
132+
postIdleWork(wrapper);
133+
uiWorkDone->lock();
134+
}
135+
113136
class ModuleNativeMethodCallInvoker : public NativeMethodCallInvoker {
114137
private:
115138
dispatch_queue_t methodQueue_;
@@ -148,7 +171,7 @@ void invokeSync(const std::string &methodName, std::function<void()> &&work) ove
148171
{
149172
if (requiresMainQueueSetup_ && methodName == "getConstants") {
150173
__block auto retainedWork = std::move(work);
151-
RCTUnsafeExecuteOnMainQueueSync(^{
174+
RCTExecuteOnMainQueueSyncFromJS(^{
152175
retainedWork();
153176
});
154177
return;
@@ -215,6 +238,7 @@ @implementation RCTTurboModuleManager {
215238

216239
RCTBridgeProxy *_bridgeProxy;
217240
RCTBridgeModuleDecorator *_bridgeModuleDecorator;
241+
std::function<bool()> _isJSThread;
218242

219243
dispatch_queue_t _sharedModuleQueue;
220244
}
@@ -224,9 +248,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
224248
bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator
225249
delegate:(id<RCTTurboModuleManagerDelegate>)delegate
226250
jsInvoker:(std::shared_ptr<CallInvoker>)jsInvoker
251+
isJSThread:(std::function<bool()>)isJSThread
227252
{
228253
if (self = [super init]) {
229254
_jsInvoker = std::move(jsInvoker);
255+
_isJSThread = isJSThread;
230256
_delegate = delegate;
231257
_bridge = bridge;
232258
_bridgeProxy = bridgeProxy;
@@ -276,19 +302,22 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
276302
bridgeProxy:nil
277303
bridgeModuleDecorator:[bridge bridgeModuleDecorator]
278304
delegate:delegate
279-
jsInvoker:jsInvoker];
305+
jsInvoker:jsInvoker
306+
isJSThread:nullptr];
280307
}
281308

282309
- (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy
283310
bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator
284311
delegate:(id<RCTTurboModuleManagerDelegate>)delegate
285312
jsInvoker:(std::shared_ptr<CallInvoker>)jsInvoker
313+
isJSThread:(std::function<bool()>)isJSThread
286314
{
287315
return [self initWithBridge:nil
288316
bridgeProxy:bridgeProxy
289317
bridgeModuleDecorator:bridgeModuleDecorator
290318
delegate:delegate
291-
jsInvoker:jsInvoker];
319+
jsInvoker:jsInvoker
320+
isJSThread:isJSThread];
292321
}
293322

294323
/**
@@ -601,11 +630,11 @@ - (ModuleHolder *)_getOrCreateModuleHolder:(const char *)moduleName
601630
};
602631

603632
if ([self _requiresMainQueueSetup:moduleClass]) {
604-
NSString *message = [NSString
605-
stringWithFormat:
606-
@"Lazily setting up TurboModule \"%s\" on the main queue. This could deadlock react native, if it happens during sync rendering. Please fix this by avoiding lazy main queue setup.",
607-
moduleName];
608-
RCTUnsafeExecuteOnMainQueueSyncWithError(work, message);
633+
if (_isJSThread && _isJSThread()) {
634+
RCTExecuteOnMainQueueSyncFromJS(work);
635+
} else {
636+
RCTUnsafeExecuteOnMainQueueSync(work);
637+
}
609638
} else {
610639
work();
611640
}

packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm

+98-92
Original file line numberDiff line numberDiff line change
@@ -325,118 +325,124 @@ - (void)_start
325325
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridgeProxy:bridgeProxy
326326
bridgeModuleDecorator:_bridgeModuleDecorator
327327
delegate:self
328-
jsInvoker:jsCallInvoker];
328+
jsInvoker:jsCallInvoker
329+
isJsThread:[threadId = [_jsThreadManager threadId]]() {
330+
return std::this_thread::get_id() == threadId;
331+
}]
332+
};
329333

330334
#if RCT_DEV
331-
/**
332-
* Instantiating DevMenu has the side-effect of registering
333-
* shortcuts for CMD + d, CMD + i, and CMD + n via RCTDevMenu.
334-
* Therefore, when TurboModules are enabled, we must manually create this
335-
* NativeModule.
336-
*/
337-
[_turboModuleManager moduleForName:"RCTDevMenu"];
335+
/**
336+
* Instantiating DevMenu has the side-effect of registering
337+
* shortcuts for CMD + d, CMD + i, and CMD + n via RCTDevMenu.
338+
* Therefore, when TurboModules are enabled, we must manually create this
339+
* NativeModule.
340+
*/
341+
[_turboModuleManager moduleForName:"RCTDevMenu"];
338342
#endif // end RCT_DEV
339343

340-
// Initialize RCTModuleRegistry so that TurboModules can require other TurboModules.
341-
[_bridgeModuleDecorator.moduleRegistry setTurboModuleRegistry:_turboModuleManager];
344+
// Initialize RCTModuleRegistry so that TurboModules can require other TurboModules.
345+
[_bridgeModuleDecorator.moduleRegistry setTurboModuleRegistry:_turboModuleManager];
342346

343-
if (ReactNativeFeatureFlags::enableMainQueueModulesOnIOS()) {
344-
/**
345-
* Some native modules need to capture uikit objects on the main thread.
346-
* Start initializing those modules on the main queue here. The JavaScript thread
347-
* will wait until this module init finishes, before executing the js bundle.
348-
*/
349-
NSArray<NSString *> *modulesRequiringMainQueueSetup = [_delegate unstableModulesRequiringMainQueueSetup];
347+
if (ReactNativeFeatureFlags::enableMainQueueModulesOnIOS()) {
348+
/**
349+
* Some native modules need to capture uikit objects on the main thread.
350+
* Start initializing those modules on the main queue here. The JavaScript thread
351+
* will wait until this module init finishes, before executing the js bundle.
352+
*/
353+
NSArray<NSString *> *modulesRequiringMainQueueSetup = [_delegate unstableModulesRequiringMainQueueSetup];
350354

351-
std::shared_ptr<std::mutex> mutex = std::make_shared<std::mutex>();
352-
std::shared_ptr<std::condition_variable> cv = std::make_shared<std::condition_variable>();
353-
std::shared_ptr<bool> isReady = std::make_shared<bool>(false);
355+
std::shared_ptr<std::mutex> mutex = std::make_shared<std::mutex>();
356+
std::shared_ptr<std::condition_variable> cv = std::make_shared<std::condition_variable>();
357+
std::shared_ptr<bool> isReady = std::make_shared<bool>(false);
354358

355-
_waitUntilModuleSetupComplete = ^{
356-
std::unique_lock<std::mutex> lock(*mutex);
357-
cv->wait(lock, [isReady] { return *isReady; });
358-
};
359+
_waitUntilModuleSetupComplete = ^{
360+
std::unique_lock<std::mutex> lock(*mutex);
361+
cv->wait(lock, [isReady] { return *isReady; });
362+
};
359363

360-
// TODO(T218039767): Integrate perf logging into main queue module init
361-
RCTExecuteOnMainQueue(^{
362-
for (NSString *moduleName in modulesRequiringMainQueueSetup) {
363-
[self->_bridgeModuleDecorator.moduleRegistry moduleForName:[moduleName UTF8String]];
364-
}
364+
// TODO(T218039767): Integrate perf logging into main queue module init
365+
RCTExecuteOnMainQueue(^{
366+
for (NSString *moduleName in modulesRequiringMainQueueSetup) {
367+
[self->_bridgeModuleDecorator.moduleRegistry moduleForName:[moduleName UTF8String]];
368+
}
365369

366-
RCTScreenSize();
367-
RCTScreenScale();
370+
RCTScreenSize();
371+
RCTScreenScale();
368372

369-
std::lock_guard<std::mutex> lock(*mutex);
370-
*isReady = true;
371-
cv->notify_all();
372-
});
373-
}
373+
std::lock_guard<std::mutex> lock(*mutex);
374+
*isReady = true;
375+
cv->notify_all();
376+
});
377+
}
374378

375-
RCTLogSetBridgelessModuleRegistry(_bridgeModuleDecorator.moduleRegistry);
376-
RCTLogSetBridgelessCallableJSModules(_bridgeModuleDecorator.callableJSModules);
377-
378-
auto contextContainer = std::make_shared<ContextContainer>();
379-
[_delegate didCreateContextContainer:contextContainer];
380-
381-
contextContainer->insert(
382-
"RCTImageLoader", facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTImageLoader"]));
383-
contextContainer->insert(
384-
"RCTEventDispatcher",
385-
facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTEventDispatcher"]));
386-
contextContainer->insert("RCTBridgeModuleDecorator", facebook::react::wrapManagedObject(_bridgeModuleDecorator));
387-
contextContainer->insert(RuntimeSchedulerKey, std::weak_ptr<RuntimeScheduler>(_reactInstance->getRuntimeScheduler()));
388-
contextContainer->insert("RCTBridgeProxy", facebook::react::wrapManagedObject(bridgeProxy));
389-
390-
_surfacePresenter = [[RCTSurfacePresenter alloc]
391-
initWithContextContainer:contextContainer
392-
runtimeExecutor:bufferedRuntimeExecutor
393-
bridgelessBindingsExecutor:std::optional(_reactInstance->getUnbufferedRuntimeExecutor())];
394-
395-
// This enables RCTViewRegistry in modules to return UIViews from its viewForReactTag method
396-
__weak RCTSurfacePresenter *weakSurfacePresenter = _surfacePresenter;
397-
[_bridgeModuleDecorator.viewRegistry_DEPRECATED setBridgelessComponentViewProvider:^UIView *(NSNumber *reactTag) {
398-
RCTSurfacePresenter *strongSurfacePresenter = weakSurfacePresenter;
399-
if (strongSurfacePresenter == nil) {
400-
return nil;
401-
}
379+
RCTLogSetBridgelessModuleRegistry(_bridgeModuleDecorator.moduleRegistry);
380+
RCTLogSetBridgelessCallableJSModules(_bridgeModuleDecorator.callableJSModules);
381+
382+
auto contextContainer = std::make_shared<ContextContainer>();
383+
[_delegate didCreateContextContainer:contextContainer];
384+
385+
contextContainer->insert(
386+
"RCTImageLoader",
387+
facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTImageLoader"]));
388+
contextContainer->insert(
389+
"RCTEventDispatcher",
390+
facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTEventDispatcher"]));
391+
contextContainer->insert("RCTBridgeModuleDecorator", facebook::react::wrapManagedObject(_bridgeModuleDecorator));
392+
contextContainer->insert(RuntimeSchedulerKey, std::weak_ptr<RuntimeScheduler>(_reactInstance->getRuntimeScheduler()));
393+
contextContainer->insert("RCTBridgeProxy", facebook::react::wrapManagedObject(bridgeProxy));
394+
395+
_surfacePresenter = [[RCTSurfacePresenter alloc]
396+
initWithContextContainer:contextContainer
397+
runtimeExecutor:bufferedRuntimeExecutor
398+
bridgelessBindingsExecutor:std::optional(_reactInstance->getUnbufferedRuntimeExecutor())];
399+
400+
// This enables RCTViewRegistry in modules to return UIViews from its viewForReactTag method
401+
__weak RCTSurfacePresenter *weakSurfacePresenter = _surfacePresenter;
402+
[_bridgeModuleDecorator.viewRegistry_DEPRECATED setBridgelessComponentViewProvider:^UIView *(NSNumber *reactTag) {
403+
RCTSurfacePresenter *strongSurfacePresenter = weakSurfacePresenter;
404+
if (strongSurfacePresenter == nil) {
405+
return nil;
406+
}
402407

403-
return [strongSurfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue];
404-
}];
408+
return [strongSurfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue];
409+
}];
405410

406-
// DisplayLink is used to call timer callbacks.
407-
_displayLink = [RCTDisplayLink new];
411+
// DisplayLink is used to call timer callbacks.
412+
_displayLink = [RCTDisplayLink new];
408413

409-
ReactInstance::JSRuntimeFlags options = {
410-
.isProfiling = false, .runtimeDiagnosticFlags = [RCTInstanceRuntimeDiagnosticFlags() UTF8String]};
411-
_reactInstance->initializeRuntime(options, [=](jsi::Runtime &runtime) {
412-
__strong __typeof(self) strongSelf = weakSelf;
413-
if (!strongSelf) {
414-
return;
415-
}
414+
ReactInstance::JSRuntimeFlags options = {
415+
.isProfiling = false,
416+
.runtimeDiagnosticFlags = [RCTInstanceRuntimeDiagnosticFlags() UTF8String]};
417+
_reactInstance->initializeRuntime(options, [=](jsi::Runtime &runtime) {
418+
__strong __typeof(self) strongSelf = weakSelf;
419+
if (!strongSelf) {
420+
return;
421+
}
416422

417-
[strongSelf->_turboModuleManager installJSBindings:runtime];
418-
facebook::react::bindNativeLogger(runtime, [](const std::string &message, unsigned int logLevel) {
419-
_RCTLogJavaScriptInternal(static_cast<RCTLogLevel>(logLevel), [NSString stringWithUTF8String:message.c_str()]);
420-
});
421-
RCTInstallNativeComponentRegistryBinding(runtime);
423+
[strongSelf->_turboModuleManager installJSBindings:runtime];
424+
facebook::react::bindNativeLogger(runtime, [](const std::string &message, unsigned int logLevel) {
425+
_RCTLogJavaScriptInternal(static_cast<RCTLogLevel>(logLevel), [NSString stringWithUTF8String:message.c_str()]);
426+
});
427+
RCTInstallNativeComponentRegistryBinding(runtime);
422428

423-
if (ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode()) {
424-
installLegacyUIManagerConstantsProviderBinding(runtime);
425-
}
429+
if (ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode()) {
430+
installLegacyUIManagerConstantsProviderBinding(runtime);
431+
}
426432

427-
[strongSelf->_delegate instance:strongSelf didInitializeRuntime:runtime];
433+
[strongSelf->_delegate instance:strongSelf didInitializeRuntime:runtime];
428434

429-
// Set up Display Link
430-
id<RCTDisplayLinkModuleHolder> moduleHolder = [[RCTBridgelessDisplayLinkModuleHolder alloc] initWithModule:timing];
431-
[strongSelf->_displayLink registerModuleForFrameUpdates:timing withModuleHolder:moduleHolder];
432-
[strongSelf->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
435+
// Set up Display Link
436+
id<RCTDisplayLinkModuleHolder> moduleHolder = [[RCTBridgelessDisplayLinkModuleHolder alloc] initWithModule:timing];
437+
[strongSelf->_displayLink registerModuleForFrameUpdates:timing withModuleHolder:moduleHolder];
438+
[strongSelf->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
433439

434-
// Attempt to load bundle synchronously, fallback to asynchronously.
435-
[strongSelf->_performanceLogger markStartForTag:RCTPLScriptDownload];
436-
[strongSelf _loadJSBundle:[strongSelf->_bridgeModuleDecorator.bundleManager bundleURL]];
437-
});
440+
// Attempt to load bundle synchronously, fallback to asynchronously.
441+
[strongSelf->_performanceLogger markStartForTag:RCTPLScriptDownload];
442+
[strongSelf _loadJSBundle:[strongSelf->_bridgeModuleDecorator.bundleManager bundleURL]];
443+
});
438444

439-
[_performanceLogger markStopForTag:RCTPLReactInstanceInit];
445+
[_performanceLogger markStopForTag:RCTPLReactInstanceInit];
440446
}
441447

442448
- (void)_attachBridgelessAPIsToModule:(id<RCTTurboModule>)module
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
#import <thread>
11+
#import "RCTJSThreadManager.h"
12+
13+
NS_ASSUME_NONNULL_BEGIN
14+
15+
@interface RCTJSThreadManager ()
16+
- (std::thread::id)threadId;
17+
@end
18+
19+
NS_ASSUME_NONNULL_END

packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTJSThreadManager.mm

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
#import "RCTJSThreadManager.h"
9+
#import "RCTJSThreadManager+Internal.h"
910

1011
#import <React/RCTAssert.h>
1112
#import <React/RCTCxxUtils.h>
@@ -18,6 +19,7 @@
1819
@implementation RCTJSThreadManager {
1920
NSThread *_jsThread;
2021
std::shared_ptr<facebook::react::RCTMessageThread> _jsMessageThread;
22+
std::thread::id _threadId;
2123
}
2224

2325
- (instancetype)init
@@ -28,6 +30,7 @@ - (instancetype)init
2830

2931
dispatch_block_t captureJSThreadRunLoop = ^(void) {
3032
__strong RCTJSThreadManager *strongSelf = weakSelf;
33+
strongSelf->_threadId = std::this_thread::get_id();
3134
strongSelf->_jsMessageThread =
3235
std::make_shared<facebook::react::RCTMessageThread>([NSRunLoop currentRunLoop], ^(NSError *error) {
3336
if (error) {
@@ -106,6 +109,12 @@ + (void)runRunLoop
106109
}
107110
}
108111

112+
#pragma mark - Internal
113+
- (std::thread::id)threadId
114+
{
115+
return _threadId;
116+
}
117+
109118
#pragma mark - Private
110119

111120
- (void)_handleError:(NSError *)error

0 commit comments

Comments
 (0)