Skip to content

Commit

Permalink
Random Access Modules native infra
Browse files Browse the repository at this point in the history
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
  • Loading branch information
martinbigio authored and Facebook Github Bot 8 committed Mar 13, 2016
1 parent 88ebdb2 commit 7338c57
Showing 1 changed file with 102 additions and 1 deletion.
103 changes: 102 additions & 1 deletion React/Executors/RCTJSCExecutor.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@

static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled";

// TODO: add lineNo
typedef struct ModuleData
{
uint32_t offset;
} ModuleData;

@interface RCTJavaScriptContext : NSObject <RCTInvalidating>

@property (nonatomic, strong, readonly) JSContext *context;
Expand Down Expand Up @@ -94,7 +100,10 @@ @implementation RCTJSCExecutor
{
RCTJavaScriptContext *_context;
NSThread *_javaScriptThread;
NSURL *_bundleURL;

NSData *_bundle;
JSStringRef _bundleURL;
CFMutableDictionaryRef _jsModules;
}

@synthesize valid = _valid;
Expand Down Expand Up @@ -385,6 +394,10 @@ - (void)invalidate

_valid = NO;

if (_jsModules) {
CFRelease(_jsModules);
}

#if RCT_DEV
[[NSNotificationCenter defaultCenter] removeObserver:self];
#endif
Expand Down Expand Up @@ -534,9 +547,21 @@ - (void)executeApplicationScript:(NSData *)script
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((^{
Expand Down Expand Up @@ -633,6 +658,82 @@ - (void)injectJSONText:(NSString *)script
}), 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
Expand Down

0 comments on commit 7338c57

Please sign in to comment.