Skip to content

Commit 56a0ee3

Browse files
committed
Avoid closure allocation in RCTDeviceEventEmitter.emit when tracing is disabled
1 parent b71d466 commit 56a0ee3

2 files changed

Lines changed: 111 additions & 7 deletions

File tree

‎packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import type {IEventEmitter} from '../vendor/emitter/EventEmitter';
1212

13-
import {trace} from '../Performance/Systrace';
13+
import {beginEvent, endEvent, isEnabled} from '../Performance/Systrace';
1414
import EventEmitter from '../vendor/emitter/EventEmitter';
1515

1616
// FIXME: use typed events
@@ -24,17 +24,24 @@ type RCTDeviceEventDefinitions = {[name: string]: Array<any>};
2424
* NativeModules that emit events should instead subclass `NativeEventEmitter`.
2525
*/
2626
class RCTDeviceEventEmitterImpl extends EventEmitter<RCTDeviceEventDefinitions> {
27-
// Add systrace to RCTDeviceEventEmitter.emit method for debugging
27+
// Add systrace to RCTDeviceEventEmitter.emit method for debugging.
28+
// `beginEvent`/`endEvent` are used instead of `trace` so that no closures
29+
// are allocated when tracing is disabled (the common case); `trace` always
30+
// allocates a callback, which is costly on this hot path.
2831
emit<TEvent extends keyof RCTDeviceEventDefinitions>(
2932
eventType: TEvent,
3033
...args: RCTDeviceEventDefinitions[TEvent]
3134
): void {
32-
trace(
33-
() => `RCTDeviceEventEmitter.emit#${eventType}`,
34-
() => {
35+
if (isEnabled()) {
36+
beginEvent(`RCTDeviceEventEmitter.emit#${eventType}`);
37+
try {
3538
super.emit(eventType, ...args);
36-
},
37-
);
39+
} finally {
40+
endEvent();
41+
}
42+
return;
43+
}
44+
super.emit(eventType, ...args);
3845
}
3946
}
4047
const RCTDeviceEventEmitter: IEventEmitter<RCTDeviceEventDefinitions> =
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import RCTDeviceEventEmitter from '../RCTDeviceEventEmitter';
12+
13+
const TRACE_TAG_REACT = 1 << 13; // eslint-disable-line no-bitwise
14+
15+
function enableTracing() {
16+
global.nativeTraceIsTracing = jest.fn(() => true);
17+
global.nativeTraceBeginSection = jest.fn();
18+
global.nativeTraceEndSection = jest.fn();
19+
}
20+
21+
function disableTracing() {
22+
delete global.nativeTraceIsTracing;
23+
delete global.nativeTraceBeginSection;
24+
delete global.nativeTraceEndSection;
25+
delete global.__RCTProfileIsProfiling;
26+
}
27+
28+
describe('RCTDeviceEventEmitter', () => {
29+
afterEach(() => {
30+
RCTDeviceEventEmitter.removeAllListeners();
31+
disableTracing();
32+
});
33+
34+
it('forwards events and arguments to listeners', () => {
35+
const listener = jest.fn();
36+
RCTDeviceEventEmitter.addListener('event', listener);
37+
38+
RCTDeviceEventEmitter.emit('event', 'one', 2);
39+
40+
expect(listener).toHaveBeenCalledTimes(1);
41+
expect(listener).toHaveBeenCalledWith('one', 2);
42+
});
43+
44+
it('does not call trace sections when tracing is disabled', () => {
45+
const listener = jest.fn();
46+
RCTDeviceEventEmitter.addListener('event', listener);
47+
48+
RCTDeviceEventEmitter.emit('event');
49+
50+
expect(listener).toHaveBeenCalledTimes(1);
51+
expect(global.nativeTraceBeginSection).toBeUndefined();
52+
});
53+
54+
it('wraps emit in a trace section when tracing is enabled', () => {
55+
enableTracing();
56+
const listener = jest.fn();
57+
RCTDeviceEventEmitter.addListener('event', listener);
58+
59+
RCTDeviceEventEmitter.emit('event');
60+
61+
expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1);
62+
expect(global.nativeTraceBeginSection).toHaveBeenCalledWith(
63+
TRACE_TAG_REACT,
64+
'RCTDeviceEventEmitter.emit#event',
65+
undefined,
66+
);
67+
expect(listener).toHaveBeenCalledTimes(1);
68+
expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1);
69+
expect(global.nativeTraceEndSection).toHaveBeenCalledWith(
70+
TRACE_TAG_REACT,
71+
undefined,
72+
);
73+
});
74+
75+
it('ends the trace section even when a listener throws', () => {
76+
enableTracing();
77+
RCTDeviceEventEmitter.addListener('event', () => {
78+
throw new Error('boom');
79+
});
80+
81+
expect(() => RCTDeviceEventEmitter.emit('event')).toThrow('boom');
82+
83+
expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1);
84+
expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1);
85+
});
86+
87+
it('traces when __RCTProfileIsProfiling is set and nativeTraceIsTracing is absent', () => {
88+
global.__RCTProfileIsProfiling = true;
89+
global.nativeTraceBeginSection = jest.fn();
90+
global.nativeTraceEndSection = jest.fn();
91+
92+
RCTDeviceEventEmitter.emit('event');
93+
94+
expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1);
95+
expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1);
96+
});
97+
});

0 commit comments

Comments
 (0)