Skip to content

Commit

Permalink
Eagerly initialize TurboModules before executing JS bundle
Browse files Browse the repository at this point in the history
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<RCTTurboModuleRegistry>` 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
  • Loading branch information
RSNara authored and facebook-github-bot committed Jul 7, 2020
1 parent 5c24746 commit 103c863
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 0 deletions.
3 changes: 3 additions & 0 deletions React/Base/RCTBridgeModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ RCT_EXTERN_C_END
*/
- (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure;
- (BOOL)moduleIsInitialized:(const char *)moduleName;

- (NSArray<NSString *> *)eagerInitModuleNames;
- (NSArray<NSString *> *)eagerInitMainQueueModuleNames;
@end

/**
Expand Down
21 changes: 21 additions & 0 deletions React/CxxBridge/RCTCxxBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,27 @@ - (void)start
}));
}

/**
* id<RCTCxxBridgeDelegate> jsExecutorFactory may create and assign an id<RCTTurboModuleLookupDelegate> object to
* RCTCxxBridge If id<RCTTurboModuleLookupDelegate> 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<RCTTurboModuleLookupDelegate> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
initParams:
(const facebook::react::ObjCTurboModule::InitParams &)params;
@optional
- (NSArray<NSString *> *)getEagerInitModuleNames;
- (NSArray<NSString *> *)getEagerInitMainQueueModuleNames;

@optional

Expand Down
18 changes: 18 additions & 0 deletions ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,24 @@ - (BOOL)moduleIsInitialized:(const char *)moduleName
return _turboModuleHolders.find(moduleName) != _turboModuleHolders.end();
}

- (NSArray<NSString *> *)eagerInitModuleNames
{
if ([_delegate respondsToSelector:@selector(getEagerInitModuleNames)]) {
return [_delegate getEagerInitModuleNames];
}

return @[];
}

- (NSArray<NSString *> *)eagerInitMainQueueModuleNames
{
if ([_delegate respondsToSelector:@selector(getEagerInitMainQueueModuleNames)]) {
return [_delegate getEagerInitMainQueueModuleNames];
}

return @[];
}

#pragma mark Invalidation logic

- (void)bridgeWillInvalidateModules:(NSNotification *)notification
Expand Down

0 comments on commit 103c863

Please sign in to comment.