Skip to content

Commit

Permalink
Use JSStringCreateWithUTF8CString and skip NSString decoding when loa…
Browse files Browse the repository at this point in the history
…ding the bundle

Summary: public

Benchmarking our startup path has shown we spend a lot of time decoding strings (iPhone 4S / iPhone 5):

* reading a 2MB JS bundle: 35ms / 15ms
* decoding is to an `NSString`: 186ms / 78ms
* transforming that to a `JSString`: 29ms / 10ms

Instead of going through an `NSString` transformation, we generate a null-terminated bundle (0.1ms / 0.05ms to copy the data) and use `JSStringCreateWithUTF8CString` (121ms / 53ms) to generate the string. That makes decoding 70% faster.

Reviewed By: javache

Differential Revision: D2541140

fb-gh-sync-id: 09a016b8edfd46a9b62682c76705564d2024e75e
  • Loading branch information
ndfred authored and facebook-github-bot-3 committed Oct 16, 2015
1 parent 8e2ec64 commit 4a3857e
Show file tree
Hide file tree
Showing 10 changed files with 47 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ - (void)setUp
- (void)testNativeLoggingHookExceptionBehavior
{
dispatch_semaphore_t doneSem = dispatch_semaphore_create(0);
[_executor executeApplicationScript:@"var x = {toString: function() { throw 1; }}; nativeLoggingHook(x);"
[_executor executeApplicationScript:[@"var x = {toString: function() { throw 1; }}; nativeLoggingHook(x);" dataUsingEncoding:NSUTF8StringEncoding]
sourceURL:[NSURL URLWithString:@"file://"]
onComplete:^(__unused id error){
dispatch_semaphore_signal(doneSem);
Expand Down Expand Up @@ -128,7 +128,7 @@ function require(module) { \
} \
";

[_executor executeApplicationScript:script sourceURL:[NSURL URLWithString:@"http://localhost:8081/"] onComplete:^(__unused NSError *error) {
[_executor executeApplicationScript:[script dataUsingEncoding:NSUTF8StringEncoding] sourceURL:[NSURL URLWithString:@"http://localhost:8081/"] onComplete:^(__unused NSError *error) {
NSMutableArray *params = [NSMutableArray new];
id data = @1;
for (int i = 0; i < 4; i++) {
Expand Down
2 changes: 1 addition & 1 deletion Libraries/WebSocket/RCTWebSocketExecutor.m
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ - (void)sendMessage:(NSDictionary *)message waitForReply:(RCTWSMessageCallback)c
});
}

- (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete
- (void)executeApplicationScript:(NSData *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
NSDictionary *message = @{
@"method": @"executeApplicationScript",
Expand Down
19 changes: 10 additions & 9 deletions React/Base/RCTBatchedBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ - (void)start
dispatch_group_t initModulesAndLoadSource = dispatch_group_create();
dispatch_group_enter(initModulesAndLoadSource);
__weak RCTBatchedBridge *weakSelf = self;
__block NSString *sourceCode;
[self loadSource:^(NSError *error, NSString *source) {
__block NSData *sourceCode;
[self loadSource:^(NSError *error, NSData *source) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf stopLoadingWithError:error];
Expand Down Expand Up @@ -184,7 +184,7 @@ - (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad
RCTPerformanceLoggerStart(RCTPLScriptDownload);
int cookie = RCTProfileBeginAsyncEvent(0, @"JavaScript download", nil);

RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSString *source) {
RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSData *source) {
RCTProfileEndAsyncEvent(0, @"init,download", cookie, @"JavaScript download", nil);
RCTPerformanceLoggerEnd(RCTPLScriptDownload);

Expand All @@ -195,12 +195,13 @@ - (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad

// Force JS __DEV__ value to match RCT_DEBUG
if (shouldOverrideDev) {
NSRange range = [source rangeOfString:@"__DEV__="];
NSString *sourceString = [[NSString alloc] initWithData:source encoding:NSUTF8StringEncoding];
NSRange range = [sourceString rangeOfString:@"__DEV__="];
RCTAssert(range.location != NSNotFound, @"It looks like the implementation"
"of __DEV__ has changed. Update -[RCTBatchedBridge loadSource:].");
NSRange valueRange = {range.location + range.length, 2};
if ([[source substringWithRange:valueRange] isEqualToString:@"!1"]) {
source = [source stringByReplacingCharactersInRange:valueRange withString:@" 1"];
if ([[sourceString substringWithRange:valueRange] isEqualToString:@"!1"]) {
source = [[sourceString stringByReplacingCharactersInRange:valueRange withString:@" 1"] dataUsingEncoding:NSUTF8StringEncoding];
}
}

Expand Down Expand Up @@ -355,15 +356,15 @@ - (void)injectJSONConfiguration:(NSString *)configJSON
callback:onComplete];
}

- (void)executeSourceCode:(NSString *)sourceCode
- (void)executeSourceCode:(NSData *)sourceCode
{
if (!self.valid || !_javaScriptExecutor) {
return;
}

RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
sourceCodeModule.scriptURL = self.bundleURL;
sourceCodeModule.scriptText = sourceCode;
sourceCodeModule.scriptData = sourceCode;

[self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:^(NSError *loadError) {
if (!self.isValid) {
Expand Down Expand Up @@ -585,7 +586,7 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer
}
}

- (void)enqueueApplicationScript:(NSString *)script
- (void)enqueueApplicationScript:(NSData *)script
url:(NSURL *)url
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
Expand Down
2 changes: 1 addition & 1 deletion React/Base/RCTBridgeDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/

typedef void (^RCTSourceLoadBlock)(NSError *error, NSString *source);
typedef void (^RCTSourceLoadBlock)(NSError *error, NSData *source);

@class RCTBridge;

Expand Down
2 changes: 1 addition & 1 deletion React/Base/RCTJavaScriptExecutor.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
/**
* Runs an application script, and notifies of the script load being complete via `onComplete`.
*/
- (void)executeApplicationScript:(NSString *)script
- (void)executeApplicationScript:(NSData *)script
sourceURL:(NSURL *)sourceURL
onComplete:(RCTJavaScriptCompleteBlock)onComplete;

Expand Down
10 changes: 6 additions & 4 deletions React/Base/RCTJavaScriptLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ + (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComp
NSString *filePath = scriptURL.path;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
NSString *rawText = [NSString stringWithContentsOfFile:filePath usedEncoding:NULL error:&error];
onComplete(error, rawText);
NSData *source = [NSData dataWithContentsOfFile:filePath
options:NSDataReadingMappedIfSafe
error:&error];
onComplete(error, source);
});
return;
}
Expand Down Expand Up @@ -71,10 +73,10 @@ + (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComp
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];

// Handle HTTP errors
if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) {
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]] &&
Expand All @@ -101,7 +103,7 @@ + (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComp
onComplete(error, nil);
return;
}
onComplete(nil, rawText);
onComplete(nil, data);
}];

[task resume];
Expand Down
10 changes: 8 additions & 2 deletions React/Executors/RCTContextExecutor.m
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ - (void)executeJSCall:(NSString *)name
}), 0, @"js_call", (@{@"module":name, @"method": method, @"args": arguments}))];
}

- (void)executeApplicationScript:(NSString *)script
- (void)executeApplicationScript:(NSData *)script
sourceURL:(NSURL *)sourceURL
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
Expand All @@ -508,9 +508,15 @@ - (void)executeApplicationScript:(NSString *)script
return;
}

// JSStringCreateWithUTF8CString expects a null terminated C string
NSMutableData *nullTerminatedScript = [NSMutableData dataWithCapacity:script.length + 1];

[nullTerminatedScript appendData:script];
[nullTerminatedScript appendBytes:"" length:1];

RCTPerformanceLoggerStart(RCTPLScriptExecution);
JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
JSStringRef execJSString = JSStringCreateWithUTF8CString(nullTerminatedScript.bytes);
JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError);
JSStringRelease(jsURL);
Expand Down
25 changes: 13 additions & 12 deletions React/Executors/RCTWebViewExecutor.m
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ - (void)executeJSCall:(NSString *)name
* debugger. So we have to use this (essentially) async API - and register
* ourselves as the webview delegate to be notified when load is complete.
*/
- (void)executeApplicationScript:(NSString *)script
- (void)executeApplicationScript:(NSData *)script
sourceURL:(NSURL *)url
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
Expand All @@ -142,6 +142,7 @@ - (void)executeApplicationScript:(NSString *)script
}

