[ReactNative] Send batched calls from objc to js every frame + add bridge profiling

This commit is contained in:
Tadeu Zagallo 2015-04-17 04:02:37 -07:00
parent 70a28547dd
commit 1883ba535c
10 changed files with 277 additions and 163 deletions

View File

@ -74,7 +74,8 @@ var BatchedBridgeFactory = {
setLoggingEnabled: messageQueue.setLoggingEnabled.bind(messageQueue),
getLoggedOutgoingItems: messageQueue.getLoggedOutgoingItems.bind(messageQueue),
getLoggedIncomingItems: messageQueue.getLoggedIncomingItems.bind(messageQueue),
replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue)
replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue),
processBatch: messageQueue.processBatch.bind(messageQueue),
};
}
};

View File

@ -307,6 +307,25 @@ var MessageQueueMixin = {
);
},
processBatch: function (batch) {
var self = this;
batch.forEach(function (call) {
invariant(
call.module === 'BatchedBridge',
'All the calls should pass through the BatchedBridge module'
);
if (call.method === 'callFunctionReturnFlushedQueue') {
self.callFunction.apply(self, call.args);
} else if (call.method === 'invokeCallbackAndReturnFlushedQueue') {
self.invokeCallback.apply(self, call.args);
} else {
throw new Error(
'Unrecognized method called on BatchedBridge: ' + call.method);
}
});
return this.flushedQueue();
},
setLoggingEnabled: function(enabled) {
this._enableLogging = enabled;
this._loggedIncomingItems = [];

View File

@ -42,6 +42,45 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
RCTBridgeFieldFlushDateMillis
};
/**
* Temporarily allow to turn on and off the call batching in case someone wants
* to profile both
*/
#define BATCHED_BRIDGE 1
#ifdef DEBUG
#define RCT_PROFILE_START() \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
NSTimeInterval __rct_profile_start = CACurrentMediaTime() \
_Pragma("clang diagnostic pop")
#define RCT_PROFILE_END(cat, args, profileName...) \
do { \
if (_profile) { \
[_profileLock lock]; \
[_profile addObject:@{ \
@"name": [@[profileName] componentsJoinedByString: @"_"], \
@"cat": @ #cat, \
@"ts": @((NSUInteger)((__rct_profile_start - _startingTime) * 1e6)), \
@"dur": @((NSUInteger)((CACurrentMediaTime() - __rct_profile_start) * 1e6)), \
@"ph": @"X", \
@"pid": @([[NSProcessInfo processInfo] processIdentifier]), \
@"tid": [[NSThread currentThread] description], \
@"args": args ?: [NSNull null], \
}]; \
[_profileLock unlock]; \
} \
} while(0)
#else
#define RCT_PROFILE_START(...)
#define RCT_PROFILE_END(...)
#endif
#ifdef __LP64__
typedef uint64_t RCTHeaderValue;
typedef struct section_64 RCTHeaderSection;
@ -191,10 +230,16 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void)
@interface RCTBridge ()
@property (nonatomic, copy, readonly) NSArray *profile;
- (void)_invokeAndProcessModule:(NSString *)module
method:(NSString *)method
arguments:(NSArray *)args;
- (void)_actuallyInvokeAndProcessModule:(NSString *)module
method:(NSString *)method
arguments:(NSArray *)args;
@end
/**
@ -754,7 +799,13 @@ static NSDictionary *RCTLocalModulesConfig()
RCTBridgeModuleProviderBlock _moduleProvider;
RCTDisplayLink *_displayLink;
NSMutableSet *_frameUpdateObservers;
NSMutableArray *_scheduledCalls;
NSMutableArray *_scheduledCallbacks;
BOOL _loading;
NSUInteger _startingTime;
NSMutableArray *_profile;
NSLock *_profileLock;
}
static id<RCTJavaScriptExecutor> _latestJSExecutor;
@ -782,6 +833,8 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
_shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL);
_displayLink = [[RCTDisplayLink alloc] initWithBridge:self];
_frameUpdateObservers = [[NSMutableSet alloc] init];
_scheduledCalls = [[NSMutableArray alloc] init];
_scheduledCallbacks = [[NSMutableArray alloc] init];
// Register passed-in module instances
NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
@ -1005,20 +1058,54 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
}
}
/**
* Private hack to support `setTimeout(fn, 0)`
*/
- (void)_immediatelyCallTimer:(NSNumber *)timer
{
NSString *moduleDotMethod = @"RCTJSTimers.callTimers";
NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod];
RCTAssert(moduleID != nil, @"Module '%@' not registered.",
[[moduleDotMethod componentsSeparatedByString:@"."] firstObject]);
NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod];
RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod);
if (!_loading) {
#if BATCHED_BRIDGE
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue"
arguments:@[moduleID, methodID, @[@[timer]]]];
#else
[self _invokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue"
arguments:@[moduleID, methodID, @[@[timer]]]];
#endif
}
}
- (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");
RCT_PROFILE_START();
[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {
RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script");
if (scriptLoadError) {
onComplete(scriptLoadError);
return;
}
RCT_PROFILE_START();
[_javaScriptExecutor executeJSCall:@"BatchedBridge"
method:@"flushedQueue"
arguments:@[]
callback:^(id json, NSError *error) {
RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue");
RCT_PROFILE_START();
[self _handleBuffer:json];
RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
onComplete(error);
}];
}];
@ -1028,11 +1115,46 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
{
#if BATCHED_BRIDGE
RCT_PROFILE_START();
if ([module isEqualToString:@"RCTEventEmitter"]) {
for (NSDictionary *call in _scheduledCalls) {
if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) {
[_scheduledCalls removeObject:call];
}
}
}
id call = @{
@"module": module,
@"method": method,
@"args": args,
};
if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
[_scheduledCallbacks addObject:call];
} else {
[_scheduledCalls addObject:call];
}
RCT_PROFILE_END(js_call, args, @"schedule", module, method);
}
- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
{
#endif
[[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil];
NSString *moduleDotMethod = [NSString stringWithFormat:@"%@.%@", module, method];
RCT_PROFILE_START();
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil];
RCT_PROFILE_END(js_call, args, moduleDotMethod);
RCT_PROFILE_START();
[self _handleBuffer:json];
RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
};
[_javaScriptExecutor executeJSCall:module
@ -1151,12 +1273,34 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
- (void)_update:(CADisplayLink *)displayLink
{
RCT_PROFILE_START();
RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink];
for (id<RCTFrameUpdateObserver> observer in _frameUpdateObservers) {
if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) {
[observer didUpdateFrame:frameUpdate];
}
}
[self _runScheduledCalls];
RCT_PROFILE_END(display_link, nil, @"main_thread");
}
- (void)_runScheduledCalls
{
#if BATCHED_BRIDGE
NSArray *calls = [_scheduledCallbacks arrayByAddingObjectsFromArray:_scheduledCalls];
if (calls.count > 0) {
_scheduledCalls = [[NSMutableArray alloc] init];
_scheduledCallbacks = [[NSMutableArray alloc] init];
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
method:@"processBatch"
arguments:@[calls]];
}
#endif
}
- (void)addFrameUpdateObserver:(id<RCTFrameUpdateObserver>)observer
@ -1194,4 +1338,42 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
callback:^(id json, NSError *error) {}];
}
- (void)startProfiling
{
if (![_bundleURL.scheme isEqualToString:@"http"]) {
RCTLogError(@"To run the profiler you must be running from the dev server");
return;
}
_profileLock = [[NSLock alloc] init];
_startingTime = CACurrentMediaTime();
[_profileLock lock];
_profile = [[NSMutableArray alloc] init];
[_profileLock unlock];
}
- (void)stopProfiling
{
[_profileLock lock];
NSArray *profile = _profile;
_profile = nil;
[_profileLock unlock];
_profileLock = nil;
NSString *log = RCTJSONStringify(profile, NULL);
NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", _bundleURL.scheme, _bundleURL.host, _bundleURL.port];
NSURL *URL = [NSURL URLWithString:URLString];
NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL];
URLRequest.HTTPMethod = @"POST";
[URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSURLSessionTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest
fromData:[log dataUsingEncoding:NSUTF8StringEncoding]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
RCTLogError(@"%@", error.localizedDescription);
}
}];
[task resume];
}
@end

View File

@ -14,6 +14,15 @@
#import "RCTSourceCode.h"
#import "RCTWebViewExecutor.h"
@interface RCTBridge (RCTDevMenu)
@property (nonatomic, copy, readonly) NSArray *profile;
- (void)startProfiling;
- (void)stopProfiling;
@end
@interface RCTDevMenu () <UIActionSheetDelegate>
@end
@ -37,11 +46,12 @@
NSString *debugTitleChrome = _bridge.executorClass != Nil && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging";
NSString *debugTitleSafari = _bridge.executorClass == [RCTWebViewExecutor class] ? @"Disable Safari Debugging" : @"Enable Safari Debugging";
NSString *liveReloadTitle = _liveReload ? @"Disable Live Reload" : @"Enable Live Reload";
NSString *profilingTitle = _bridge.profile ? @"Stop Profiling" : @"Start Profiling";
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, nil];
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil];
actionSheet.actionSheetStyle = UIBarStyleBlack;
[actionSheet showInView:[[[[UIApplication sharedApplication] keyWindow] rootViewController] view]];
}
@ -61,6 +71,12 @@
} else if (buttonIndex == 3) {
_liveReload = !_liveReload;
[self _pollAndReload];
} else if (buttonIndex == 4) {
if (_bridge.profile) {
[_bridge stopProfiling];
} else {
[_bridge startProfiling];
}
}
}

