Enqueue events at 60fps + profiling helpers

This commit is contained in:
Tadeu Zagallo 2015-02-27 04:05:35 -08:00
parent ddf8933904
commit e53558d94a
5 changed files with 188 additions and 21 deletions

View File

@ -38,4 +38,7 @@
- (void)reload;
+ (void)reloadAll;
- (void)startOrResetInteractionTiming;
- (NSDictionary *)endAndResetInteractionTiming;
@end

View File

@ -131,6 +131,7 @@ static Class _globalExecutorClass;
// Clean up
[self removeGestureRecognizer:_touchHandler];
[_touchHandler invalidate];
[_executor invalidate];
[_bridge invalidate];
@ -231,4 +232,14 @@ static Class _globalExecutorClass;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTRootViewReloadNotification object:nil];
}
- (void)startOrResetInteractionTiming
{
[_touchHandler startOrResetInteractionTiming];
}
- (NSDictionary *)endAndResetInteractionTiming
{
return [_touchHandler endAndResetInteractionTiming];
}
@end

View File

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

View File

@ -14,10 +14,42 @@
// 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 newID];
touchEvent->_eventName = [eventName copy];
touchEvent->_touches = [touches copy];
touchEvent->_changedIndexes = [changedIndexes copy];
touchEvent->_originatingTime = originatingTime;
return touchEvent;
}
+ (NSUInteger)newID
{
static NSUInteger id = 0;
return ++id;
}
@end
@implementation RCTTouchHandler
{
__weak RCTBridge *_bridge;
/**
* Arrays managed in parallel tracking native touch object along with the
* native view that was touched, and the react touch data dictionary.
@ -27,6 +59,12 @@
NSMutableOrderedSet *_nativeTouches;
NSMutableArray *_reactTouches;
NSMutableArray *_touchViews;
BOOL _recordingInteractionTiming;
CFTimeInterval _mostRecentEnqueueJS;
CADisplayLink *_displayLink;
NSMutableArray *_pendingTouches;
NSMutableArray *_bridgeInteractionTiming;
}
- (instancetype)initWithTarget:(id)target action:(SEL)action
@ -37,15 +75,21 @@
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super initWithTarget:nil action:NULL])) {
RCTAssert(bridge != nil, @"Expect an event dispatcher");
_bridge = bridge;
_nativeTouches = [[NSMutableOrderedSet alloc] init];
_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;
@ -53,6 +97,17 @@
return self;
}
- (BOOL)isValid
{
return _displayLink != nil;
}
- (void)invalidate
{
[_displayLink invalidate];
_displayLink = nil;
}
typedef NS_ENUM(NSInteger, RCTTouchEventType) {
RCTTouchEventTypeStart,
RCTTouchEventTypeMove,
@ -65,10 +120,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
- (void)_recordNewTouches:(NSSet *)touches
{
for (UITouch *touch in touches) {
RCTAssert(![_nativeTouches containsObject:touch],
@"Touch is already recorded. This is a critical bug.");
// Find closest React-managed touchable view
UIView *targetView = touch.view;
while (targetView) {
@ -95,14 +150,14 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
break;
}
}
// Create touch
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9];
reactTouch[@"target"] = [targetView reactTagAtPoint:[touch locationInView:targetView]];
reactTouch[@"identifier"] = @(touchID);
reactTouch[@"touches"] = [NSNull null]; // We hijack this touchObj to serve both as an event
reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly.
// Add to arrays
[_touchViews addObject:targetView];
[_nativeTouches addObject:touch];
@ -129,10 +184,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
UITouch *nativeTouch = _nativeTouches[touchIndex];
CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window];
CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:self.view];
UIView *touchView = _touchViews[touchIndex];
CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView];
NSMutableDictionary *reactTouch = _reactTouches[touchIndex];
reactTouch[@"pageX"] = @(rootViewLocation.x);
reactTouch[@"pageY"] = @(rootViewLocation.y);
@ -157,9 +212,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
* (start/end/move/cancel) and the indices that represent "changed" `Touch`es
* from that array.
*/
- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName
- (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];
@ -181,10 +237,72 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
for (NSDictionary *touch in _reactTouches) {
[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
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
args:@[eventName, reactTouches, changedIndexes]];
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 newID]),
}];
}
}
[_pendingTouches removeAllObjects];
}
- (void)startOrResetInteractionTiming
{
RCTAssertMainThread();
[_bridgeInteractionTiming removeAllObjects];
_recordingInteractionTiming = YES;
}
- (NSDictionary *)endAndResetInteractionTiming
{
RCTAssertMainThread();
_recordingInteractionTiming = NO;
NSArray *_prevInteractionTimingData = _bridgeInteractionTiming;
_bridgeInteractionTiming = [[NSMutableArray alloc] init];
return @{ @"interactionTiming": _prevInteractionTimingData };
}
#pragma mark - Gesture Recognizer Delegate Callbacks
@ -193,11 +311,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
{
[super touchesBegan:touches withEvent:event];
self.state = UIGestureRecognizerStateBegan;
// "start" has to record new touches before extracting the event.
// "end"/"cancel" needs to remove the touch *after* extracting the event.
[self _recordNewTouches:touches];
[self _updateAndDispatchTouches:touches eventName:@"topTouchStart"];
[self _updateAndDispatchTouches:touches eventName:@"topTouchStart" originatingTime:event.timestamp];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
@ -206,20 +324,20 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
if (self.state == UIGestureRecognizerStateFailed) {
return;
}
[self _updateAndDispatchTouches:touches eventName:@"topTouchMove"];
[self _updateAndDispatchTouches:touches eventName:@"topTouchMove" originatingTime:event.timestamp];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
[self _updateAndDispatchTouches:touches eventName:@"topTouchEnd"];
[self _updateAndDispatchTouches:touches eventName:@"topTouchEnd" originatingTime:event.timestamp];
[self _recordRemovedTouches:touches];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
[self _updateAndDispatchTouches:touches eventName:@"topTouchCancel"];
[self _updateAndDispatchTouches:touches eventName:@"topTouchCancel" originatingTime:event.timestamp];
[self _recordRemovedTouches:touches];
}

View File

@ -1342,6 +1342,37 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
_nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config callback:callback];
}
- (void)startOrResetInteractionTiming
{
RCT_EXPORT();
NSSet *rootViewTags = [_rootViewTags copy];
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
for (NSNumber *reactTag in rootViewTags) {
RCTRootView *rootView = viewRegistry[reactTag];
[rootView startOrResetInteractionTiming];
}
}];
}
- (void)endAndResetInteractionTiming:(RCTResponseSenderBlock)onSuccess
onError:(RCTResponseSenderBlock)onError
{
RCT_EXPORT();
NSSet *rootViewTags = [_rootViewTags copy];
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
NSMutableDictionary *timingData = [[NSMutableDictionary alloc] init];
for (NSNumber *reactTag in rootViewTags) {
RCTRootView *rootView = viewRegistry[reactTag];
if (rootView) {
timingData[reactTag.stringValue] = [rootView endAndResetInteractionTiming];
}
}
onSuccess(@[ timingData ]);
}];
}
static UIView *_jsResponder;
+ (UIView *)JSResponder