Skip to content

Commit 1d3db4c

Browse files
majakFacebook Github Bot 8
authored andcommitted
better event emitting
Summary:Previously, (mostly touch and scroll) event handling on iOS worked in a hybrid way: * All incoming coalesce-able events would be pooled and retrieved by js thread in the beginning of its frame (all of this happens on js thread) * Any non-coalesce-able event would be immediately dispatched on a js thread (triggered from main thread), and if there would be pooled coalesce-able events they would be immediately dispatched at first too. This behavior has a subtle race condition, where two events are produced (on MT) in one order and received in js in different order. See facebook#5246 (comment) for further explanation of this case. The new event handling is (afaik) what Android already does. When an event comes we add it into a pool of events and dispatch a block on js thread to inform js there are events to be processed. We keep track of whether we did so, so there is at most one of these blocks waiting to be processed. When the block is executed js will process all events that are in pool at that time (NOT at time of enqueuing the block). This creates a single way of processing events and makes it impossible to process them in different order in js. The tricky part was making sure we don't coalesce events across gestures/different scrolls. Before this was achieved by knowing that gestures and scrolls start/end with non-coalesce-able event, so the pool never contained events that shouldn't be coalesced together. That "assumption" doesn't hold now. I've re-added `coalescingKey` and made touch and scroll events use it to prevent coalescing events of the same type that should remain separate in previous diffs (see dependencies). On top of it it decreases latency in events processing in case where we get only coalesce-able events. Previously these would be processed at begging of the next js frame, even when js would be free and could process them sooner. This delay is done, since they would get processed as soon as the enqueued block would run. To illustrate this improvement let's look at these two systraces. Before: https://cloud.githubusercontent.com/assets/713625/14021417/47b35b7a-f1d3-11e5-93dd-4363edfa1923.png After: https://cloud.githubusercontent.com/assets/713625/14021415/4798582a-f1d3-11e5-8715-425596e0781c.png Reviewed By: javache Differential Revision: D3092867 fb-gh-sync-id: 29071780f00fcddb0b1886a46caabdb3da1d5d84 fbshipit-source-id: 29071780f00fcddb0b1886a46caabdb3da1d5d84
1 parent b1b53aa commit 1d3db4c

File tree

3 files changed

+110
-70
lines changed

3 files changed

+110
-70
lines changed

Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#import <OCMock/OCMock.h>
1919
#import "RCTEventDispatcher.h"
20+
#import "RCTBridge+Private.h"
2021

2122
@interface RCTTestEvent : NSObject <RCTEvent>
2223
@property (atomic, assign, readwrite) BOOL canCoalesce;
@@ -82,7 +83,8 @@ - (void)setUp
8283
{
8384
[super setUp];
8485

85-
_bridge = [OCMockObject mockForClass:[RCTBridge class]];
86+
_bridge = [OCMockObject mockForClass:[RCTBatchedBridge class]];
87+
8688
_eventDispatcher = [RCTEventDispatcher new];
8789
[_eventDispatcher setValue:_bridge forKey:@"bridge"];
8890

@@ -106,61 +108,79 @@ - (void)testLegacyEventsAreImmediatelyDispatched
106108
[_bridge verify];
107109
}
108110

109-
- (void)testNonCoalescingEventsAreImmediatelyDispatched
111+
- (void)testNonCoalescingEventIsImmediatelyDispatched
110112
{
111113
_testEvent.canCoalesce = NO;
112-
[[_bridge expect] enqueueJSCall:_JSMethod
113-
args:[_testEvent arguments]];
114+
115+
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
114116

115117
[_eventDispatcher sendEvent:_testEvent];
116118

117119
[_bridge verify];
118120
}
119121

120-
- (void)testCoalescedEventShouldBeDispatchedOnFrameUpdate
122+
- (void)testCoalescingEventIsImmediatelyDispatched
121123
{
124+
_testEvent.canCoalesce = YES;
125+
126+
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
127+
122128
[_eventDispatcher sendEvent:_testEvent];
123-
[_bridge verify];
124-
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
125-
args:[_testEvent arguments]];
126129

127-
[(id<RCTFrameUpdateObserver>)_eventDispatcher didUpdateFrame:nil];
130+
[_bridge verify];
131+
}
128132

133+
- (void)testMultipleEventsResultInOnlyOneDispatchAfterTheFirstOne
134+
{
135+
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
136+
[_eventDispatcher sendEvent:_testEvent];
137+
[_eventDispatcher sendEvent:_testEvent];
138+
[_eventDispatcher sendEvent:_testEvent];
139+
[_eventDispatcher sendEvent:_testEvent];
140+
[_eventDispatcher sendEvent:_testEvent];
129141
[_bridge verify];
130142
}
131143