View File

@ -68,7 +68,6 @@
_bridge = bridge;
_moduleName = moduleName;
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(bundleFinishedLoading)
@ -105,7 +104,6 @@
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_touchHandler invalidate];
if (_contentView) {
[_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer"
args:@[_contentView.reactTag]];
@ -148,7 +146,6 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer)
* NOTE: Since the bridge persists, the RootViews might be reused, so now
* the react tag is assigned every time we load new content.
*/
[_touchHandler invalidate];
[_contentView removeFromSuperview];
_contentView = [[UIView alloc] initWithFrame:self.bounds];
_contentView.reactTag = [_bridge.uiManager allocateRootTag];

View File

@ -9,14 +9,12 @@
#import <UIKit/UIKit.h>
#import "RCTInvalidating.h"
#import "RCTFrameUpdate.h"
@class RCTBridge;
@interface RCTTouchHandler : UIGestureRecognizer<RCTInvalidating>
@interface RCTTouchHandler : UIGestureRecognizer
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
- (void)startOrResetInteractionTiming;
- (NSDictionary *)endAndResetInteractionTiming;
@end

View File

@ -20,39 +20,6 @@
// TODO: this class behaves a lot like a module, and could be implemented as a
// module if we were to assume that modules and RootViews had a 1:1 relationship
@interface RCTTouchEvent : NSObject
@property (nonatomic, assign, readonly) NSUInteger id;
@property (nonatomic, copy, readonly) NSString *eventName;
@property (nonatomic, copy, readonly) NSArray *touches;
@property (nonatomic, copy, readonly) NSArray *changedIndexes;
@property (nonatomic, assign, readonly) CFTimeInterval originatingTime;
@end
@implementation RCTTouchEvent
+ (instancetype)touchWithEventName:(NSString *)eventName touches:(NSArray *)touches changedIndexes:(NSArray *)changedIndexes originatingTime:(CFTimeInterval)originatingTime
{
RCTTouchEvent *touchEvent = [[self alloc] init];
touchEvent->_id = [self newTaskID];
touchEvent->_eventName = [eventName copy];
touchEvent->_touches = [touches copy];
touchEvent->_changedIndexes = [changedIndexes copy];
touchEvent->_originatingTime = originatingTime;
return touchEvent;
}
+ (NSUInteger)newTaskID
{
static NSUInteger taskID = 0;
return ++taskID;
}
@end
@implementation RCTTouchHandler
{
__weak RCTBridge *_bridge;
@ -69,7 +36,6 @@
BOOL _recordingInteractionTiming;
CFTimeInterval _mostRecentEnqueueJS;
CADisplayLink *_displayLink;
NSMutableArray *_pendingTouches;
NSMutableArray *_bridgeInteractionTiming;
}
@ -86,12 +52,9 @@
_reactTouches = [[NSMutableArray alloc] init];
_touchViews = [[NSMutableArray alloc] init];
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)];
_pendingTouches = [[NSMutableArray alloc] init];
_bridgeInteractionTiming = [[NSMutableArray alloc] init];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower
// level components not build using RCT, will fail to recognize gestures.
self.cancelsTouchesInView = NO;
@ -99,17 +62,6 @@
return self;
}
- (BOOL)isValid
{
return _displayLink != nil;
}
- (void)invalidate
{
[_displayLink invalidate];
_displayLink = nil;
}
typedef NS_ENUM(NSInteger, RCTTouchEventType) {
RCTTouchEventTypeStart,
RCTTouchEventTypeMove,
@ -216,7 +168,6 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches);
- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime
{
// Update touches
CFTimeInterval enqueueTime = CACurrentMediaTime();
NSMutableArray *changedIndexes = [[NSMutableArray alloc] init];
for (UITouch *touch in touches) {
NSInteger index = [_nativeTouches indexOfObject:touch];
@ -239,71 +190,8 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches);
[reactTouches addObject:[touch copy]];
}
RCTTouchEvent *touch = [RCTTouchEvent touchWithEventName:eventName
touches:reactTouches
changedIndexes:changedIndexes
originatingTime:originatingTime];
[_pendingTouches addObject:touch];
if (_recordingInteractionTiming) {
[_bridgeInteractionTiming addObject:@{
@"timeSeconds": @(touch.originatingTime),
@"operation": @"taskOriginated",
@"taskID": @(touch.id),
}];
[_bridgeInteractionTiming addObject:@{
@"timeSeconds": @(enqueueTime),
@"operation": @"taskEnqueuedPending",
@"taskID": @(touch.id),
}];
}
}
- (void)_update:(CADisplayLink *)sender
{
// Dispatch touch event
NSUInteger pendingCount = _pendingTouches.count;
for (RCTTouchEvent *touch in _pendingTouches) {
_mostRecentEnqueueJS = CACurrentMediaTime();
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
args:@[touch.eventName, touch.touches, touch.changedIndexes]];
}
if (_recordingInteractionTiming) {
for (RCTTouchEvent *touch in _pendingTouches) {
[_bridgeInteractionTiming addObject:@{
@"timeSeconds": @(sender.timestamp),
@"operation": @"frameAlignedDispatch",
@"taskID": @(touch.id),
}];
}
if (pendingCount > 0 || sender.timestamp - _mostRecentEnqueueJS < 0.1) {
[_bridgeInteractionTiming addObject:@{
@"timeSeconds": @(sender.timestamp),
@"operation": @"mainThreadDisplayLink",
@"taskID": @([RCTTouchEvent newTaskID]),
}];
}
}
[_pendingTouches removeAllObjects];
}
- (void)startOrResetInteractionTiming
{
RCTAssertMainThread();
[_bridgeInteractionTiming removeAllObjects];
_recordingInteractionTiming = YES;
}
- (NSDictionary *)endAndResetInteractionTiming
{
RCTAssertMainThread();
_recordingInteractionTiming = NO;
NSArray *_prevInteractionTimingData = _bridgeInteractionTiming;
_bridgeInteractionTiming = [[NSMutableArray alloc] init];
return @{ @"interactionTiming": _prevInteractionTimingData };
args:@[eventName, reactTouches, changedIndexes]];
}
#pragma mark - Gesture Recognizer Delegate Callbacks

