From 144dc3066144a48fc13bb7832abc9e645024fb88 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Fri, 1 Apr 2016 14:25:51 -0700 Subject: [PATCH] Revert [RN][iOS] better event emitting Reviewed By: sahrens Differential Revision: D3128586 fb-gh-sync-id: 5c3c79fa983f6c1c43319a6c14049f99e0dfee8a fbshipit-source-id: 5c3c79fa983f6c1c43319a6c14049f99e0dfee8a --- .../RCTEventDispatcherTests.m | 128 +++++------------- React/Base/RCTEventDispatcher.h | 9 +- React/Base/RCTEventDispatcher.m | 51 ++++--- 3 files changed, 74 insertions(+), 114 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m index 6c43e2dd4..cc1a9def8 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m @@ -17,7 +17,6 @@ #import #import "RCTEventDispatcher.h" -#import "RCTBridge+Private.h" @interface RCTTestEvent : NSObject @property (atomic, assign, readwrite) BOOL canCoalesce; @@ -83,8 +82,7 @@ { [super setUp]; - _bridge = [OCMockObject mockForClass:[RCTBatchedBridge class]]; - + _bridge = [OCMockObject mockForClass:[RCTBridge class]]; _eventDispatcher = [RCTEventDispatcher new]; [_eventDispatcher setValue:_bridge forKey:@"bridge"]; @@ -108,79 +106,61 @@ [_bridge verify]; } -- (void)testNonCoalescingEventIsImmediatelyDispatched +- (void)testNonCoalescingEventsAreImmediatelyDispatched { _testEvent.canCoalesce = NO; - - [[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread]; + [[_bridge expect] enqueueJSCall:_JSMethod + args:[_testEvent arguments]]; [_eventDispatcher sendEvent:_testEvent]; [_bridge verify]; } -- (void)testCoalescingEventIsImmediatelyDispatched +- (void)testCoalescedEventShouldBeDispatchedOnFrameUpdate { - _testEvent.canCoalesce = YES; - - [[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread]; - - [_eventDispatcher sendEvent:_testEvent]; - - [_bridge verify]; -} - -- (void)testMultipleEventsResultInOnlyOneDispatchAfterTheFirstOne -{ - [[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread]; - [_eventDispatcher sendEvent:_testEvent]; - [_eventDispatcher sendEvent:_testEvent]; - [_eventDispatcher sendEvent:_testEvent]; - [_eventDispatcher sendEvent:_testEvent]; [_eventDispatcher sendEvent:_testEvent]; [_bridge verify]; -} - -- (void)testRunningTheDispatchedBlockResultInANewOneBeingEnqueued -{ - __block dispatch_block_t eventsEmittingBlock; - [[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) { - eventsEmittingBlock = block; - return YES; - }] queue:RCTJSThread]; - [_eventDispatcher sendEvent:_testEvent]; - [_bridge verify]; - - - // eventsEmittingBlock would be called when js is no longer busy, which will result in emitting events [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:[_testEvent arguments]]; - eventsEmittingBlock(); + + [(id)_eventDispatcher didUpdateFrame:nil]; + [_bridge verify]; +} - - [[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread]; +- (void)testNonCoalescingEventForcesColescedEventsToBeImmediatelyDispatched +{ + RCTTestEvent *nonCoalescingEvent = [[RCTTestEvent alloc] initWithViewTag:nil + eventName:_eventName + body:@{} + coalescingKey:0]; + nonCoalescingEvent.canCoalesce = NO; [_eventDispatcher sendEvent:_testEvent]; + + [[_bridge expect] enqueueJSCall:[[_testEvent class] moduleDotMethod] + args:[_testEvent arguments]]; + [[_bridge expect] enqueueJSCall:[[nonCoalescingEvent class] moduleDotMethod] + args:[nonCoalescingEvent arguments]]; + + [_eventDispatcher sendEvent:nonCoalescingEvent]; [_bridge verify]; } - (void)testBasicCoalescingReturnsLastEvent { - __block dispatch_block_t eventsEmittingBlock; - [[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) { - eventsEmittingBlock = block; - return YES; - }] queue:RCTJSThread]; - [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" - args:[_testEvent arguments]]; - RCTTestEvent *ignoredEvent = [[RCTTestEvent alloc] initWithViewTag:nil eventName:_eventName body:@{ @"other": @"body" } coalescingKey:0]; + [_eventDispatcher sendEvent:ignoredEvent]; [_eventDispatcher sendEvent:_testEvent]; - eventsEmittingBlock(); + + [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" + args:[_testEvent arguments]]; + + [(id)_eventDispatcher didUpdateFrame:nil]; [_bridge verify]; } @@ -189,60 +169,20 @@ { NSString *firstEventName = RCTNormalizeInputEventName(@"firstEvent"); RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:nil - eventName:firstEventName - body:_body + eventName:firstEventName + body:_body coalescingKey:0]; - __block dispatch_block_t eventsEmittingBlock; - [[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) { - eventsEmittingBlock = block; - return YES; - }] queue:RCTJSThread]; - [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" - args:[firstEvent arguments]]; - [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" - args:[_testEvent arguments]]; - - [_eventDispatcher sendEvent:firstEvent]; [_eventDispatcher sendEvent:_testEvent]; - eventsEmittingBlock(); - [_bridge verify]; -} - -- (void)testSameEventTypesWithDifferentCoalesceKeysDontCoalesce -{ - NSString *eventName = RCTNormalizeInputEventName(@"firstEvent"); - RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:nil - eventName:eventName - body:_body - coalescingKey:0]; - RCTTestEvent *secondEvent = [[RCTTestEvent alloc] initWithViewTag:nil - eventName:eventName - body:_body - coalescingKey:1]; - - __block dispatch_block_t eventsEmittingBlock; - [[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) { - eventsEmittingBlock = block; - return YES; - }] queue:RCTJSThread]; [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:[firstEvent arguments]]; + [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" - args:[secondEvent arguments]]; + args:[_testEvent arguments]]; - - [_eventDispatcher sendEvent:firstEvent]; - [_eventDispatcher sendEvent:secondEvent]; - [_eventDispatcher sendEvent:firstEvent]; - [_eventDispatcher sendEvent:secondEvent]; - [_eventDispatcher sendEvent:secondEvent]; - [_eventDispatcher sendEvent:firstEvent]; - [_eventDispatcher sendEvent:firstEvent]; - - eventsEmittingBlock(); + [(id)_eventDispatcher didUpdateFrame:nil]; [_bridge verify]; } diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 02fd28356..4957efcc8 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -97,8 +97,13 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); /** * Send a pre-prepared event object. * - * Events are sent to JS as soon as the thread is free to process them. - * If an event can be coalesced and there is another compatible event waiting, the coalescing will happen immediately. + * If the event can be coalesced it is added to a pool of events that are sent at the beginning of the next js frame. + * Otherwise if the event cannot be coalesced we first flush the pool of coalesced events and the new event after that. + * + * Why it works this way? + * Making sure js gets events in the right order is crucial for correctly interpreting gestures. + * Unfortunately we cannot emit all events as they come. If we would do that we would have to emit scroll and touch moved event on every frame, + * which is too much data to transfer and process on older devices. This is especially bad when js starts lagging behind main thread. */ - (void)sendEvent:(id)event; diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index e31093581..9796b8131 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -11,10 +11,7 @@ #import "RCTAssert.h" #import "RCTBridge.h" -#import "RCTBridge+Private.h" #import "RCTUtils.h" -#import "RCTProfile.h" - const NSInteger RCTTextUpdateLagWarningThreshold = 3; @@ -38,24 +35,38 @@ static NSNumber *RCTGetEventID(id event) ); } +@interface RCTEventDispatcher() + +@end + @implementation RCTEventDispatcher { - // We need this lock to protect access to _eventQueue and __eventsDispatchScheduled. It's filled in on main thread and consumed on js thread. - NSLock *_eventQueueLock; NSMutableDictionary *_eventQueue; - BOOL _eventsDispatchScheduled; + NSLock *_eventQueueLock; } @synthesize bridge = _bridge; +@synthesize paused = _paused; +@synthesize pauseCallback = _pauseCallback; RCT_EXPORT_MODULE() - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; + _paused = YES; _eventQueue = [NSMutableDictionary new]; _eventQueueLock = [NSLock new]; - _eventsDispatchScheduled = NO; +} + +- (void)setPaused:(BOOL)paused +{ + if (_paused != paused) { + _paused = paused; + if (_pauseCallback) { + _pauseCallback(); + } + } } - (void)sendAppEventWithName:(NSString *)name body:(id)body @@ -128,23 +139,23 @@ RCT_EXPORT_MODULE() - (void)sendEvent:(id)event { + if (!event.canCoalesce) { + [self flushEventsQueue]; + [self dispatchEvent:event]; + return; + } + [_eventQueueLock lock]; NSNumber *eventID = RCTGetEventID(event); - id previousEvent = _eventQueue[eventID]; + if (previousEvent) { - RCTAssert([event canCoalesce], @"Got event %@ which cannot be coalesced, but has the same eventID %@ as the previous event %@", event, eventID, previousEvent); event = [previousEvent coalesceWithEvent:event]; } - _eventQueue[eventID] = event; - if (!_eventsDispatchScheduled) { - _eventsDispatchScheduled = YES; - [_bridge dispatchBlock:^{ - [self flushEventsQueue]; - } queue:RCTJSThread]; - } + _eventQueue[eventID] = event; + self.paused = NO; [_eventQueueLock unlock]; } @@ -159,13 +170,17 @@ RCT_EXPORT_MODULE() return RCTJSThread; } -// js thread only +- (void)didUpdateFrame:(__unused RCTFrameUpdate *)update +{ + [self flushEventsQueue]; +} + - (void)flushEventsQueue { [_eventQueueLock lock]; NSDictionary *eventQueue = _eventQueue; _eventQueue = [NSMutableDictionary new]; - _eventsDispatchScheduled = NO; + self.paused = YES; [_eventQueueLock unlock]; for (id event in eventQueue.allValues) {