Skip to content

Commit a308014

Browse files
emily8rownmeta-codesync[bot]
authored andcommitted
Suppress LogBox during performance tracing (#55470)
Summary: Pull Request resolved: #55470 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 fbshipit-source-id: 975e042766194d9702be2c9e222fd452bb99d841
1 parent a08b2d2 commit a308014

10 files changed

Lines changed: 428 additions & 5 deletions

File tree

packages/react-native/Libraries/LogBox/Data/LogBoxData.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {Stack} from './LogBoxSymbolication';
1414
import type {Category, ExtendedExceptionData, Message} from './parseLogBoxLog';
1515

1616
import DebuggerSessionObserver from '../../../src/private/devsupport/rndevtools/FuseboxSessionObserver';
17+
import TracingStateObserver from '../../../src/private/devsupport/rndevtools/TracingStateObserver';
1718
import toExtendedError from '../../../src/private/utilities/toExtendedError';
1819
import parseErrorStack from '../../Core/Devtools/parseErrorStack';
1920
import NativeLogBox from '../../NativeModules/specs/NativeLogBox';
@@ -71,6 +72,7 @@ let _isDisabled = false;
7172
let _selectedIndex = -1;
7273
let hasShownFuseboxWarningsMigrationMessage = false;
7374
let hostTargetSessionObserverSubscription = null;
75+
let tracingStateObserverSubscription = null;
7476

7577
let warningFilter: WarningFilter = function (format) {
7678
return {
@@ -205,6 +207,20 @@ export function addLog(log: LogData): void {
205207
);
206208
}
207209

210+
if (tracingStateObserverSubscription == null) {
211+
tracingStateObserverSubscription = TracingStateObserver.subscribe(
212+
isTracing => {
213+
if (isTracing) {
214+
clear();
215+
}
216+
},
217+
);
218+
}
219+
220+
if (TracingStateObserver.isTracing()) {
221+
return;
222+
}
223+
208224
// If Host has Fusebox support
209225
if (log.level === 'warn' && global.__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__) {
210226
// And there is no active debugging session
@@ -241,6 +257,10 @@ export function addLog(log: LogData): void {
241257
}
242258

243259
export function addException(error: ExtendedExceptionData): void {
260+
if (TracingStateObserver.isTracing()) {
261+
return;
262+
}
263+
244264
// Parsing logs are expensive so we schedule this
245265
// otherwise spammy logs would pause rendering.
246266
setImmediate(() => {

packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxData-test.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,4 +721,137 @@ describe('LogBoxData', () => {
721721
LogBoxData.setAppInfo(() => info);
722722
expect(LogBoxData.getAppInfo()).toBe(info);
723723
});
724+
725+
describe('Performance tracing suppression', () => {
726+
let mockIsTracing: () => boolean;
727+
let mockSubscribeCallback: ((isTracing: boolean) => void) | null = null;
728+
729+
beforeEach(() => {
730+
mockIsTracing = jest.fn(() => false);
731+
mockSubscribeCallback = null;
732+
733+
jest.doMock(
734+
'../../../../src/private/devsupport/rndevtools/TracingStateObserver',
735+
() => ({
736+
__esModule: true,
737+
default: {
738+
isTracing: () => mockIsTracing(),
739+
subscribe: (callback: (isTracing: boolean) => void) => {
740+
mockSubscribeCallback = callback;
741+
return () => {
742+
mockSubscribeCallback = null;
743+
};
744+
},
745+
},
746+
}),
747+
);
748+
749+
jest.resetModules();
750+
});
751+
752+
afterEach(() => {
753+
jest.unmock(
754+
'../../../../src/private/devsupport/rndevtools/TracingStateObserver',
755+
);
756+
});
757+
758+
it('suppresses logs when performance tracing is active', () => {
759+
const LogBoxDataWithMock = require('../LogBoxData');
760+
761+
LogBoxDataWithMock.addLog({
762+
level: 'warn',
763+
message: {content: 'Before tracing', substitutions: []},
764+
category: 'before-tracing',
765+
componentStack: [],
766+
});
767+
jest.runOnlyPendingTimers();
768+
769+
const observerBefore = jest.fn();
770+
LogBoxDataWithMock.observe(observerBefore).unsubscribe();
771+
expect(Array.from(observerBefore.mock.calls[0][0].logs).length).toBe(1);
772+
773+
mockIsTracing = jest.fn(() => true);
774+
775+
LogBoxDataWithMock.addLog({
776+
level: 'warn',
777+
message: {content: 'During tracing', substitutions: []},
778+
category: 'during-tracing',
779+
componentStack: [],
780+
});
781+
jest.runOnlyPendingTimers();
782+
783+
const observerDuring = jest.fn();
784+
LogBoxDataWithMock.observe(observerDuring).unsubscribe();
785+
expect(Array.from(observerDuring.mock.calls[0][0].logs).length).toBe(1);
786+
});
787+
788+
it('suppresses exceptions when performance tracing is active', () => {
789+
const LogBoxDataWithMock = require('../LogBoxData');
790+
791+
LogBoxDataWithMock.addException({
792+
message: 'Before tracing exception',
793+
isComponentError: false,
794+
originalMessage: 'Before tracing exception',
795+
name: 'console.error',
796+
componentStack: '',
797+
stack: [],
798+
id: 0,
799+
isFatal: false,
800+
});
801+
jest.runOnlyPendingTimers();
802+
803+
const observerBefore = jest.fn();
804+
LogBoxDataWithMock.observe(observerBefore).unsubscribe();
805+
expect(Array.from(observerBefore.mock.calls[0][0].logs).length).toBe(1);
806+
807+
mockIsTracing = jest.fn(() => true);
808+
809+
LogBoxDataWithMock.addException({
810+
message: 'During tracing exception',
811+
isComponentError: false,
812+
originalMessage: 'During tracing exception',
813+
name: 'console.error',
814+
componentStack: '',
815+
stack: [],
816+
id: 1,
817+
isFatal: false,
818+
});
819+
jest.runOnlyPendingTimers();
820+
821+
const observerDuring = jest.fn();
822+
LogBoxDataWithMock.observe(observerDuring).unsubscribe();
823+
expect(Array.from(observerDuring.mock.calls[0][0].logs).length).toBe(1);
824+
});
825+
826+
it('clears logs when tracing starts', () => {
827+
const LogBoxDataWithMock = require('../LogBoxData');
828+
829+
LogBoxDataWithMock.addLog({
830+
level: 'warn',
831+
message: {content: 'Log 1', substitutions: []},
832+
category: 'log-1',
833+
componentStack: [],
834+
});
835+
LogBoxDataWithMock.addLog({
836+
level: 'warn',
837+
message: {content: 'Log 2', substitutions: []},
838+
category: 'log-2',
839+
componentStack: [],
840+
});
841+
jest.runOnlyPendingTimers();
842+
843+
const observerBefore = jest.fn();
844+
LogBoxDataWithMock.observe(observerBefore).unsubscribe();
845+
expect(Array.from(observerBefore.mock.calls[0][0].logs).length).toBe(2);
846+
847+
if (mockSubscribeCallback) {
848+
mockSubscribeCallback(true);
849+
}
850+
jest.runOnlyPendingTimers();
851+
852+
const observerAfter = jest.fn();
853+
LogBoxDataWithMock.observe(observerAfter).unsubscribe();
854+
expect(Array.from(observerAfter.mock.calls[0][0].logs).length).toBe(0);
855+
});
856+
});
724857
});

packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,15 @@ RuntimeTracingAgent::RuntimeTracingAgent(
155155
if (state.enabledCategories.contains(tracing::Category::JavaScriptSampling)) {
156156
targetController_.enableSamplingProfiler();
157157
}
158+
if (state.mode == tracing::Mode::CDP) {
159+
targetController_.emitTracingStateChange(true);
160+
}
158161
}
159162

160163
RuntimeTracingAgent::~RuntimeTracingAgent() {
164+
if (state_.mode == tracing::Mode::CDP) {
165+
targetController_.emitTracingStateChange(false);
166+
}
161167
if (state_.enabledCategories.contains(
162168
tracing::Category::JavaScriptSampling)) {
163169
targetController_.disableSamplingProfiler();

packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
#include <jsinspector-modern/RuntimeTarget.h>
1111
#include <jsinspector-modern/RuntimeTargetGlobalStateObserver.h>
12-
#include <jsinspector-modern/tracing/PerformanceTracer.h>
12+
#include <jsinspector-modern/RuntimeTargetTracingStateObserver.h>
1313

1414
#include <utility>
1515

@@ -43,6 +43,7 @@ void RuntimeTarget::installGlobals() {
4343
// NOTE: RuntimeTarget::installDebuggerSessionObserver is in
4444
// RuntimeTargetDebuggerSessionObserver.cpp
4545
installDebuggerSessionObserver();
46+
installTracingStateObserver();
4647
// NOTE: RuntimeTarget::installNetworkReporterAPI is in
4748
// RuntimeTargetNetwork.cpp
4849
installNetworkReporterAPI();
@@ -157,6 +158,23 @@ void RuntimeTarget::emitDebuggerSessionDestroyed() {
157158
});
158159
}
159160

161+
void RuntimeTarget::installTracingStateObserver() {
162+
jsExecutor_([](jsi::Runtime& runtime) {
163+
jsinspector_modern::installTracingStateObserver(runtime);
164+
});
165+
}
166+
167+
void RuntimeTarget::emitTracingStateChange(bool isTracing) {
168+
jsExecutor_([isTracing](jsi::Runtime& runtime) {
169+
try {
170+
emitTracingStateObserverChange(runtime, isTracing);
171+
} catch (jsi::JSError&) {
172+
// Suppress any errors, they should not be visible to the user
173+
// and should not affect runtime.
174+
}
175+
});
176+
}
177+
160178
void RuntimeTarget::enableSamplingProfiler() {
161179
delegate_.enableSamplingProfiler();
162180
}
@@ -275,6 +293,10 @@ RuntimeTargetController::collectSamplingProfile() {
275293
return target_.collectSamplingProfile();
276294
}
277295

296+
void RuntimeTargetController::emitTracingStateChange(bool isTracing) {
297+
target_.emitTracingStateChange(isTracing);
298+
}
299+
278300
void RuntimeTargetController::notifyDomainStateChanged(
279301
Domain domain,
280302
bool enabled,

packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <jsinspector-modern/tracing/TraceRecordingState.h>
2222

2323
#include <memory>
24+
#include <optional>
2425
#include <utility>
2526

2627
#ifndef JSINSPECTOR_EXPORT
@@ -147,6 +148,11 @@ class RuntimeTargetController {
147148
*/
148149
tracing::RuntimeSamplingProfile collectSamplingProfile();
149150

151+
/**
152+
* Emits a tracing state change to JavaScript via the tracing state observer.
153+
*/
154+
void emitTracingStateChange(bool isTracing);
155+
150156
private:
151157
RuntimeTarget &target_;
152158
};
@@ -262,6 +268,7 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis<RuntimeTa
262268
* session - HostTargetTraceRecording.
263269
*/
264270
std::weak_ptr<RuntimeTracingAgent> tracingAgent_;
271+
265272
/**
266273
* Start sampling profiler for a particular JavaScript runtime.
267274
*/
@@ -304,6 +311,13 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis<RuntimeTa
304311
*/
305312
void installDebuggerSessionObserver();
306313

314+
/**
315+
* Installs __TRACING_STATE_OBSERVER__ object on the JavaScript's global
316+
* object, which can be referenced from JavaScript side for determining the
317+
* status of performance tracing.
318+
*/
319+
void installTracingStateObserver();
320+
307321
/**
308322
* Installs the private __NETWORK_REPORTER__ object on the Runtime's
309323
* global object.
@@ -322,6 +336,11 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis<RuntimeTa
322336
*/
323337
void emitDebuggerSessionDestroyed();
324338

339+
/**
340+
* Emits a tracing state change to JavaScript via the tracing state observer.
341+
*/
342+
void emitTracingStateChange(bool isTracing);
343+
325344
/**
326345
* \returns a globally unique ID for a network request.
327346
* May be called from any thread as long as the RuntimeTarget is valid.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
8+
#include "RuntimeTargetTracingStateObserver.h"
9+
10+
#include <jsinspector-modern/RuntimeTargetGlobalStateObserver.h>
11+
12+
namespace facebook::react::jsinspector_modern {
13+
14+
void installTracingStateObserver(jsi::Runtime& runtime) {
15+
installGlobalStateObserver(
16+
runtime,
17+
"__TRACING_STATE_OBSERVER__",
18+
"isTracing",
19+
"onTracingStateChange");
20+
}
21+
22+
void emitTracingStateObserverChange(jsi::Runtime& runtime, bool isTracing) {
23+
emitGlobalStateObserverChange(
24+
runtime, "__TRACING_STATE_OBSERVER__", "onTracingStateChange", isTracing);
25+
}
26+
27+
} // namespace facebook::react::jsinspector_modern
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
8+
#pragma once
9+
10+
#include <jsi/jsi.h>
11+
12+
namespace facebook::react::jsinspector_modern {
13+
14+
/**
15+
* Installs __TRACING_STATE_OBSERVER__ object on the JavaScript's global
16+
* object, which can be referenced from JavaScript side for determining the
17+
* status of performance tracing.
18+
*/
19+
void installTracingStateObserver(jsi::Runtime &runtime);
20+
21+
/**
22+
* Emits the tracing state change to JavaScript by calling onTracingStateChange
23+
* on __TRACING_STATE_OBSERVER__.
24+
*/
25+
void emitTracingStateObserverChange(jsi::Runtime &runtime, bool isTracing);
26+
27+
} // namespace facebook::react::jsinspector_modern

packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,28 @@
2121
* This class provides a JS-friendly API over that global object.
2222
*/
2323
class GlobalStateObserver {
24-
#hasNativeSupport: boolean;
2524
#globalName: string;
2625
#statusProperty: string;
2726

2827
constructor(globalName: string, statusProperty: string) {
2928
this.#globalName = globalName;
3029
this.#statusProperty = statusProperty;
31-
this.#hasNativeSupport = global.hasOwnProperty(globalName);
30+
}
31+
32+
#hasNativeSupport(): boolean {
33+
return global.hasOwnProperty(this.#globalName);
3234
}
3335

3436
getStatus(): boolean {
35-
if (!this.#hasNativeSupport) {
37+
if (!this.#hasNativeSupport()) {
3638
return false;
3739
}
3840

3941
return global[this.#globalName][this.#statusProperty];
4042
}
4143

4244
subscribe(callback: (status: boolean) => void): () => void {
43-
if (!this.#hasNativeSupport) {
45+
if (!this.#hasNativeSupport()) {
4446
return () => {};
4547
}
4648

0 commit comments

Comments
 (0)