From 103c863eaa325b191107e58d59c64243f67d37cd Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Tue, 7 Jul 2020 16:22:59 -0700 Subject: [PATCH] Eagerly initialize TurboModules before executing JS bundle Summary: ## Context 1. In FBReactModule jsExecutorForBridge, we asynchronously initialize a list of TurboModules on the main queue: https://fburl.com/diffusion/i56wi3px 2. After initializing the bridge, we start executing the JS bundle, here: https://github.com/facebook/react-native/blob/e23e9328aa164d0a70fe4f16042c982e7801d924/React/CxxBridge/RCTCxxBridge.mm#L414-L417. Since bridge initialization knows nothing about TurboModule eager initialization, this happens concurrently with 1, and starts requiring NativeModules/TurboModules on the JS thread. ## The Race 1. Both the main thread and the JS thread race to create a TurboModule that requires main queue setup. 2. The JS thread wins, and starts creating the TurboModule. Meanwhile, the main thread blocks, waiting on a signal here, in RCTTurboModuleManager: https://github.com/facebook/react-native/blob/e23e9328aa164d0a70fe4f16042c982e7801d924/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm#L430 3. The JS thread tries to dispatch_sync to the main queue to setup the TurboModule because the TurboModule requires main queue setup, here: https://github.com/facebook/react-native/blob/e23e9328aa164d0a70fe4f16042c982e7801d924/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm#L402 4. We deadlock. ## The fix Succinctly, NativeModule eager initialization finishes before execute the JS bundle, but TurboModule initialization doesn't. This diff corrects that mistake. The changes in this diff: 1. The RN application via the TurboModuleManager delegate can now optionally provide the names of all eagerly initialized TurboModules by implementing two methods `getEagerInitModuleNames`, `getEagerInitMainQueueModuleNames`. 2. The TurboModuleManager grabs these two lists from the delegate, and exposes them to its owner via the `RCTTurboModuleRegistry` protocol. 3. The RCTCxxBridge, which already owns a `id` object, uses it to eagerly initialize the TurboModules in these two lists with the correct timing requirements. This is exactly how we implement eager initialization in Android. **Note:** Right now, phase one and two of TurboModule eager initialization happen after phase one and two of NativeModule eager initialization. We could make the timing even more correct by initializing the TurboModules at the exact same time we initialize the NativeModules. However, that would require a bit more surgery on the bridge, and the bridge delegate. I think this is good enough for now. Changelog: [iOS][Fixed] - Fix TurboModule eager init race Reviewed By: PeteTheHeat Differential Revision: D22406171 fbshipit-source-id: 4715be0bceb478a8e4aa206180c0316eaaf287e8 --- React/Base/RCTBridgeModule.h | 3 +++ React/CxxBridge/RCTCxxBridge.mm | 21 +++++++++++++++++++ .../core/platform/ios/RCTTurboModuleManager.h | 3 +++ .../platform/ios/RCTTurboModuleManager.mm | 18 ++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 98b7754ae75..0f82178d63a 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -360,6 +360,9 @@ RCT_EXTERN_C_END */ - (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure; - (BOOL)moduleIsInitialized:(const char *)moduleName; + +- (NSArray *)eagerInitModuleNames; +- (NSArray *)eagerInitMainQueueModuleNames; @end /** diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 0d9f380da7c..4922eb65707 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -381,6 +381,27 @@ struct RCTInstanceCallback : public InstanceCallback { })); } + /** + * id jsExecutorFactory may create and assign an id object to + * RCTCxxBridge If id is assigned by this time, eagerly initialize all TurboModules + */ + if (_turboModuleLookupDelegate) { + for (NSString *moduleName in [_turboModuleLookupDelegate eagerInitModuleNames]) { + [_turboModuleLookupDelegate moduleForName:[moduleName UTF8String]]; + } + + for (NSString *moduleName in [_turboModuleLookupDelegate eagerInitMainQueueModuleNames]) { + if (RCTIsMainQueue()) { + [_turboModuleLookupDelegate moduleForName:[moduleName UTF8String]]; + } else { + id turboModuleLookupDelegate = _turboModuleLookupDelegate; + dispatch_group_async(prepareBridge, dispatch_get_main_queue(), ^{ + [turboModuleLookupDelegate moduleForName:[moduleName UTF8String]]; + }); + } + } + } + // Dispatch the instance initialization as soon as the initial module metadata has // been collected (see initModules) dispatch_group_enter(prepareBridge); diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.h b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.h index 7edc90ca386..62779de897d 100644 --- a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.h +++ b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.h @@ -19,6 +19,9 @@ - (std::shared_ptr)getTurboModule:(const std::string &)name initParams: (const facebook::react::ObjCTurboModule::InitParams &)params; +@optional +- (NSArray *)getEagerInitModuleNames; +- (NSArray *)getEagerInitMainQueueModuleNames; @optional diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm index ce61d802a7f..218059dfaf8 100644 --- a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm +++ b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm @@ -714,6 +714,24 @@ static Class getFallbackClassFromName(const char *name) return _turboModuleHolders.find(moduleName) != _turboModuleHolders.end(); } +- (NSArray *)eagerInitModuleNames +{ + if ([_delegate respondsToSelector:@selector(getEagerInitModuleNames)]) { + return [_delegate getEagerInitModuleNames]; + } + + return @[]; +} + +- (NSArray *)eagerInitMainQueueModuleNames +{ + if ([_delegate respondsToSelector:@selector(getEagerInitMainQueueModuleNames)]) { + return [_delegate getEagerInitMainQueueModuleNames]; + } + + return @[]; +} + #pragma mark Invalidation logic - (void)bridgeWillInvalidateModules:(NSNotification *)notification