From ba788c2dadef58b8ab26fc61b6bc70426eaeeae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 22 May 2026 04:45:50 -0700 Subject: [PATCH 1/2] Add `trace` helper to Systrace (#56936) Summary: Adds a new `trace` helper to the `Systrace` module that wraps a function call between `beginEvent` and `endEvent`, ensuring the trace section is closed even if the function throws. Usage: ``` Systrace.trace('name', () => { // logic to trace }); ``` The implementation is guarded with `isEnabled()` and inlines `beginEvent`/`endEvent` so that the no-op path avoids redundant work when tracing is disabled. Changelog: [General][Added] - Add `Systrace.trace` helper that wraps a function with begin/end events using try/finally Reviewed By: javache Differential Revision: D106071568 --- .../Libraries/Performance/Systrace.js | 28 +++++++++++++++++++ packages/react-native/ReactNativeApi.d.ts | 10 +++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Performance/Systrace.js b/packages/react-native/Libraries/Performance/Systrace.js index 4e1be11ce6f5..8b636f13f345 100644 --- a/packages/react-native/Libraries/Performance/Systrace.js +++ b/packages/react-native/Libraries/Performance/Systrace.js @@ -65,6 +65,33 @@ export function endEvent(args?: EventArgs): void { } } +/** + * Traces the execution of the given function by marking its start with + * `beginEvent` and its end with `endEvent`, even if the function throws. + * + * @example + * Systrace.trace('myEvent', () => { + * // logic to trace + * }); + */ +export function trace( + eventName: EventName, + fn: () => T, + args?: EventArgs, +): T { + if (isEnabled()) { + const eventNameString = + typeof eventName === 'function' ? eventName() : eventName; + global.nativeTraceBeginSection(TRACE_TAG_REACT, eventNameString, args); + try { + return fn(); + } finally { + global.nativeTraceEndSection(TRACE_TAG_REACT); + } + } + return fn(); +} + /** * Marks the start of a potentially asynchronous event. The end of this event * should be marked calling the `endAsyncEvent` function with the cookie @@ -128,6 +155,7 @@ if (__DEV__) { setEnabled, beginEvent, endEvent, + trace, beginAsyncEvent, endAsyncEvent, counterEvent, diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 12b2066109a4..abd52ddb4370 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7f88fbd0bea021db8f52ca8ef3709d9e>> + * @generated SignedSource<<08dd369849273136812ea5edbda6e1df>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -5041,6 +5041,7 @@ declare namespace Systrace { setEnabled, beginEvent, endEvent, + trace, beginAsyncEvent, endAsyncEvent, counterEvent, @@ -5628,6 +5629,11 @@ declare type TouchEventProps = { readonly onTouchStart?: (e: GestureResponderEvent) => void readonly onTouchStartCapture?: (e: GestureResponderEvent) => void } +declare function trace( + eventName: EventName, + fn: () => T, + args?: EventArgs, +): T declare type TransformsStyle = ____TransformStyle_Internal declare interface TurboModule extends DEPRECATED_RCTExport {} declare namespace TurboModuleRegistry { @@ -6217,7 +6223,7 @@ export { Switch, // 015be3f7 SwitchChangeEvent, // 63e9c50b SwitchProps, // 0dbf23ea - Systrace, // b5aa21fc + Systrace, // 626d178c TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 From 52b3488e66ed220d4f8544846ccc00c7d051dc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 22 May 2026 04:45:50 -0700 Subject: [PATCH 2/2] Migrate begin/endEvent try/finally patterns to `Systrace.trace` (#56937) Summary: Migrates existing `Systrace.beginEvent` / `Systrace.endEvent` (and bare `beginEvent`/`endEvent`) try/finally blocks to the new `Systrace.trace` helper, which keeps the same semantics but is more concise. Other existing `beginEvent`/`endEvent` call sites are left untouched because they have `__DEV__` guards that do not wrap the entire body and would require additional restructuring. Changelog: [Internal] Reviewed By: javache Differential Revision: D106073157 --- .../Libraries/BatchedBridge/MessageQueue.js | 93 +++++++--------- .../Libraries/Core/Timers/JSTimers.js | 105 +++++++++--------- .../EventEmitter/RCTDeviceEventEmitter.js | 14 +-- .../internals/IntersectionObserverManager.js | 10 +- .../internals/MutationObserverManager.js | 12 +- 5 files changed, 110 insertions(+), 124 deletions(-) diff --git a/packages/react-native/Libraries/BatchedBridge/MessageQueue.js b/packages/react-native/Libraries/BatchedBridge/MessageQueue.js index b177b30c1c1d..137233be1dfa 100644 --- a/packages/react-native/Libraries/BatchedBridge/MessageQueue.js +++ b/packages/react-native/Libraries/BatchedBridge/MessageQueue.js @@ -393,54 +393,49 @@ class MessageQueue { } __callReactNativeMicrotasks() { - Systrace.beginEvent('JSTimers.callReactNativeMicrotasks()'); - try { + Systrace.trace('JSTimers.callReactNativeMicrotasks()', () => { if (this._reactNativeMicrotasksCallback != null) { this._reactNativeMicrotasksCallback(); } - } finally { - Systrace.endEvent(); - } + }); } __callFunction(module: string, method: string, args: unknown[]): void { this._lastFlush = Date.now(); this._eventLoopStartTime = this._lastFlush; - if (__DEV__ || this.__spy) { - Systrace.beginEvent(`${module}.${method}(${stringifySafe(args)})`); - } else { - Systrace.beginEvent(`${module}.${method}(...)`); - } - try { - if (this.__spy) { - this.__spy({type: TO_JS, module, method, args}); - } - const moduleMethods = this.getCallableModule(module); - if (!moduleMethods) { - const callableModuleNames = Object.keys(this._lazyCallableModules); - const n = callableModuleNames.length; - const callableModuleNameList = callableModuleNames.join(', '); - - // TODO(T122225939): Remove after investigation: Why are we getting to this line in bridgeless mode? - const isBridgelessMode = - global.RN$Bridgeless === true ? 'true' : 'false'; - invariant( - false, - `Failed to call into JavaScript module method ${module}.${method}(). Module has not been registered as callable. Bridgeless Mode: ${isBridgelessMode}. Registered callable JavaScript modules (n = ${n}): ${callableModuleNameList}. + Systrace.trace( + __DEV__ || this.__spy + ? `${module}.${method}(${stringifySafe(args)})` + : `${module}.${method}(...)`, + () => { + if (this.__spy) { + this.__spy({type: TO_JS, module, method, args}); + } + const moduleMethods = this.getCallableModule(module); + if (!moduleMethods) { + const callableModuleNames = Object.keys(this._lazyCallableModules); + const n = callableModuleNames.length; + const callableModuleNameList = callableModuleNames.join(', '); + + // TODO(T122225939): Remove after investigation: Why are we getting to this line in bridgeless mode? + const isBridgelessMode = + global.RN$Bridgeless === true ? 'true' : 'false'; + invariant( + false, + `Failed to call into JavaScript module method ${module}.${method}(). Module has not been registered as callable. Bridgeless Mode: ${isBridgelessMode}. Registered callable JavaScript modules (n = ${n}): ${callableModuleNameList}. A frequent cause of the error is that the application entry file path is incorrect. This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.`, - ); - } - // $FlowFixMe[invalid-computed-prop] - if (!moduleMethods[method]) { - invariant( - false, - `Failed to call into JavaScript module method ${module}.${method}(). Module exists, but the method is undefined.`, - ); - } - moduleMethods[method].apply(moduleMethods, args); - } finally { - Systrace.endEvent(); - } + ); + } + // $FlowFixMe[invalid-computed-prop] + if (!moduleMethods[method]) { + invariant( + false, + `Failed to call into JavaScript module method ${module}.${method}(). Module exists, but the method is undefined.`, + ); + } + moduleMethods[method].apply(moduleMethods, args); + }, + ); } __invokeCallback(cbID: number, args: unknown[]): void { @@ -471,28 +466,24 @@ class MessageQueue { const profileName = debug ? '' : cbID; - /* $FlowFixMe[constant-condition] Error discovered during Constant - * Condition roll out. See https://fburl.com/workplace/1v97vimq. */ - if (callback && this.__spy) { + if (this.__spy) { this.__spy({type: TO_JS, module: null, method: profileName, args}); } - Systrace.beginEvent( + Systrace.trace( `MessageQueue.invokeCallback(${profileName}, ${stringifySafe(args)})`, + () => { + this._successCallbacks.delete(callID); + this._failureCallbacks.delete(callID); + callback(...args); + }, ); - } - - try { + } else { if (!callback) { return; } - this._successCallbacks.delete(callID); this._failureCallbacks.delete(callID); callback(...args); - } finally { - if (__DEV__) { - Systrace.endEvent(); - } } } } diff --git a/packages/react-native/Libraries/Core/Timers/JSTimers.js b/packages/react-native/Libraries/Core/Timers/JSTimers.js index 8dd62b7e21e6..82bd3c81de00 100644 --- a/packages/react-native/Libraries/Core/Timers/JSTimers.js +++ b/packages/react-native/Libraries/Core/Timers/JSTimers.js @@ -13,7 +13,7 @@ import NativeTiming from './NativeTiming'; const toError = require('../../../src/private/utilities/toError').default; const BatchedBridge = require('../../BatchedBridge/BatchedBridge').default; -const Systrace = require('../../Performance/Systrace'); +const {trace} = require('../../Performance/Systrace'); const invariant = require('invariant'); /** @@ -96,47 +96,47 @@ function _callTimer(timerID: number, frameTime: number, didTimeout: ?boolean) { return; } - if (__DEV__) { - Systrace.beginEvent(type + ' [invoke]'); - } - - // Clear the metadata - if (type !== 'setInterval') { - _clearIndex(timerIndex); - } + const doCallTimer = () => { + // Clear the metadata + if (type !== 'setInterval') { + _clearIndex(timerIndex); + } - try { - if ( - type === 'setTimeout' || - type === 'setInterval' || - type === 'queueReactNativeMicrotask' - ) { - callback(); - } else if (type === 'requestAnimationFrame') { - callback(global.performance.now()); - } else if (type === 'requestIdleCallback') { - callback({ - timeRemaining: function () { - // TODO: Optimisation: allow running for longer than one frame if - // there are no pending JS calls on the bridge from native. This - // would require a way to check the bridge queue synchronously. - return Math.max( - 0, - FRAME_DURATION - (global.performance.now() - frameTime), - ); - }, - didTimeout: !!didTimeout, - }); - } else { - console.error('Tried to call a callback with invalid type: ' + type); + try { + if ( + type === 'setTimeout' || + type === 'setInterval' || + type === 'queueReactNativeMicrotask' + ) { + callback(); + } else if (type === 'requestAnimationFrame') { + callback(global.performance.now()); + } else if (type === 'requestIdleCallback') { + callback({ + timeRemaining: function () { + // TODO: Optimisation: allow running for longer than one frame if + // there are no pending JS calls on the bridge from native. This + // would require a way to check the bridge queue synchronously. + return Math.max( + 0, + FRAME_DURATION - (global.performance.now() - frameTime), + ); + }, + didTimeout: !!didTimeout, + }); + } else { + console.error('Tried to call a callback with invalid type: ' + type); + } + } catch (e: unknown) { + // Don't rethrow so that we can run all timers. + errors.push(toError(e)); } - } catch (e: unknown) { - // Don't rethrow so that we can run all timers. - errors.push(toError(e)); - } + }; if (__DEV__) { - Systrace.endEvent(); + trace(type + ' [invoke]', doCallTimer); + } else { + doCallTimer(); } } @@ -149,24 +149,25 @@ function _callReactNativeMicrotasksPass() { return false; } - if (__DEV__) { - Systrace.beginEvent('callReactNativeMicrotasksPass()'); - } - - // The main reason to extract a single pass is so that we can track - // in the system trace - const passReactNativeMicrotasks = reactNativeMicrotasks; - reactNativeMicrotasks = []; + const runPass = () => { + // The main reason to extract a single pass is so that we can track + // in the system trace + const passReactNativeMicrotasks = reactNativeMicrotasks; + reactNativeMicrotasks = []; - // Use for loop rather than forEach as per @vjeux's advice - // https://github.com/facebook/react-native/commit/c8fd9f7588ad02d2293cac7224715f4af7b0f352#commitcomment-14570051 - for (let i = 0; i < passReactNativeMicrotasks.length; ++i) { - _callTimer(passReactNativeMicrotasks[i], 0); - } + // Use for loop rather than forEach as per @vjeux's advice + // https://github.com/facebook/react-native/commit/c8fd9f7588ad02d2293cac7224715f4af7b0f352#commitcomment-14570051 + for (let i = 0; i < passReactNativeMicrotasks.length; ++i) { + _callTimer(passReactNativeMicrotasks[i], 0); + } + }; if (__DEV__) { - Systrace.endEvent(); + trace('callReactNativeMicrotasksPass()', runPass); + } else { + runPass(); } + return reactNativeMicrotasks.length > 0; } diff --git a/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js index c1f6db1c5ee4..9b371bdb5b31 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 {beginEvent, endEvent} from '../Performance/Systrace'; +import {trace} from '../Performance/Systrace'; import EventEmitter from '../vendor/emitter/EventEmitter'; // FIXME: use typed events @@ -29,12 +29,12 @@ class RCTDeviceEventEmitterImpl extends EventEmitter eventType: TEvent, ...args: RCTDeviceEventDefinitions[TEvent] ): void { - beginEvent(() => `RCTDeviceEventEmitter.emit#${eventType}`); - try { - super.emit(eventType, ...args); - } finally { - endEvent(); - } + trace( + () => `RCTDeviceEventEmitter.emit#${eventType}`, + () => { + super.emit(eventType, ...args); + }, + ); } } const RCTDeviceEventEmitter: IEventEmitter = diff --git a/packages/react-native/src/private/webapis/intersectionobserver/internals/IntersectionObserverManager.js b/packages/react-native/src/private/webapis/intersectionobserver/internals/IntersectionObserverManager.js index 7e69617b1d66..97e0fcf49051 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/internals/IntersectionObserverManager.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/internals/IntersectionObserverManager.js @@ -25,7 +25,7 @@ import type IntersectionObserver, { import type IntersectionObserverEntry from '../IntersectionObserverEntry'; import type {NativeIntersectionObserverToken} from '../specs/NativeIntersectionObserver'; -import * as Systrace from '../../../../../Libraries/Performance/Systrace'; +import {trace} from '../../../../../Libraries/Performance/Systrace'; import { getInstanceHandle, getNativeNodeReference, @@ -219,14 +219,10 @@ export function unobserve( * entries to dispatch. */ function notifyIntersectionObservers(): void { - Systrace.beginEvent( + trace( 'IntersectionObserverManager.notifyIntersectionObservers', + doNotifyIntersectionObservers, ); - try { - doNotifyIntersectionObservers(); - } finally { - Systrace.endEvent(); - } } function doNotifyIntersectionObservers(): void { diff --git a/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js b/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js index 21c7698db84d..84cc0df65381 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js +++ b/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js @@ -24,7 +24,7 @@ import type MutationObserver, { } from '../MutationObserver'; import type MutationRecord from '../MutationRecord'; -import * as Systrace from '../../../../../Libraries/Performance/Systrace'; +import {trace} from '../../../../../Libraries/Performance/Systrace'; import {getPublicInstanceFromInternalInstanceHandle} from '../../../../../Libraries/ReactNative/RendererProxy'; import warnOnce from '../../../../../Libraries/Utilities/warnOnce'; import {getNativeNodeReference} from '../../dom/nodes/internals/NodeInternals'; @@ -147,12 +147,10 @@ export function unobserveAll(mutationObserverId: number): void { * entries to dispatch. */ function notifyMutationObservers(): void { - Systrace.beginEvent('MutationObserverManager.notifyMutationObservers'); - try { - doNotifyMutationObservers(); - } finally { - Systrace.endEvent(); - } + trace( + 'MutationObserverManager.notifyMutationObservers', + doNotifyMutationObservers, + ); } function doNotifyMutationObservers(): void {