132-
- (void)testNonCoalescingEventForcesColescedEventsToBeImmediatelyDispatched
144+
- (void)testRunningTheDispatchedBlockResultInANewOneBeingEnqueued
133145
{
134-
RCTTestEvent *nonCoalescingEvent = [[RCTTestEvent alloc] initWithViewTag:nil
135-
eventName:_eventName
136-
body:@{}
137-
coalescingKey:0];
138-
nonCoalescingEvent.canCoalesce = NO;
146+
__block dispatch_block_t eventsEmittingBlock;
147+
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
148+
eventsEmittingBlock = block;
149+
return YES;
150+
}] queue:RCTJSThread];
139151
[_eventDispatcher sendEvent:_testEvent];
152+
[_bridge verify];
153+
140154

141-
[[_bridge expect] enqueueJSCall:[[_testEvent class] moduleDotMethod]
155+
// eventsEmittingBlock would be called when js is no longer busy, which will result in emitting events
156+
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
142157
args:[_testEvent arguments]];
143-
[[_bridge expect] enqueueJSCall:[[nonCoalescingEvent class] moduleDotMethod]
144-
args:[nonCoalescingEvent arguments]];
158+
eventsEmittingBlock();
159+
[_bridge verify];
160+
145161

146-
[_eventDispatcher sendEvent:nonCoalescingEvent];
162+
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
163+
[_eventDispatcher sendEvent:_testEvent];
147164
[_bridge verify];
148165
}
149166

150167
- (void)testBasicCoalescingReturnsLastEvent
151168
{
169+
__block dispatch_block_t eventsEmittingBlock;
170+
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
171+
eventsEmittingBlock = block;
172+
return YES;
173+
}] queue:RCTJSThread];
174+
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
175+
args:[_testEvent arguments]];
176+
152177
RCTTestEvent *ignoredEvent = [[RCTTestEvent alloc] initWithViewTag:nil
153178
eventName:_eventName
154179
body:@{ @"other": @"body" }
155180
coalescingKey:0];
156-
157181
[_eventDispatcher sendEvent:ignoredEvent];
158182
[_eventDispatcher sendEvent:_testEvent];
159-
160-
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
161-
args:[_testEvent arguments]];
162-
163-
[(id<RCTFrameUpdateObserver>)_eventDispatcher didUpdateFrame:nil];
183+
eventsEmittingBlock();
164184

165185
[_bridge verify];
166186
}
@@ -169,20 +189,60 @@ - (void)testDifferentEventTypesDontCoalesce
169189
{
170190
NSString *firstEventName = RCTNormalizeInputEventName(@"firstEvent");
171191
RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:nil
172-
eventName:firstEventName
173-
body:_body
192+
eventName:firstEventName
193+
body:_body
174194
coalescingKey:0];
175195

196+
__block dispatch_block_t eventsEmittingBlock;
197+
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
198+
eventsEmittingBlock = block;
199+
return YES;
200+
}] queue:RCTJSThread];
201+
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
202+
args:[firstEvent arguments]];
203+
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
204+
args:[_testEvent arguments]];
205+
206+
176207
[_eventDispatcher sendEvent:firstEvent];
177208
[_eventDispatcher sendEvent:_testEvent];
209+
eventsEmittingBlock();
178210

211+
[_bridge verify];
212+
}
213+
214+
- (void)testSameEventTypesWithDifferentCoalesceKeysDontCoalesce
215+
{
216+
NSString *eventName = RCTNormalizeInputEventName(@"firstEvent");
217+
RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:nil
218+
eventName:eventName
219+
body:_body
220+
coalescingKey:0];
221+
RCTTestEvent *secondEvent = [[RCTTestEvent alloc] initWithViewTag:nil
222+
eventName:eventName
223+
body:_body
224+
coalescingKey:1];
225+
226+
__block dispatch_block_t eventsEmittingBlock;
227+
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
228+
eventsEmittingBlock = block;
229+
return YES;
230+
}] queue:RCTJSThread];
179231
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
180232
args:[firstEvent arguments]];
181-
182233
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
183-
args:[_testEvent arguments]];
234+
args:[secondEvent arguments]];
235+
236+
237+
[_eventDispatcher sendEvent:firstEvent];
238+
[_eventDispatcher sendEvent:secondEvent];
239+
[_eventDispatcher sendEvent:firstEvent];
240+
[_eventDispatcher sendEvent:secondEvent];
241+
[_eventDispatcher sendEvent:secondEvent];
242+
[_eventDispatcher sendEvent:firstEvent];
243+
[_eventDispatcher sendEvent:firstEvent];
184244

185-
[(id<RCTFrameUpdateObserver>)_eventDispatcher didUpdateFrame:nil];
245+
eventsEmittingBlock();
186246

