From ee7ac8be55ff763c2a80695a163f10fc5dfec750 Mon Sep 17 00:00:00 2001 From: Emily Brown Date: Thu, 12 Feb 2026 08:44:49 -0800 Subject: [PATCH] Suppress LogBox during performance tracing (#55470) Summary: LogBox errors and warnings can affect performance trace measurements by triggering UI updates during profiling. This change adds a mechanism to suppress LogBox messages when CDP performance tracing is active. It clears the logbox when tracing starts and drops messages during tracing The implementation consists of: 1. **Native observer infrastructure** (`RuntimeTarget.cpp/h`, `RuntimeTargetPerformanceTracerObserver.cpp/h`): `RuntimeTarget` subscribes to the `PerformanceTracer` singleton's state changes and forwards them into JavaScript. It uses helper functions in `RuntimeTargetPerformanceTracerObserver` to install a `__PERFORMANCE_TRACER_OBSERVER__` object on the JavaScript global (a global because it needs to exist before the module system initializes), which tracks tracing state and notifies subscribers via `onTracingStateChange`. 2. **JavaScript observer** (`PerformanceTracerObserver.js`): Provides a clean API to check tracing status (`isTracing()`) and subscribe to state changes. Gracefully handles environments where native support isn't available. 3. **LogBox integration** (`LogBoxData.js`): Subscribes to the performance tracer observer and: - Clears all LogBox messages when tracing starts - Skips logging new errors and exceptions while tracing is active This ensures trace measurements are not impacted by LogBox UI rendering during profiling sessions. Changelog: [GENERAL] [CHANGED] - Suppress LogBox warnings and errors during CDP performance tracing Reviewed By: hoxyq Differential Revision: D92527815 --- .../Libraries/LogBox/Data/LogBoxData.js | 20 +++ .../LogBox/Data/__tests__/LogBoxData-test.js | 133 +++++++++++++++++ .../jsinspector-modern/RuntimeAgent.cpp | 6 + .../jsinspector-modern/RuntimeTarget.cpp | 24 ++- .../jsinspector-modern/RuntimeTarget.h | 19 +++ .../RuntimeTargetTracingStateObserver.cpp | 27 ++++ .../RuntimeTargetTracingStateObserver.h | 27 ++++ .../rndevtools/GlobalStateObserver.js | 10 +- .../rndevtools/TracingStateObserver.js | 28 ++++ .../__tests__/TracingStateObserver-test.js | 139 ++++++++++++++++++ 10 files changed, 428 insertions(+), 5 deletions(-) create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracingStateObserver.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracingStateObserver.h create mode 100644 packages/react-native/src/private/devsupport/rndevtools/TracingStateObserver.js create mode 100644 packages/react-native/src/private/devsupport/rndevtools/__tests__/TracingStateObserver-test.js diff --git a/packages/react-native/Libraries/LogBox/Data/LogBoxData.js b/packages/react-native/Libraries/LogBox/Data/LogBoxData.js index de1ff3a5cc03c5..94b632e431cee6 100644 --- a/packages/react-native/Libraries/LogBox/Data/LogBoxData.js +++ b/packages/react-native/Libraries/LogBox/Data/LogBoxData.js @@ -14,6 +14,7 @@ import type {Stack} from './LogBoxSymbolication'; import type {Category, ExtendedExceptionData, Message} from './parseLogBoxLog'; import DebuggerSessionObserver from '../../../src/private/devsupport/rndevtools/FuseboxSessionObserver'; +import TracingStateObserver from '../../../src/private/devsupport/rndevtools/TracingStateObserver'; import toExtendedError from '../../../src/private/utilities/toExtendedError'; import parseErrorStack from '../../Core/Devtools/parseErrorStack'; import NativeLogBox from '../../NativeModules/specs/NativeLogBox'; @@ -71,6 +72,7 @@ let _isDisabled = false; let _selectedIndex = -1; let hasShownFuseboxWarningsMigrationMessage = false; let hostTargetSessionObserverSubscription = null; +let tracingStateObserverSubscription = null; let warningFilter: WarningFilter = function (format) { return { @@ -205,6 +207,20 @@ export function addLog(log: LogData): void { ); } + if (tracingStateObserverSubscription == null) { + tracingStateObserverSubscription = TracingStateObserver.subscribe( + isTracing => { + if (isTracing) { + clear(); + } + }, + ); + } + + if (TracingStateObserver.isTracing()) { + return; + } + // If Host has Fusebox support if (log.level === 'warn' && global.__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__) { // And there is no active debugging session @@ -241,6 +257,10 @@ export function addLog(log: LogData): void { } export function addException(error: ExtendedExceptionData): void { + if (TracingStateObserver.isTracing()) { + return; + } + // Parsing logs are expensive so we schedule this // otherwise spammy logs would pause rendering. setImmediate(() => { diff --git a/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxData-test.js b/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxData-test.js index e7880ef8c384e6..15d955b2094792 100644 --- a/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxData-test.js +++ b/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxData-test.js @@ -721,4 +721,137 @@ describe('LogBoxData', () => { LogBoxData.setAppInfo(() => info); expect(LogBoxData.getAppInfo()).toBe(info); }); + + describe('Performance tracing suppression', () => { + let mockIsTracing: () => boolean; + let mockSubscribeCallback: ((isTracing: boolean) => void) | null = null; + + beforeEach(() => { + mockIsTracing = jest.fn(() => false); + mockSubscribeCallback = null; + + jest.doMock( + '../../../../src/private/devsupport/rndevtools/TracingStateObserver', + () => ({ + __esModule: true, + default: { + isTracing: () => mockIsTracing(), + subscribe: (callback: (isTracing: boolean) => void) => { + mockSubscribeCallback = callback; + return () => { + mockSubscribeCallback = null; + }; + }, + }, + }), + ); + + jest.resetModules(); + }); + + afterEach(() => { + jest.unmock( + '../../../../src/private/devsupport/rndevtools/TracingStateObserver', + ); + }); + + it('suppresses logs when performance tracing is active', () => { + const LogBoxDataWithMock = require('../LogBoxData'); + + LogBoxDataWithMock.addLog({ + level: 'warn', + message: {content: 'Before tracing', substitutions: []}, + category: 'before-tracing', + componentStack: [], + }); + jest.runOnlyPendingTimers(); + + const observerBefore = jest.fn(); + LogBoxDataWithMock.observe(observerBefore).unsubscribe(); + expect(Array.from(observerBefore.mock.calls[0][0].logs).length).toBe(1); + + mockIsTracing = jest.fn(() => true); + + LogBoxDataWithMock.addLog({ + level: 'warn', + message: {content: 'During tracing', substitutions: []}, + category: 'during-tracing', + componentStack: [], + }); + jest.runOnlyPendingTimers(); + + const observerDuring = jest.fn(); + LogBoxDataWithMock.observe(observerDuring).unsubscribe(); + expect(Array.from(observerDuring.mock.calls[0][0].logs).length).toBe(1); + }); + + it('suppresses exceptions when performance tracing is active', () => { + const LogBoxDataWithMock = require('../LogBoxData'); + + LogBoxDataWithMock.addException({ + message: 'Before tracing exception', + isComponentError: false, + originalMessage: 'Before tracing exception', + name: 'console.error', + componentStack: '', + stack: [], + id: 0, + isFatal: false, + }); + jest.runOnlyPendingTimers(); + + const observerBefore = jest.fn(); + LogBoxDataWithMock.observe(observerBefore).unsubscribe(); + expect(Array.from(observerBefore.mock.calls[0][0].logs).length).toBe(1); + + mockIsTracing = jest.fn(() => true); + + LogBoxDataWithMock.addException({ + message: 'During tracing exception', + isComponentError: false, + originalMessage: 'During tracing exception', + name: 'console.error', + componentStack: '', + stack: [], + id: 1, + isFatal: false, + }); + jest.runOnlyPendingTimers(); + + const observerDuring = jest.fn(); + LogBoxDataWithMock.observe(observerDuring).unsubscribe(); + expect(Array.from(observerDuring.mock.calls[0][0].logs).length).toBe(1); + }); + + it('clears logs when tracing starts', () => { + const LogBoxDataWithMock = require('../LogBoxData'); + + LogBoxDataWithMock.addLog({ + level: 'warn', + message: {content: 'Log 1', substitutions: []}, + category: 'log-1', + componentStack: [], + }); + LogBoxDataWithMock.addLog({ + level: 'warn', + message: {content: 'Log 2', substitutions: []}, + category: 'log-2', + componentStack: [], + }); + jest.runOnlyPendingTimers(); + + const observerBefore = jest.fn(); + LogBoxDataWithMock.observe(observerBefore).unsubscribe(); + expect(Array.from(observerBefore.mock.calls[0][0].logs).length).toBe(2); + + if (mockSubscribeCallback) { + mockSubscribeCallback(true); + } + jest.runOnlyPendingTimers(); + + const observerAfter = jest.fn(); + LogBoxDataWithMock.observe(observerAfter).unsubscribe(); + expect(Array.from(observerAfter.mock.calls[0][0].logs).length).toBe(0); + }); + }); }); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp index 819f9205e7a645..24b89fa9336abe 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp @@ -155,9 +155,15 @@ RuntimeTracingAgent::RuntimeTracingAgent( if (state.enabledCategories.contains(tracing::Category::JavaScriptSampling)) { targetController_.enableSamplingProfiler(); } + if (state.mode == tracing::Mode::CDP) { + targetController_.emitTracingStateChange(true); + } } RuntimeTracingAgent::~RuntimeTracingAgent() { + if (state_.mode == tracing::Mode::CDP) { + targetController_.emitTracingStateChange(false); + } if (state_.enabledCategories.contains( tracing::Category::JavaScriptSampling)) { targetController_.disableSamplingProfiler(); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp index 1a1807696fbf7d..317f1e1ae55b27 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include @@ -43,6 +43,7 @@ void RuntimeTarget::installGlobals() { // NOTE: RuntimeTarget::installDebuggerSessionObserver is in // RuntimeTargetDebuggerSessionObserver.cpp installDebuggerSessionObserver(); + installTracingStateObserver(); // NOTE: RuntimeTarget::installNetworkReporterAPI is in // RuntimeTargetNetwork.cpp installNetworkReporterAPI(); @@ -157,6 +158,23 @@ void RuntimeTarget::emitDebuggerSessionDestroyed() { }); } +void RuntimeTarget::installTracingStateObserver() { + jsExecutor_([](jsi::Runtime& runtime) { + jsinspector_modern::installTracingStateObserver(runtime); + }); +} + +void RuntimeTarget::emitTracingStateChange(bool isTracing) { + jsExecutor_([isTracing](jsi::Runtime& runtime) { + try { + emitTracingStateObserverChange(runtime, isTracing); + } catch (jsi::JSError&) { + // Suppress any errors, they should not be visible to the user + // and should not affect runtime. + } + }); +} + void RuntimeTarget::enableSamplingProfiler() { delegate_.enableSamplingProfiler(); } @@ -275,6 +293,10 @@ RuntimeTargetController::collectSamplingProfile() { return target_.collectSamplingProfile(); } +void RuntimeTargetController::emitTracingStateChange(bool isTracing) { + target_.emitTracingStateChange(isTracing); +} + void RuntimeTargetController::notifyDomainStateChanged( Domain domain, bool enabled, diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h index 7312baac0dbc3e..14f28919054304 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h @@ -21,6 +21,7 @@ #include #include +#include #include #ifndef JSINSPECTOR_EXPORT @@ -147,6 +148,11 @@ class RuntimeTargetController { */ tracing::RuntimeSamplingProfile collectSamplingProfile(); + /** + * Emits a tracing state change to JavaScript via the tracing state observer. + */ + void emitTracingStateChange(bool isTracing); + private: RuntimeTarget &target_; }; @@ -262,6 +268,7 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis tracingAgent_; + /** * Start sampling profiler for a particular JavaScript runtime. */ @@ -304,6 +311,13 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis + +namespace facebook::react::jsinspector_modern { + +void installTracingStateObserver(jsi::Runtime& runtime) { + installGlobalStateObserver( + runtime, + "__TRACING_STATE_OBSERVER__", + "isTracing", + "onTracingStateChange"); +} + +void emitTracingStateObserverChange(jsi::Runtime& runtime, bool isTracing) { + emitGlobalStateObserverChange( + runtime, "__TRACING_STATE_OBSERVER__", "onTracingStateChange", isTracing); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracingStateObserver.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracingStateObserver.h new file mode 100644 index 00000000000000..95f1595e88349c --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracingStateObserver.h @@ -0,0 +1,27 @@ +/* + * 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. + */ + +#pragma once + +#include + +namespace facebook::react::jsinspector_modern { + +/** + * Installs __TRACING_STATE_OBSERVER__ object on the JavaScript's global + * object, which can be referenced from JavaScript side for determining the + * status of performance tracing. + */ +void installTracingStateObserver(jsi::Runtime &runtime); + +/** + * Emits the tracing state change to JavaScript by calling onTracingStateChange + * on __TRACING_STATE_OBSERVER__. + */ +void emitTracingStateObserverChange(jsi::Runtime &runtime, bool isTracing); + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js b/packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js index 2a802c83429760..dc1988a4c806cc 100644 --- a/packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js +++ b/packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js @@ -21,18 +21,20 @@ * This class provides a JS-friendly API over that global object. */ class GlobalStateObserver { - #hasNativeSupport: boolean; #globalName: string; #statusProperty: string; constructor(globalName: string, statusProperty: string) { this.#globalName = globalName; this.#statusProperty = statusProperty; - this.#hasNativeSupport = global.hasOwnProperty(globalName); + } + + #hasNativeSupport(): boolean { + return global.hasOwnProperty(this.#globalName); } getStatus(): boolean { - if (!this.#hasNativeSupport) { + if (!this.#hasNativeSupport()) { return false; } @@ -40,7 +42,7 @@ class GlobalStateObserver { } subscribe(callback: (status: boolean) => void): () => void { - if (!this.#hasNativeSupport) { + if (!this.#hasNativeSupport()) { return () => {}; } diff --git a/packages/react-native/src/private/devsupport/rndevtools/TracingStateObserver.js b/packages/react-native/src/private/devsupport/rndevtools/TracingStateObserver.js new file mode 100644 index 00000000000000..821fc28a567637 --- /dev/null +++ b/packages/react-native/src/private/devsupport/rndevtools/TracingStateObserver.js @@ -0,0 +1,28 @@ +/** + * 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 + * @format + */ + +import GlobalStateObserver from './GlobalStateObserver'; + +const observer = new GlobalStateObserver( + '__TRACING_STATE_OBSERVER__', + 'isTracing', +); + +const TracingStateObserver = { + isTracing(): boolean { + return observer.getStatus(); + }, + + subscribe(callback: (isTracing: boolean) => void): () => void { + return observer.subscribe(callback); + }, +}; + +export default TracingStateObserver; diff --git a/packages/react-native/src/private/devsupport/rndevtools/__tests__/TracingStateObserver-test.js b/packages/react-native/src/private/devsupport/rndevtools/__tests__/TracingStateObserver-test.js new file mode 100644 index 00000000000000..d780def274edb4 --- /dev/null +++ b/packages/react-native/src/private/devsupport/rndevtools/__tests__/TracingStateObserver-test.js @@ -0,0 +1,139 @@ +/** + * 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 + */ + +'use strict'; + +describe('TracingStateObserver', () => { + beforeEach(() => { + jest.resetModules(); + delete global.__TRACING_STATE_OBSERVER__; + }); + + describe('without native support', () => { + it('isTracing returns false when native observer is not available', () => { + const TracingStateObserver = require('../TracingStateObserver').default; + expect(TracingStateObserver.isTracing()).toBe(false); + }); + + it('subscribe returns a no-op unsubscribe when native observer is not available', () => { + const TracingStateObserver = require('../TracingStateObserver').default; + const callback = jest.fn(); + const unsubscribe = TracingStateObserver.subscribe(callback); + + expect(typeof unsubscribe).toBe('function'); + + expect(() => unsubscribe()).not.toThrow(); + }); + }); + + describe('with native support', () => { + beforeEach(() => { + const mockSubscribers = new Set<(isTracing: boolean) => void>(); + global.__TRACING_STATE_OBSERVER__ = { + isTracing: false, + subscribers: mockSubscribers, + onTracingStateChange: (isTracing: boolean) => { + global.__TRACING_STATE_OBSERVER__.isTracing = isTracing; + mockSubscribers.forEach(callback => callback(isTracing)); + }, + }; + }); + + it('isTracing returns current tracing state', () => { + const TracingStateObserver = require('../TracingStateObserver').default; + + expect(TracingStateObserver.isTracing()).toBe(false); + + global.__TRACING_STATE_OBSERVER__.onTracingStateChange(true); + expect(TracingStateObserver.isTracing()).toBe(true); + + global.__TRACING_STATE_OBSERVER__.onTracingStateChange(false); + expect(TracingStateObserver.isTracing()).toBe(false); + }); + + it('subscribe adds callback to subscribers and returns unsubscribe function', () => { + const TracingStateObserver = require('../TracingStateObserver').default; + const callback = jest.fn(); + + const unsubscribe = TracingStateObserver.subscribe(callback); + + expect(global.__TRACING_STATE_OBSERVER__.subscribers.has(callback)).toBe( + true, + ); + + global.__TRACING_STATE_OBSERVER__.onTracingStateChange(true); + expect(callback).toHaveBeenCalledWith(true); + + unsubscribe(); + expect(global.__TRACING_STATE_OBSERVER__.subscribers.has(callback)).toBe( + false, + ); + + callback.mockClear(); + global.__TRACING_STATE_OBSERVER__.onTracingStateChange(false); + expect(callback).not.toHaveBeenCalled(); + }); + + it('multiple subscribers receive state changes', () => { + const TracingStateObserver = require('../TracingStateObserver').default; + const callback1 = jest.fn(); + const callback2 = jest.fn(); + + TracingStateObserver.subscribe(callback1); + TracingStateObserver.subscribe(callback2); + + global.__TRACING_STATE_OBSERVER__.onTracingStateChange(true); + + expect(callback1).toHaveBeenCalledWith(true); + expect(callback2).toHaveBeenCalledWith(true); + }); + }); + + describe('with late native support installation', () => { + it('detects native global installed after module load', () => { + const TracingStateObserver = require('../TracingStateObserver').default; + expect(TracingStateObserver.isTracing()).toBe(false); + + const mockSubscribers = new Set<(isTracing: boolean) => void>(); + global.__TRACING_STATE_OBSERVER__ = { + isTracing: true, + subscribers: mockSubscribers, + onTracingStateChange: (isTracing: boolean) => { + global.__TRACING_STATE_OBSERVER__.isTracing = isTracing; + mockSubscribers.forEach(cb => cb(isTracing)); + }, + }; + + expect(TracingStateObserver.isTracing()).toBe(true); + }); + + it('subscribes when native global is installed after module load', () => { + const TracingStateObserver = require('../TracingStateObserver').default; + + const mockSubscribers = new Set<(isTracing: boolean) => void>(); + global.__TRACING_STATE_OBSERVER__ = { + isTracing: false, + subscribers: mockSubscribers, + onTracingStateChange: (isTracing: boolean) => { + global.__TRACING_STATE_OBSERVER__.isTracing = isTracing; + mockSubscribers.forEach(cb => cb(isTracing)); + }, + }; + + const callback = jest.fn(); + TracingStateObserver.subscribe(callback); + + expect(mockSubscribers.has(callback)).toBe(true); + + global.__TRACING_STATE_OBSERVER__.onTracingStateChange(true); + expect(callback).toHaveBeenCalledWith(true); + }); + }); +});