RCTAssert(onComplete != nil, @"");
NSString *scriptString = [[NSString alloc] initWithData:script encoding:NSUTF8StringEncoding];
__weak RCTWebViewExecutor *weakSelf = self;
_onApplicationScriptLoaded = ^(NSError *error){
RCTWebViewExecutor *strongSelf = weakSelf;
Expand All @@ -163,23 +164,23 @@ - (void)executeApplicationScript:(NSString *)script
}];
[_objectsToInject removeAllObjects];
[scriptWithInjections appendString:@"/* END NATIVELY INJECTED OBJECTS */\n"];
[scriptWithInjections appendString:script];
script = scriptWithInjections;
[scriptWithInjections appendString:scriptString];
scriptString = scriptWithInjections;
}

script = [_commentsRegex stringByReplacingMatchesInString:script
options:0
range:NSMakeRange(0, script.length)
withTemplate:@""];
script = [_scriptTagsRegex stringByReplacingMatchesInString:script
options:0
range:NSMakeRange(0, script.length)
withTemplate:@"\\\\<$1\\\\>"];
scriptString = [_commentsRegex stringByReplacingMatchesInString:scriptString
options:0
range:NSMakeRange(0, script.length)
withTemplate:@""];
scriptString = [_scriptTagsRegex stringByReplacingMatchesInString:scriptString
options:0
range:NSMakeRange(0, script.length)
withTemplate:@"\\\\<$1\\\\>"];

NSString *runScript =
[NSString
stringWithFormat:@"<html><head></head><body><script type='text/javascript'>%@</script></body></html>",
script
scriptString
];
[_webView loadHTMLString:runScript baseURL:url];
}
Expand Down
2 changes: 1 addition & 1 deletion React/Modules/RCTSourceCode.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

@interface RCTSourceCode : NSObject <RCTBridgeModule>

@property (nonatomic, copy) NSString *scriptText;
@property (nonatomic, copy) NSData *scriptData;
@property (nonatomic, copy) NSURL *scriptURL;

@end
6 changes: 4 additions & 2 deletions React/Modules/RCTSourceCode.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ - (void)setScriptText:(NSString *)scriptText {}
RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback
failureCallback:(RCTResponseErrorBlock)failureCallback)
{
if (RCT_DEV && self.scriptText && self.scriptURL) {
successCallback(@[@{@"text": self.scriptText, @"url": self.scriptURL.absoluteString}]);
if (RCT_DEV && self.scriptData && self.scriptURL) {
NSString *scriptText = [[NSString alloc] initWithData:self.scriptData encoding:NSUTF8StringEncoding];

successCallback(@[@{@"text": scriptText, @"url": self.scriptURL.absoluteString}]);
} else {
failureCallback(RCTErrorWithMessage(@"Source code is not available"));
}
Expand Down

0 comments on commit 4a3857e

Please sign in to comment.