From 7338c5704eb766a9e60c3c9fed9a8384401e38fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Sun, 13 Mar 2016 11:13:39 -0700 Subject: [PATCH] Random Access Modules native infra Summary:At the moment, to initialize a React Native app, the entire JS bundle needs to be loaded. Parsing JS code takes a while which makes paying for every feature the app has very expensive on start up. Even worse, as the bundle gets bigger and bigger because the app has more and more features, start up time becomes slower. This rev introduces the few remaining pieces of infrastructure to load JS modules incrementally. This way, on start up we only inject into JSC the modules we actually need. More importantly, by using this piece of infrastructure, the app start up time won't be affected as the JS bundle increases it's size. Props to davidaurelio and tadeuzagallo for the original work. I'm just wrapping their work. Differential Revision: D2995425 fb-gh-sync-id: caaaa880b5370c3bb36a11ae694dc303cd53d0e2 shipit-source-id: caaaa880b5370c3bb36a11ae694dc303cd53d0e2 --- React/Executors/RCTJSCExecutor.m | 103 ++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/React/Executors/RCTJSCExecutor.m b/React/Executors/RCTJSCExecutor.m index 2a0e5f137..af94cf2e1 100644 --- a/React/Executors/RCTJSCExecutor.m +++ b/React/Executors/RCTJSCExecutor.m @@ -32,6 +32,12 @@ NSString *const RCTJavaScriptContextCreatedNotification = @"RCTJavaScriptContext static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled"; +// TODO: add lineNo +typedef struct ModuleData +{ + uint32_t offset; +} ModuleData; + @interface RCTJavaScriptContext : NSObject @property (nonatomic, strong, readonly) JSContext *context; @@ -94,7 +100,10 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) { RCTJavaScriptContext *_context; NSThread *_javaScriptThread; - NSURL *_bundleURL; + + NSData *_bundle; + JSStringRef _bundleURL; + CFMutableDictionaryRef _jsModules; } @synthesize valid = _valid; @@ -385,6 +394,10 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) _valid = NO; + if (_jsModules) { + CFRelease(_jsModules); + } + #if RCT_DEV [[NSNotificationCenter defaultCenter] removeObserver:self]; #endif @@ -534,9 +547,21 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) sourceURL:(NSURL *)sourceURL onComplete:(RCTJavaScriptCompleteBlock)onComplete { + // Check if it's a `RAM bundle` ("Random Access Modules" bundle). + // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. + // The benefit of RAM bundle over a regular bundle is that we can lazily inject + // modules into JSC as they're required. + static const uint32_t ramBundleMagicNumber = 0xFB0BD1E5; + uint32_t magicNumber = *(uint32_t *)script.bytes; + if (magicNumber == ramBundleMagicNumber) { + script = [self loadRAMBundle:script]; + } + RCTAssertParam(script); RCTAssertParam(sourceURL); + _bundleURL = JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String); + __weak RCTJSCExecutor *weakSelf = self; [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ @@ -633,6 +658,82 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) }), 0, @"js_call,json_call", (@{@"objectName": objectName}))]; } +static int streq(const char *a, const char *b) +{ + return strcmp(a, b) == 0; +} + +static void freeModuleData(__unused CFAllocatorRef allocator, void *ptr) +{ + free(ptr); +} + +- (NSData *)loadRAMBundle:(NSData *)script +{ + __weak RCTJSCExecutor *weakSelf = self; + _context.context[@"nativeRequire"] = ^(NSString *moduleName) { + RCTJSCExecutor *strongSelf = weakSelf; + if (!strongSelf || !moduleName) { + return; + } + + ModuleData *moduleData = (ModuleData *)CFDictionaryGetValue(strongSelf->_jsModules, moduleName.UTF8String); + JSStringRef module = JSStringCreateWithUTF8CString((const char *)_bundle.bytes + moduleData->offset); + JSValueRef jsError = NULL; + JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, module, NULL, strongSelf->_bundleURL, NULL, &jsError); + + CFDictionaryRemoveValue(strongSelf->_jsModules, moduleName.UTF8String); + JSStringRelease(module); + + if (!result) { + dispatch_async(dispatch_get_main_queue(), ^{ + RCTFatal(RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError)); + [strongSelf invalidate]; + }); + } + }; + + _bundle = script; + CFDictionaryKeyCallBacks keyCallbacks = { 0, NULL, NULL, NULL, (CFDictionaryEqualCallBack)streq, (CFDictionaryHashCallBack)strlen }; + // once a module has been loaded free its space from the heap, remove it from the index and release the module name + CFDictionaryValueCallBacks valueCallbacks = { 0, NULL, (CFDictionaryReleaseCallBack)freeModuleData, NULL, NULL }; + _jsModules = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks); + + const uint8_t *bytes = script.bytes; + uint32_t currentOffset = 4; // skip magic number + + uint32_t tableLength; + memcpy(&tableLength, bytes + currentOffset, sizeof(tableLength)); + tableLength = NSSwapLittleIntToHost(tableLength); + + uint32_t baseOffset = currentOffset + tableLength; + currentOffset += sizeof(baseOffset); + + while (currentOffset < baseOffset) { + const char *moduleName = (const char *)bytes + currentOffset; + uint32_t offset; + + // the space allocated for each module's metada gets freed when the module is injected into JSC on `nativeRequire` + ModuleData *moduleData = malloc(sizeof(moduleData)); + + // skip module name and null byte terminator + currentOffset += strlen(moduleName) + 1; + + // read and save offset + memcpy(&offset, bytes + currentOffset, sizeof(offset)); + offset = NSSwapLittleIntToHost(offset); + moduleData->offset = baseOffset + offset; + + // TODO: replace length with lineNo + currentOffset += sizeof(offset) * 2; // skip both offset and lenght + + CFDictionarySetValue(_jsModules, moduleName, moduleData); + } + + uint32_t offset = ((ModuleData *)CFDictionaryGetValue(_jsModules, ""))->offset; + return [NSData dataWithBytesNoCopy:((char *) script.bytes) + offset length:script.length - offset freeWhenDone:NO]; +} + RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)name) { #pragma clang diagnostic push