View File

@ -15,6 +15,15 @@
#import "RCTSparseArray.h"
#import "RCTUtils.h"
@interface RCTBridge (Private)
/**
* Allow super fast, one time, timers to skip the queue and be directly executed
*/
- (void)_immediatelyCallTimer:(NSNumber *)timer;
@end
@interface RCTTimer : NSObject
@property (nonatomic, strong, readonly) NSDate *target;
@ -160,7 +169,7 @@ RCT_EXPORT_METHOD(createTimer:(NSNumber *)callbackID
{
if (jsDuration == 0 && repeats == NO) {
// For super fast, one-off timers, just enqueue them immediately rather than waiting a frame.
[_bridge enqueueJSCall:@"RCTJSTimers.callTimers" args:@[@[callbackID]]];
[_bridge _immediatelyCallTimer:callbackID];
return;
}

View File

@ -677,14 +677,12 @@ RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
// TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient
// Figure out what to insert - merge temporary inserts and adds
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
for (NSInteger index = 0; index < temporarilyRemovedChildren.count; index++) {
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) {
destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index];
}
for (NSInteger index = 0; index < addAtIndices.count; index++) {
for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) {
id view = registry[addChildReactTags[index]];
if (view) {
destinationsToChildrenToAdd[addAtIndices[index]] = view;
@ -1420,41 +1418,6 @@ RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config
callback:callback];
}
RCT_EXPORT_METHOD(startOrResetInteractionTiming)
{
NSSet *rootViewTags = [_rootViewTags copy];
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
for (NSNumber *reactTag in rootViewTags) {
UIView *rootView = viewRegistry[reactTag];
for (RCTTouchHandler *handler in rootView.gestureRecognizers) {
if ([handler isKindOfClass:[RCTTouchHandler class]]) {
[handler startOrResetInteractionTiming];
break;
}
}
}
}];
}
RCT_EXPORT_METHOD(endAndResetInteractionTiming:(RCTResponseSenderBlock)onSuccess
onError:(RCTResponseSenderBlock)onError)
{
NSSet *rootViewTags = [_rootViewTags copy];
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
NSMutableDictionary *timingData = [[NSMutableDictionary alloc] init];
for (NSNumber *reactTag in rootViewTags) {
UIView *rootView = viewRegistry[reactTag];
for (RCTTouchHandler *handler in rootView.gestureRecognizers) {
if ([handler isKindOfClass:[RCTTouchHandler class]]) {
[handler endAndResetInteractionTiming];
break;
}
}
}
onSuccess(@[timingData]);
}];
}
static UIView *_jsResponder;
+ (UIView *)JSResponder