187247
[_bridge verify];
188248
}

React/Base/RCTEventDispatcher.h

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,8 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName);
9797
/**
9898
* Send a pre-prepared event object.
9999
*
100-
* 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.
101-
* Otherwise if the event cannot be coalesced we first flush the pool of coalesced events and the new event after that.
102-
*
103-
* Why it works this way?
104-
* Making sure js gets events in the right order is crucial for correctly interpreting gestures.
105-
* 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,
106-
* which is too much data to transfer and process on older devices. This is especially bad when js starts lagging behind main thread.
100+
* Events are sent to JS as soon as the thread is free to process them.
101+
* If an event can be coalesced and there is another compatible event waiting, the coalescing will happen immediately.
107102
*/
108103
- (void)sendEvent:(id<RCTEvent>)event;
109104

React/Base/RCTEventDispatcher.m

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
#import "RCTAssert.h"
1313
#import "RCTBridge.h"
14+
#import "RCTBridge+Private.h"
1415
#import "RCTUtils.h"
16+
#import "RCTProfile.h"
17+
1518

1619
const NSInteger RCTTextUpdateLagWarningThreshold = 3;
1720

@@ -35,38 +38,24 @@
3538
);
3639
}
3740

38-
@interface RCTEventDispatcher() <RCTFrameUpdateObserver>
39-
40-
@end
41-
4241
@implementation RCTEventDispatcher
4342
{
44-
NSMutableDictionary *_eventQueue;
43+
// We need this lock to protect access to _eventQueue and __eventsDispatchScheduled. It's filled in on main thread and consumed on js thread.
4544
NSLock *_eventQueueLock;
45+
NSMutableDictionary *_eventQueue;
46+
BOOL _eventsDispatchScheduled;
4647
}
4748

4849
@synthesize bridge = _bridge;
49-
@synthesize paused = _paused;
50-
@synthesize pauseCallback = _pauseCallback;
5150

5251
RCT_EXPORT_MODULE()
5352

5453
- (void)setBridge:(RCTBridge *)bridge
5554
{
5655
_bridge = bridge;
57-
_paused = YES;
5856
_eventQueue = [NSMutableDictionary new];
5957
_eventQueueLock = [NSLock new];
60-
}
61-
62-
- (void)setPaused:(BOOL)paused
63-
{
64-
if (_paused != paused) {
65-
_paused = paused;
66-
if (_pauseCallback) {
67-
_pauseCallback();
68-
}
69-
}
58+
_eventsDispatchScheduled = NO;
7059
}
7160

7261
- (void)sendAppEventWithName:(NSString *)name body:(id)body
@@ -139,23 +128,23 @@ - (void)sendTextEventWithType:(RCTTextEventType)type
139128

140129
- (void)sendEvent:(id<RCTEvent>)event
141130
{
142-
if (!event.canCoalesce) {
143-
[self flushEventsQueue];
144-
[self dispatchEvent:event];
145-
return;
146-
}
147-
148131
[_eventQueueLock lock];
149132

150133
NSNumber *eventID = RCTGetEventID(event);
151-
id<RCTEvent> previousEvent = _eventQueue[eventID];
152134

135+
id<RCTEvent> previousEvent = _eventQueue[eventID];
153136
if (previousEvent) {
137+
RCTAssert([event canCoalesce], @"Got event %@ which cannot be coalesced, but has the same eventID %@ as the previous event %@", event, eventID, previousEvent);
154138
event = [previousEvent coalesceWithEvent:event];
155139
}
156-
157140
_eventQueue[eventID] = event;
158-
self.paused = NO;
141+
142+
if (!_eventsDispatchScheduled) {
143+
_eventsDispatchScheduled = YES;
144+
[_bridge dispatchBlock:^{
145+
[self flushEventsQueue];
146+
} queue:RCTJSThread];
147+
}
159148

160149
[_eventQueueLock unlock];
161150
}
@@ -170,17 +159,13 @@ - (dispatch_queue_t)methodQueue
170159
return RCTJSThread;
171160
}
172161

173-
- (void)didUpdateFrame:(__unused RCTFrameUpdate *)update
174-
{
175-
[self flushEventsQueue];
176-
}
177-
162+
// js thread only
178163
- (void)flushEventsQueue
179164
{
180165
[_eventQueueLock lock];
181166
NSDictionary *eventQueue = _eventQueue;
182167
_eventQueue = [NSMutableDictionary new];
183-
self.paused = YES;
168+
_eventsDispatchScheduled = NO;
184169
[_eventQueueLock unlock];
185170

186171
for (id<RCTEvent> event in eventQueue.allValues) {

0 commit comments

Comments
 (0)