diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m index cc1a9def8..6c43e2dd4 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m @@ -17,6 +17,7 @@ #import #import "RCTEventDispatcher.h" +#import "RCTBridge+Private.h" @interface RCTTestEvent : NSObject @property (atomic, assign, readwrite) BOOL canCoalesce; @@ -82,7 +83,8 @@ { [super setUp]; - _bridge = [OCMockObject mockForClass:[RCTBridge class]]; + _bridge = [OCMockObject mockForClass:[RCTBatchedBridge class]]; + _eventDispatcher = [RCTEventDispatcher new]; [_eventDispatcher setValue:_bridge forKey:@"bridge"]; @@ -106,61 +108,79 @@ [_bridge verify]; } -- (void)testNonCoalescingEventsAreImmediatelyDispatched +- (void)testNonCoalescingEventIsImmediatelyDispatched { _testEvent.canCoalesce = NO; - [[_bridge expect] enqueueJSCall:_JSMethod - args:[_testEvent arguments]]; + + [[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread]; [_eventDispatcher sendEvent:_testEvent]; [_bridge verify]; } -- (void)testCoalescedEventShouldBeDispatchedOnFrameUpdate +- (void)testCoalescingEventIsImmediatelyDispatched { + _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]]; - - [(id)_eventDispatcher didUpdateFrame:nil]; - + eventsEmittingBlock(); [_bridge verify]; -} -- (void)testNonCoalescingEventForcesColescedEventsToBeImmediatelyDispatched -{ - RCTTestEvent *nonCoalescingEvent = [[RCTTestEvent alloc] initWithViewTag:nil - eventName:_eventName - body:@{} - coalescingKey:0]; - nonCoalescingEvent.canCoalesce = NO; + + [[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread]; [_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]; - - [[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit" - args:[_testEvent arguments]]; - - [(id)_eventDispatcher didUpdateFrame:nil]; + eventsEmittingBlock(); [_bridge verify]; } @@ -169,20 +189,60 @@ { NSString *firstEventName = RCTNormalizeInputEventName(@"firstEvent"); RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:nil - eventName:firstEventName - body:_body + eventName:firstEventName + body:_body coalescingKey:0]; - [_eventDispatcher sendEvent:firstEvent]; - [_eventDispatcher sendEvent:_testEvent]; - + __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]]; - [(id)_eventDispatcher didUpdateFrame:nil]; + + [_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]]; + + + [_eventDispatcher sendEvent:firstEvent]; + [_eventDispatcher sendEvent:secondEvent]; + [_eventDispatcher sendEvent:firstEvent]; + [_eventDispatcher sendEvent:secondEvent]; + [_eventDispatcher sendEvent:secondEvent]; + [_eventDispatcher sendEvent:firstEvent]; + [_eventDispatcher sendEvent:firstEvent]; + + eventsEmittingBlock(); [_bridge verify]; } diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 4957efcc8..02fd28356 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -97,13 +97,8 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); /** * Send a pre-prepared event object. * - * 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. + * 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. */ - (void)sendEvent:(id)event; diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 9796b8131..e31093581 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -11,7 +11,10 @@ #import "RCTAssert.h" #import "RCTBridge.h" +#import "RCTBridge+Private.h" #import "RCTUtils.h" +#import "RCTProfile.h" + const NSInteger RCTTextUpdateLagWarningThreshold = 3; @@ -35,38 +38,24 @@ static NSNumber *RCTGetEventID(id event) ); } -@interface RCTEventDispatcher() - -@end - @implementation RCTEventDispatcher { - NSMutableDictionary *_eventQueue; + // 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; } @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]; -} - -- (void)setPaused:(BOOL)paused -{ - if (_paused != paused) { - _paused = paused; - if (_pauseCallback) { - _pauseCallback(); - } - } + _eventsDispatchScheduled = NO; } - (void)sendAppEventWithName:(NSString *)name body:(id)body @@ -139,23 +128,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]; + 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; - self.paused = NO; + + if (!_eventsDispatchScheduled) { + _eventsDispatchScheduled = YES; + [_bridge dispatchBlock:^{ + [self flushEventsQueue]; + } queue:RCTJSThread]; + } [_eventQueueLock unlock]; } @@ -170,17 +159,13 @@ RCT_EXPORT_MODULE() return RCTJSThread; } -- (void)didUpdateFrame:(__unused RCTFrameUpdate *)update -{ - [self flushEventsQueue]; -} - +// js thread only - (void)flushEventsQueue { [_eventQueueLock lock]; NSDictionary *eventQueue = _eventQueue; _eventQueue = [NSMutableDictionary new]; - self.paused = YES; + _eventsDispatchScheduled = NO; [_eventQueueLock unlock]; for (id event in eventQueue.allValues) {