View File

@ -17,6 +17,8 @@ var Activity = require('../Activity');
var AssetServer = require('../AssetServer');
var Promise = require('bluebird');
var _ = require('underscore');
var exec = require('child_process').exec;
var fs = require('fs');
module.exports = Server;
@ -252,6 +254,42 @@ Server.prototype._processAssetsRequest = function(req, res) {
).done();
};
Server.prototype._processProfile = function(req, res) {
console.log('Dumping profile information...');
var dumpName = '/tmp/dump_' + Date.now() + '.json';
var prefix = process.env.TRACE_VIEWER_PATH || '';
var cmd = path.join(prefix, 'trace2html') + ' ' + dumpName;
fs.writeFileSync(dumpName, req.rawBody);
exec(cmd, function (error) {
if (error) {
if (error.code === 127) {
console.error(
'\n** Failed executing `' + cmd + '` **\n\n' +
'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' +
'You can get it at:\n\n' +
' https://github.com/google/trace-viewer\n\n' +
'If it\'s not in your path, you can set a custom path with:\n\n' +
' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' +
'NOTE: Your profile data was kept at:\n\n' +
' ' + dumpName
);
} else {
console.error('Unknown error', error);
}
res.end();
return;
} else {
exec('rm ' + dumpName);
exec('open ' + dumpName.replace(/json$/, 'html'), function (error) {
if (error) {
console.error(error);
}
res.end();
});
}
});
};
Server.prototype.processRequest = function(req, res, next) {
var urlObj = url.parse(req.url, true);
var pathname = urlObj.pathname;
@ -270,6 +308,9 @@ Server.prototype.processRequest = function(req, res, next) {
} else if (pathname.match(/^\/assets\//)) {
this._processAssetsRequest(req, res);
return;
} else if (pathname.match(/^\/profile\/?$/)) {
this._processProfile(req, res);
return;
} else {
next();
return;