Skip to content

Commit

Permalink
Implement lazy discovery scaffolding for loading NativeModules on dem…
Browse files Browse the repository at this point in the history
…and.

Reviewed By: javache

Differential Revision: D5364734

fbshipit-source-id: 5162f7d41434a3ba38c82fa610e84f865bfacf50
  • Loading branch information
Dmitry Zakharov authored and facebook-github-bot committed Aug 11, 2017
1 parent 21b1ed3 commit cf38b08
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 93 deletions.
5 changes: 5 additions & 0 deletions React/Base/RCTBridge+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ RCT_EXTERN void RCTVerifyAllModulesExported(NSArray *extraModules);
*/
- (RCTModuleData *)moduleDataForName:(NSString *)moduleName;

/**
* Registers additional classes with the ModuleRegistry.
*/
- (void)registerAdditionalModuleClasses:(NSArray<Class> *)newModules;

/**
* Systrace profiler toggling methods exposed for the RCTDevMenu
*/
Expand Down
5 changes: 5 additions & 0 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ - (void)invalidate
}
}

- (void)registerAdditionalModuleClasses:(NSArray<Class> *)modules
{
[self.batchedBridge registerAdditionalModuleClasses:modules];
}

- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
{
NSArray<NSString *> *ids = [moduleDotMethod componentsSeparatedByString:@"."];
Expand Down
9 changes: 9 additions & 0 deletions React/Base/RCTBridgeDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@
*/
- (BOOL)shouldBridgeUseCxxBridge:(RCTBridge *)bridge;

/**
* The bridge will call this method when a module been called from JS
* cannot be found among registered modules.
* It should return YES if the module with name 'moduleName' was registered
* in the implementation, and the system must attempt to look for it again among registered.
* If the module was not registered, return NO to prevent further searches.
*/
- (BOOL)bridge:(RCTBridge *)bridge didNotFindModule:(NSString *)moduleName;

/**
* The bridge will automatically attempt to load the JS source code from the
* location specified by the `sourceURLForBridge:` method, however, if you want
Expand Down
211 changes: 126 additions & 85 deletions React/CxxBridge/RCTCxxBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ @implementation RCTCxxBridge

// Native modules
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
NSArray<RCTModuleData *> *_moduleDataByID;
NSArray<Class> *_moduleClassesByID;
NSMutableArray<RCTModuleData *> *_moduleDataByID;
NSMutableArray<Class> *_moduleClassesByID;
NSUInteger _modulesInitializedOnMainQueue;
RCTDisplayLink *_displayLink;

Expand Down Expand Up @@ -195,6 +195,10 @@ - (instancetype)initWithParentBridge:(RCTBridge *)bridge
_pendingCalls = [NSMutableArray new];
_displayLink = [RCTDisplayLink new];

_moduleDataByName = [NSMutableDictionary new];
_moduleClassesByID = [NSMutableArray new];
_moduleDataByID = [NSMutableArray new];

[RCTBridge setCurrentBridge:self];
}
return self;
Expand Down Expand Up @@ -275,8 +279,13 @@ - (void)start

dispatch_group_t prepareBridge = dispatch_group_create();

[_performanceLogger markStartForTag:RCTPLNativeModuleInit];

[self registerExtraModules];
// Initialize all native modules that cannot be loaded lazily
[self _initModulesWithDispatchGroup:prepareBridge];
[self _initModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];

[_performanceLogger markStopForTag:RCTPLNativeModuleInit];

// This doesn't really do anything. The real work happens in initializeBridge.
_reactInstance.reset(new Instance);
Expand Down Expand Up @@ -442,7 +451,16 @@ - (BOOL)moduleIsInitialized:(Class)moduleClass
[_performanceLogger markStartForTag:RCTPLNativeModulePrepareConfig];
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge buildModuleRegistry]", nil);

auto registry = std::make_shared<ModuleRegistry>(createNativeModules(_moduleDataByID, self, _reactInstance));
__weak __typeof(self) weakSelf = self;
ModuleRegistry::ModuleNotFoundCallback moduleNotFoundCallback = ^bool(const std::string &name) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
return [strongSelf.delegate respondsToSelector:@selector(bridge:didNotFindModule:)] &&
[strongSelf.delegate bridge:strongSelf didNotFindModule:@(name.c_str())];
};

auto registry = std::make_shared<ModuleRegistry>(
createNativeModules(_moduleDataByID, self, _reactInstance),
moduleNotFoundCallback);

[_performanceLogger markStopForTag:RCTPLNativeModulePrepareConfig];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
Expand Down Expand Up @@ -501,20 +519,66 @@ - (NSArray *)configForModuleName:(NSString *)moduleName
return moduleData.config;
}

- (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
- (NSArray<RCTModuleData *> *)registerModulesForClasses:(NSArray<Class> *)moduleClasses
{
[_performanceLogger markStartForTag:RCTPLNativeModuleInit];
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] autoexported moduleData", nil);

NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray arrayWithCapacity:moduleClasses.count];
for (Class moduleClass in moduleClasses) {
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);

// Don't initialize the old executor in the new bridge.
// TODO mhorowitz #10487027: after D3175632 lands, we won't need
// this, because it won't be eagerly initialized.
if ([moduleName isEqualToString:@"RCTJSCExecutor"]) {
continue;
}

// Check for module name collisions
RCTModuleData *moduleData = _moduleDataByName[moduleName];
if (moduleData) {
if (moduleData.hasInstance) {
// Existing module was preregistered, so it takes precedence
continue;
} else if ([moduleClass new] == nil) {
// The new module returned nil from init, so use the old module
continue;
} else if ([moduleData.moduleClass new] != nil) {
// Both modules were non-nil, so it's unclear which should take precedence
RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the "
"name '%@', but name was already registered by class %@",
moduleClass, moduleName, moduleData.moduleClass);
}
}

// Instantiate moduleData
// TODO #13258411: can we defer this until config generation?
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self];

_moduleDataByName[moduleName] = moduleData;
[_moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
[_moduleDataByID addObjectsFromArray:moduleDataByID];

RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

return moduleDataByID;
}

- (void)registerExtraModules
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] extraModules", nil);

NSArray<id<RCTBridgeModule>> *extraModules = nil;
if (self.delegate) {
if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) {
extraModules = [self.delegate extraModulesForBridge:_parentBridge];
}
if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) {
extraModules = [self.delegate extraModulesForBridge:_parentBridge];
} else if (self.moduleProvider) {
extraModules = self.moduleProvider();
}

RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

#if RCT_DEBUG
Expand All @@ -524,10 +588,6 @@ - (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
});
#endif

NSMutableArray<Class> *moduleClassesByID = [NSMutableArray new];
NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new];
NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];

RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] preinitialized moduleData", nil);
// Set up moduleData for pre-initialized module instances
Expand All @@ -537,7 +597,7 @@ - (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup

if (RCT_DEBUG) {
// Check for name collisions between preregistered modules
RCTModuleData *moduleData = moduleDataByName[moduleName];
RCTModuleData *moduleData = _moduleDataByName[moduleName];
if (moduleData) {
RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the "
"name '%@', but name was already registered by class %@",
Expand All @@ -547,84 +607,60 @@ - (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
}

// Instantiate moduleData container
RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:module
bridge:self];
moduleDataByName[moduleName] = moduleData;
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:module bridge:self];
_moduleDataByName[moduleName] = moduleData;
[_moduleClassesByID addObject:moduleClass];
[_moduleDataByID addObject:moduleData];
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}

RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] autoexported moduleData", nil);
// Set up moduleData for automatically-exported modules
for (Class moduleClass in RCTGetModuleClasses()) {
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
- (void)_initModules:(NSArray<id<RCTBridgeModule>> *)modules
withDispatchGroup:(dispatch_group_t)dispatchGroup
lazilyDiscovered:(BOOL)lazilyDiscovered
{
RCTAssert(!(RCTIsMainQueue() && lazilyDiscovered), @"Lazy discovery can only happen off the Main Queue");

// Don't initialize the old executor in the new bridge.
// TODO mhorowitz #10487027: after D3175632 lands, we won't need
// this, because it won't be eagerly initialized.
if ([moduleName isEqual:@"RCTJSCExecutor"]) {
continue;
// Set up moduleData for automatically-exported modules
NSArray<RCTModuleData *> *moduleDataById = [self registerModulesForClasses:modules];

#ifdef RCT_DEBUG
if (lazilyDiscovered) {
// Lazily discovered modules do not require instantiation here,
// as they are not allowed to have pre-instantiated instance
// and must not require the main queue.
for (RCTModuleData *moduleData in moduleDataById) {
RCTAssert(!(moduleData.requiresMainQueueSetup || moduleData.hasInstance),
@"Module \'%@\' requires initialization on the Main Queue or has pre-instantiated, which is not supported for the lazily discovered modules.", moduleData.name);
}

// Check for module name collisions
RCTModuleData *moduleData = moduleDataByName[moduleName];
if (moduleData) {
if (moduleData.hasInstance) {
// Existing module was preregistered, so it takes precedence
continue;
} else if ([moduleClass new] == nil) {
// The new module returned nil from init, so use the old module
continue;
} else if ([moduleData.moduleClass new] != nil) {
// Both modules were non-nil, so it's unclear which should take precedence
RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the "
"name '%@', but name was already registered by class %@",
moduleClass, moduleName, moduleData.moduleClass);
}
else
#endif
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] moduleData.hasInstance", nil);
// Dispatch module init onto main thread for those modules that require it
// For non-lazily discovered modules we run through the entire set of modules
// that we have, otherwise some modules coming from the delegate
// or module provider block, will not be properly instantiated.
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) {
// Modules that were pre-initialized should ideally be set up before
// bridge init has finished, otherwise the caller may try to access the
// module directly rather than via `[bridge moduleForClass:]`, which won't
// trigger the lazy initialization process. If the module cannot safely be
// set up on the current thread, it will instead be async dispatched
// to the main thread to be set up in _prepareModulesWithDispatchGroup:.
(void)[moduleData instance];
}
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

// Instantiate moduleData
// TODO #13258411: can we defer this until config generation?
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
bridge:self];
moduleDataByName[moduleName] = moduleData;
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

// Store modules
_moduleDataByID = [moduleDataByID copy];
_moduleDataByName = [moduleDataByName copy];
_moduleClassesByID = [moduleClassesByID copy];

RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] moduleData.hasInstance", nil);
// Dispatch module init onto main thead for those modules that require it
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.hasInstance &&
(!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) {
// Modules that were pre-initialized should ideally be set up before
// bridge init has finished, otherwise the caller may try to access the
// module directly rather than via `[bridge moduleForClass:]`, which won't
// trigger the lazy initialization process. If the module cannot safely be
// set up on the current thread, it will instead be async dispatched
// to the main thread to be set up in the loop below.
(void)[moduleData instance];
}
// From this point on, RCTDidInitializeModuleNotification notifications will
// be sent the first time a module is accessed.
_moduleSetupComplete = YES;
[self _prepareModulesWithDispatchGroup:dispatchGroup];
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

// From this point on, RCTDidInitializeModuleNotification notifications will
// be sent the first time a module is accessed.
_moduleSetupComplete = YES;

[self _prepareModulesWithDispatchGroup:dispatchGroup];

[_performanceLogger markStopForTag:RCTPLNativeModuleInit];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

#if RCT_PROFILE
if (RCTProfileIsProfiling()) {
Expand All @@ -634,6 +670,11 @@ - (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
#endif
}

- (void)registerAdditionalModuleClasses:(NSArray<Class> *)modules
{
[self _initModules:modules withDispatchGroup:NULL lazilyDiscovered:YES];
}

- (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
{
RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge prepareModulesWithDispatch]", nil);
Expand All @@ -655,6 +696,7 @@ - (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup

// Set up modules that require main thread init or constants export
[_performanceLogger setValue:0 forTag:RCTPLNativeModuleMainThread];

for (RCTModuleData *moduleData in _moduleDataByID) {
if (whitelistedModules && ![whitelistedModules containsObject:[moduleData moduleClass]]) {
continue;
Expand Down Expand Up @@ -687,7 +729,6 @@ - (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
_modulesInitializedOnMainQueue++;
}
}

[_performanceLogger setValue:_modulesInitializedOnMainQueue forTag:RCTPLNativeModuleMainThreadUsesCount];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
Expand Down
23 changes: 16 additions & 7 deletions ReactCommon/cxxreact/ModuleRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ std::string normalizeName(std::string name) {

}

ModuleRegistry::ModuleRegistry(std::vector<std::unique_ptr<NativeModule>> modules)
: modules_(std::move(modules)) {}
ModuleRegistry::ModuleRegistry(std::vector<std::unique_ptr<NativeModule>> modules, ModuleNotFoundCallback callback)
: modules_{std::move(modules)}, moduleNotFoundCallback_{callback} {}

void ModuleRegistry::updateModuleNamesFromIndex(size_t index) {
for (; index < modules_.size(); index++ ) {
Expand Down Expand Up @@ -82,13 +82,22 @@ folly::Optional<ModuleConfig> ModuleRegistry::getConfig(const std::string& name)
}

auto it = modulesByName_.find(name);

if (it == modulesByName_.end()) {
unknownModules_.insert(name);
return nullptr;
if (unknownModules_.find(name) != unknownModules_.end()) {
return nullptr;
}
if (!moduleNotFoundCallback_ ||
!moduleNotFoundCallback_(name) ||
(it = modulesByName_.find(name)) == modulesByName_.end()) {
unknownModules_.insert(name);
return nullptr;
}
}
size_t index = it->second;

CHECK(it->second < modules_.size());
NativeModule* module = modules_[it->second].get();
CHECK(index < modules_.size());
NativeModule *module = modules_[index].get();

// string name, object constants, array methodNames (methodId is index), [array promiseMethodIds], [array syncMethodIds]
folly::dynamic config = folly::dynamic::array(name);
Expand Down Expand Up @@ -131,7 +140,7 @@ folly::Optional<ModuleConfig> ModuleRegistry::getConfig(const std::string& name)
// no constants or methods
return nullptr;
} else {
return ModuleConfig({it->second, config});
return ModuleConfig{index, config};
}
}

Expand Down
Loading

0 comments on commit cf38b08

Please sign in to comment.