Skip to content

Avoid closure allocation in RCTDeviceEventEmitter.emit when tracing is disabled - #58661

Closed
secitr wants to merge 3 commits into
react:mainfrom
secitr:fix/device-event-emitter-reduce-allocations
Closed

secitr wants to merge 3 commits into
react:mainfrom
secitr:fix/device-event-emitter-reduce-allocations

Conversation

@secitr

@secitr secitr commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

RCTDeviceEventEmitter.emit is one of the hottest paths in React Native:
NativeEventEmitter.emit delegates to it, so every event delivered from
native (scroll, touch, keyboard, app state, ...) goes through this method.
The current implementation wraps every call in Systrace.trace, which
always allocates two closures (a lazy event-name thunk and a callback)
on every emit — even when tracing is disabled, which is the common case.

This changes emit to guard on Systrace.isEnabled() and use
beginEvent/endEvent directly:

  • tracing disabled (common case): one branch check + a plain call —
    zero allocations
  • tracing enabled: same trace section name (RCTDeviceEventEmitter.emit#)
    and the same begin/end semantics, including endEvent() in a finally
    block when a listener throws

Behavior is unchanged; only the fast-path allocations are removed.

Changelog:

[General] [Changed] - RCTDeviceEventEmitter.emit no longer allocates closures on every emit when tracing is disabled, reducing per-event allocation on the native-to-JS event path

Test Plan:

  • New Fantom integration tests in packages/react-native/Libraries/EventEmitter/tests/RCTDeviceEventEmitter-itest.js (public API, runs against Hermes):
    • event + args are forwarded to listeners
    • no trace section calls when tracing is disabled
    • trace section is begun/ended with the correct name when tracing is enabled
    • end section is still emitted when a listener throws
    • __RCTProfileIsProfiling fallback still enables tracing
  • yarn test packages/react-native/Libraries — 433 tests pass (30 suites)
  • yarn flow-check — 0 errors
  • yarn lint — 0 warnings
  • Micro-benchmark (Node v24, tracing disabled, 5M emits with a scroll-like payload):
    • before: 158ms total (~31.6 ns/emit)
    • after: 117ms total (~23.4 ns/emit)
    • ~26% faster per emit; on Hermes/mobile CPUs the win comes from avoiding
      two heap-allocated closures per event

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 24, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 24, 2026
Comment on lines +43 to +44
}
super.emit(eventType, ...args);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's have this method in a else branch and avoid the early return please

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — moved the fast path into the else branch and removed the early return. Pushed in 4359d54.

@secitr

secitr commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

The iOS RNTester e2e (Debug) failed on retry_2 while Release passed and the same suite passed on the two previous runs — looks like a flaky simulator. A re-run might clear it. Let me know if anything on the code side needs attention.

@rubennorte

Copy link
Copy Markdown
Member

This change looks fine but please don't add new Jest tests as we're moving to Fantom to make sure the environment that runtime code runs in tests is as close as the real environment as possible. In this case, the test should be renamed to have the -itest suffix and use public APIs instead.

@secitr

secitr commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

This change looks fine but please don't add new Jest tests as we're moving to Fantom to make sure the environment that runtime code runs in tests is as close as the real environment as possible. In this case, the test should be renamed to have the -itest suffix and use public APIs instead.

Done — converted the added Jest test to a Fantom itest (RCTDeviceEventEmitter-itest.js), renamed with the -itest suffix, and it now uses the public API (DeviceEventEmitter from react-native). All 5 tests pass locally against the Hermes runtime; pushed in 844dd96.

@meta-codesync

meta-codesync Bot commented Sep 25, 2026

Copy link
Copy Markdown

@cortinico has imported this pull request. If you are a Meta employee, you can view this in D121799063.

@meta-codesync meta-codesync Bot closed this in 7f6043b Sep 25, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Sep 25, 2026
@meta-codesync

meta-codesync Bot commented Sep 25, 2026

Copy link
Copy Markdown

@cortinico merged this pull request in 7f6043b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants