Skip to content

Commit

Permalink
[ReactNative] Send batched calls from objc to js every frame + add br…
Browse files Browse the repository at this point in the history
…idge profiling
  • Loading branch information
tadeuzagallo committed Apr 17, 2015
1 parent 70a2854 commit 1883ba5
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}
};
Expand Down
19 changes: 19 additions & 0 deletions Libraries/Utilities/MessageQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
182 changes: 182 additions & 0 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -191,10 +230,16 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {

@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

/**
Expand Down Expand Up @@ -754,7 +799,13 @@ @implementation RCTBridge
RCTBridgeModuleProviderBlock _moduleProvider;
RCTDisplayLink *_displayLink;
NSMutableSet *_frameUpdateObservers;
NSMutableArray *_scheduledCalls;
NSMutableArray *_scheduledCallbacks;
BOOL _loading;

NSUInteger _startingTime;
NSMutableArray *_profile;
NSLock *_profileLock;
}

static id<RCTJavaScriptExecutor> _latestJSExecutor;
Expand Down Expand Up @@ -782,6 +833,8 @@ - (void)setUp
_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];
Expand Down Expand Up @@ -1005,20 +1058,54 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
}
}

/**
* 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);
}];
}];
Expand All @@ -1028,11 +1115,46 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:

- (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
Expand Down Expand Up @@ -1151,12 +1273,34 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i

- (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
Expand Down Expand Up @@ -1194,4 +1338,42 @@ + (void)logMessage:(NSString *)message level:(NSString *)level
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
18 changes: 17 additions & 1 deletion React/Base/RCTDevMenu.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,11 +46,12 @@ - (void)show
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]];
}
Expand All @@ -61,6 +71,12 @@ - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger
} else if (buttonIndex == 3) {
_liveReload = !_liveReload;
[self _pollAndReload];
} else if (buttonIndex == 4) {
if (_bridge.profile) {
[_bridge stopProfiling];
} else {
[_bridge startProfiling];
}
}
}

Expand Down
3 changes: 0 additions & 3 deletions React/Base/RCTRootView.m
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge

_bridge = bridge;
_moduleName = moduleName;
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(bundleFinishedLoading)
Expand Down Expand Up @@ -105,7 +104,6 @@ - (void)invalidate
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_touchHandler invalidate];
if (_contentView) {
[_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer"
args:@[_contentView.reactTag]];
Expand Down Expand Up @@ -148,7 +146,6 @@ - (void)bundleFinishedLoading
* 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];
Expand Down
6 changes: 2 additions & 4 deletions React/Base/RCTTouchHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 1883ba5

Please sign in to comment.