From 4a3857ef1dc073f4a58274b77e7f775ca81b39dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Sagnes?= Date: Fri, 16 Oct 2015 08:10:25 -0700 Subject: [PATCH] Use JSStringCreateWithUTF8CString and skip NSString decoding when loading 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 --- .../RCTContextExecutorTests.m | 4 +-- Libraries/WebSocket/RCTWebSocketExecutor.m | 2 +- React/Base/RCTBatchedBridge.m | 19 +++++++------- React/Base/RCTBridgeDelegate.h | 2 +- React/Base/RCTJavaScriptExecutor.h | 2 +- React/Base/RCTJavaScriptLoader.m | 10 +++++--- React/Executors/RCTContextExecutor.m | 10 ++++++-- React/Executors/RCTWebViewExecutor.m | 25 ++++++++++--------- React/Modules/RCTSourceCode.h | 2 +- React/Modules/RCTSourceCode.m | 6 +++-- 10 files changed, 47 insertions(+), 35 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m index fa48fcfbf..d00d65046 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m @@ -40,7 +40,7 @@ - (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); @@ -128,7 +128,7 @@ static uint64_t _get_time_nanoseconds(void) } \ "; - [_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++) { diff --git a/Libraries/WebSocket/RCTWebSocketExecutor.m b/Libraries/WebSocket/RCTWebSocketExecutor.m index 815ab56d1..017df74b1 100644 --- a/Libraries/WebSocket/RCTWebSocketExecutor.m +++ b/Libraries/WebSocket/RCTWebSocketExecutor.m @@ -148,7 +148,7 @@ RCT_EXPORT_MODULE() }); } -- (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete +- (void)executeApplicationScript:(NSData *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete { NSDictionary *message = @{ @"method": @"executeApplicationScript", diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index b0cc0f0de..e996af146 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -116,8 +116,8 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); 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]; @@ -184,7 +184,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); 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); @@ -195,12 +195,13 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); // 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]; } } @@ -355,7 +356,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); callback:onComplete]; } -- (void)executeSourceCode:(NSString *)sourceCode +- (void)executeSourceCode:(NSData *)sourceCode { if (!self.valid || !_javaScriptExecutor) { return; @@ -363,7 +364,7 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); 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) { @@ -585,7 +586,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR } } -- (void)enqueueApplicationScript:(NSString *)script +- (void)enqueueApplicationScript:(NSData *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete { diff --git a/React/Base/RCTBridgeDelegate.h b/React/Base/RCTBridgeDelegate.h index 82d58a422..a8a3c937f 100644 --- a/React/Base/RCTBridgeDelegate.h +++ b/React/Base/RCTBridgeDelegate.h @@ -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; diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h index 87d4dda78..5506ddfa3 100644 --- a/React/Base/RCTJavaScriptExecutor.h +++ b/React/Base/RCTJavaScriptExecutor.h @@ -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; diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index f11e0b334..6af63ac71 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -36,8 +36,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) 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; } @@ -71,10 +73,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) 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]] && @@ -101,7 +103,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) onComplete(error, nil); return; } - onComplete(nil, rawText); + onComplete(nil, data); }]; [task resume]; diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 05cf5c98d..05abf2e2f 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -494,7 +494,7 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) }), 0, @"js_call", (@{@"module":name, @"method": method, @"args": arguments}))]; } -- (void)executeApplicationScript:(NSString *)script +- (void)executeApplicationScript:(NSData *)script sourceURL:(NSURL *)sourceURL onComplete:(RCTJavaScriptCompleteBlock)onComplete { @@ -508,9 +508,15 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) 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); diff --git a/React/Executors/RCTWebViewExecutor.m b/React/Executors/RCTWebViewExecutor.m index 5206cabef..b3136db04 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -130,7 +130,7 @@ RCT_EXPORT_MODULE() * 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 { @@ -142,6 +142,7 @@ RCT_EXPORT_MODULE() } RCTAssert(onComplete != nil, @""); + NSString *scriptString = [[NSString alloc] initWithData:script encoding:NSUTF8StringEncoding]; __weak RCTWebViewExecutor *weakSelf = self; _onApplicationScriptLoaded = ^(NSError *error){ RCTWebViewExecutor *strongSelf = weakSelf; @@ -163,23 +164,23 @@ RCT_EXPORT_MODULE() }]; [_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:@"", - script + scriptString ]; [_webView loadHTMLString:runScript baseURL:url]; } diff --git a/React/Modules/RCTSourceCode.h b/React/Modules/RCTSourceCode.h index 4a6638873..1acf6f1e0 100644 --- a/React/Modules/RCTSourceCode.h +++ b/React/Modules/RCTSourceCode.h @@ -13,7 +13,7 @@ @interface RCTSourceCode : NSObject -@property (nonatomic, copy) NSString *scriptText; +@property (nonatomic, copy) NSData *scriptData; @property (nonatomic, copy) NSURL *scriptURL; @end diff --git a/React/Modules/RCTSourceCode.m b/React/Modules/RCTSourceCode.m index 685ae611a..ae3e22f72 100644 --- a/React/Modules/RCTSourceCode.m +++ b/React/Modules/RCTSourceCode.m @@ -27,8 +27,10 @@ RCT_EXPORT_MODULE() 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")); }