From 56a0ee3c979761c569e4eec00363b33fe89c0148 Mon Sep 17 00:00:00 2001 From: secitr Date: Thu, 24 Sep 2026 08:55:58 +0300 Subject: [PATCH 1/3] Avoid closure allocation in RCTDeviceEventEmitter.emit when tracing is disabled --- .../EventEmitter/RCTDeviceEventEmitter.js | 21 ++-- .../__tests__/RCTDeviceEventEmitter-test.js | 97 +++++++++++++++++++ 2 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-test.js diff --git a/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js index 9b371bdb5b31..62fd13bac54f 100644 --- a/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js +++ b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js @@ -10,7 +10,7 @@ import type {IEventEmitter} from '../vendor/emitter/EventEmitter'; -import {trace} from '../Performance/Systrace'; +import {beginEvent, endEvent, isEnabled} from '../Performance/Systrace'; import EventEmitter from '../vendor/emitter/EventEmitter'; // FIXME: use typed events @@ -24,17 +24,24 @@ type RCTDeviceEventDefinitions = {[name: string]: Array}; * NativeModules that emit events should instead subclass `NativeEventEmitter`. */ class RCTDeviceEventEmitterImpl extends EventEmitter { - // Add systrace to RCTDeviceEventEmitter.emit method for debugging + // Add systrace to RCTDeviceEventEmitter.emit method for debugging. + // `beginEvent`/`endEvent` are used instead of `trace` so that no closures + // are allocated when tracing is disabled (the common case); `trace` always + // allocates a callback, which is costly on this hot path. emit( eventType: TEvent, ...args: RCTDeviceEventDefinitions[TEvent] ): void { - trace( - () => `RCTDeviceEventEmitter.emit#${eventType}`, - () => { + if (isEnabled()) { + beginEvent(`RCTDeviceEventEmitter.emit#${eventType}`); + try { super.emit(eventType, ...args); - }, - ); + } finally { + endEvent(); + } + return; + } + super.emit(eventType, ...args); } } const RCTDeviceEventEmitter: IEventEmitter = diff --git a/packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-test.js b/packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-test.js new file mode 100644 index 000000000000..c6d946e57e67 --- /dev/null +++ b/packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-test.js @@ -0,0 +1,97 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import RCTDeviceEventEmitter from '../RCTDeviceEventEmitter'; + +const TRACE_TAG_REACT = 1 << 13; // eslint-disable-line no-bitwise + +function enableTracing() { + global.nativeTraceIsTracing = jest.fn(() => true); + global.nativeTraceBeginSection = jest.fn(); + global.nativeTraceEndSection = jest.fn(); +} + +function disableTracing() { + delete global.nativeTraceIsTracing; + delete global.nativeTraceBeginSection; + delete global.nativeTraceEndSection; + delete global.__RCTProfileIsProfiling; +} + +describe('RCTDeviceEventEmitter', () => { + afterEach(() => { + RCTDeviceEventEmitter.removeAllListeners(); + disableTracing(); + }); + + it('forwards events and arguments to listeners', () => { + const listener = jest.fn(); + RCTDeviceEventEmitter.addListener('event', listener); + + RCTDeviceEventEmitter.emit('event', 'one', 2); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith('one', 2); + }); + + it('does not call trace sections when tracing is disabled', () => { + const listener = jest.fn(); + RCTDeviceEventEmitter.addListener('event', listener); + + RCTDeviceEventEmitter.emit('event'); + + expect(listener).toHaveBeenCalledTimes(1); + expect(global.nativeTraceBeginSection).toBeUndefined(); + }); + + it('wraps emit in a trace section when tracing is enabled', () => { + enableTracing(); + const listener = jest.fn(); + RCTDeviceEventEmitter.addListener('event', listener); + + RCTDeviceEventEmitter.emit('event'); + + expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1); + expect(global.nativeTraceBeginSection).toHaveBeenCalledWith( + TRACE_TAG_REACT, + 'RCTDeviceEventEmitter.emit#event', + undefined, + ); + expect(listener).toHaveBeenCalledTimes(1); + expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1); + expect(global.nativeTraceEndSection).toHaveBeenCalledWith( + TRACE_TAG_REACT, + undefined, + ); + }); + + it('ends the trace section even when a listener throws', () => { + enableTracing(); + RCTDeviceEventEmitter.addListener('event', () => { + throw new Error('boom'); + }); + + expect(() => RCTDeviceEventEmitter.emit('event')).toThrow('boom'); + + expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1); + expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1); + }); + + it('traces when __RCTProfileIsProfiling is set and nativeTraceIsTracing is absent', () => { + global.__RCTProfileIsProfiling = true; + global.nativeTraceBeginSection = jest.fn(); + global.nativeTraceEndSection = jest.fn(); + + RCTDeviceEventEmitter.emit('event'); + + expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1); + expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1); + }); +}); From 4359d540b16230c7362c11029389370dd7f12747 Mon Sep 17 00:00:00 2001 From: secitr Date: Thu, 24 Sep 2026 10:17:50 +0300 Subject: [PATCH 2/3] Use else branch instead of early return in emit --- .../Libraries/EventEmitter/RCTDeviceEventEmitter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js index 62fd13bac54f..74aa36574f8b 100644 --- a/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js +++ b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js @@ -39,9 +39,9 @@ class RCTDeviceEventEmitterImpl extends EventEmitter } finally { endEvent(); } - return; + } else { + super.emit(eventType, ...args); } - super.emit(eventType, ...args); } } const RCTDeviceEventEmitter: IEventEmitter = From 844dd9664ad8a695455d371cd9ae1a69bd200223 Mon Sep 17 00:00:00 2001 From: secitr Date: Fri, 25 Sep 2026 09:25:10 +0300 Subject: [PATCH 3/3] Convert RCTDeviceEventEmitter tests to Fantom itest using public API --- ...test.js => RCTDeviceEventEmitter-itest.js} | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) rename packages/react-native/Libraries/EventEmitter/__tests__/{RCTDeviceEventEmitter-test.js => RCTDeviceEventEmitter-itest.js} (78%) diff --git a/packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-test.js b/packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-itest.js similarity index 78% rename from packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-test.js rename to packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-itest.js index c6d946e57e67..f5ed16186e92 100644 --- a/packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-test.js +++ b/packages/react-native/Libraries/EventEmitter/__tests__/RCTDeviceEventEmitter-itest.js @@ -8,7 +8,9 @@ * @format */ -import RCTDeviceEventEmitter from '../RCTDeviceEventEmitter'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import {DeviceEventEmitter} from 'react-native'; const TRACE_TAG_REACT = 1 << 13; // eslint-disable-line no-bitwise @@ -25,17 +27,17 @@ function disableTracing() { delete global.__RCTProfileIsProfiling; } -describe('RCTDeviceEventEmitter', () => { +describe('DeviceEventEmitter', () => { afterEach(() => { - RCTDeviceEventEmitter.removeAllListeners(); + DeviceEventEmitter.removeAllListeners(); disableTracing(); }); it('forwards events and arguments to listeners', () => { const listener = jest.fn(); - RCTDeviceEventEmitter.addListener('event', listener); + DeviceEventEmitter.addListener('event', listener); - RCTDeviceEventEmitter.emit('event', 'one', 2); + DeviceEventEmitter.emit('event', 'one', 2); expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledWith('one', 2); @@ -43,9 +45,9 @@ describe('RCTDeviceEventEmitter', () => { it('does not call trace sections when tracing is disabled', () => { const listener = jest.fn(); - RCTDeviceEventEmitter.addListener('event', listener); + DeviceEventEmitter.addListener('event', listener); - RCTDeviceEventEmitter.emit('event'); + DeviceEventEmitter.emit('event'); expect(listener).toHaveBeenCalledTimes(1); expect(global.nativeTraceBeginSection).toBeUndefined(); @@ -54,9 +56,9 @@ describe('RCTDeviceEventEmitter', () => { it('wraps emit in a trace section when tracing is enabled', () => { enableTracing(); const listener = jest.fn(); - RCTDeviceEventEmitter.addListener('event', listener); + DeviceEventEmitter.addListener('event', listener); - RCTDeviceEventEmitter.emit('event'); + DeviceEventEmitter.emit('event'); expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1); expect(global.nativeTraceBeginSection).toHaveBeenCalledWith( @@ -74,11 +76,11 @@ describe('RCTDeviceEventEmitter', () => { it('ends the trace section even when a listener throws', () => { enableTracing(); - RCTDeviceEventEmitter.addListener('event', () => { + DeviceEventEmitter.addListener('event', () => { throw new Error('boom'); }); - expect(() => RCTDeviceEventEmitter.emit('event')).toThrow('boom'); + expect(() => DeviceEventEmitter.emit('event')).toThrow('boom'); expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1); expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1); @@ -89,7 +91,7 @@ describe('RCTDeviceEventEmitter', () => { global.nativeTraceBeginSection = jest.fn(); global.nativeTraceEndSection = jest.fn(); - RCTDeviceEventEmitter.emit('event'); + DeviceEventEmitter.emit('event'); expect(global.nativeTraceBeginSection).toHaveBeenCalledTimes(1); expect(global.nativeTraceEndSection).toHaveBeenCalledTimes(1);