Skip to content

Commit d5bba18

Browse files
authored
fix[react-devtools]: record timeline data only when supported (#31154)
Stacked on #31132. See last commit. There are 2 issues: 1. We've been recording timeline events, even if Timeline Profiler was not supported by the Host. We've been doing this for React Native, for example, which would significantly regress perf of recording a profiling session, but we were not even using this data. 2. Currently, we are generating component stack for every state update event. This is extremely expensive, and we should not be doing this. We can't currently fix the second one, because we would still need to generate all these stacks, and this would still take quite a lot of time. As of right now, we can't generate a component stack lazily without relying on the fact that reference to the Fiber is not stale. With `enableOwnerStacks` we could populate component stacks in some collection, which would be cached at the Backend, and then returned only once Frontend asks for it. This approach also eliminates the need for keeping a reference to a Fiber.
1 parent bfe91fb commit d5bba18

File tree

10 files changed

+126
-62
lines changed

10 files changed

+126
-62
lines changed

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,17 @@ export default class Agent extends EventEmitter<{
153153
_persistedSelection: PersistedSelection | null = null;
154154
_persistedSelectionMatch: PathMatch | null = null;
155155
_traceUpdatesEnabled: boolean = false;
156-
_onReloadAndProfile: ((recordChangeDescriptions: boolean) => void) | void;
156+
_onReloadAndProfile:
157+
| ((recordChangeDescriptions: boolean, recordTimeline: boolean) => void)
158+
| void;
157159

158160
constructor(
159161
bridge: BackendBridge,
160162
isProfiling: boolean = false,
161-
onReloadAndProfile?: (recordChangeDescriptions: boolean) => void,
163+
onReloadAndProfile?: (
164+
recordChangeDescriptions: boolean,
165+
recordTimeline: boolean,
166+
) => void,
162167
) {
163168
super();
164169

@@ -658,17 +663,19 @@ export default class Agent extends EventEmitter<{
658663
this._bridge.send('isReloadAndProfileSupportedByBackend', true);
659664
};
660665

661-
reloadAndProfile: (recordChangeDescriptions: boolean) => void =
662-
recordChangeDescriptions => {
663-
if (typeof this._onReloadAndProfile === 'function') {
664-
this._onReloadAndProfile(recordChangeDescriptions);
665-
}
666+
reloadAndProfile: ({
667+
recordChangeDescriptions: boolean,
668+
recordTimeline: boolean,
669+
}) => void = ({recordChangeDescriptions, recordTimeline}) => {
670+
if (typeof this._onReloadAndProfile === 'function') {
671+
this._onReloadAndProfile(recordChangeDescriptions, recordTimeline);
672+
}
666673

667-
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
668-
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
669-
// The agent can't do this in a way that is renderer agnostic.
670-
this._bridge.send('reloadAppForProfiling');
671-
};
674+
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
675+
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
676+
// The agent can't do this in a way that is renderer agnostic.
677+
this._bridge.send('reloadAppForProfiling');
678+
};
672679

673680
renamePath: RenamePathParams => void = ({
674681
hookID,
@@ -740,17 +747,19 @@ export default class Agent extends EventEmitter<{
740747
this.removeAllListeners();
741748
};
742749

743-
startProfiling: (recordChangeDescriptions: boolean) => void =
744-
recordChangeDescriptions => {
745-
this._isProfiling = true;
746-
for (const rendererID in this._rendererInterfaces) {
747-
const renderer = ((this._rendererInterfaces[
748-
(rendererID: any)
749-
]: any): RendererInterface);
750-
renderer.startProfiling(recordChangeDescriptions);
751-
}
752-
this._bridge.send('profilingStatus', this._isProfiling);
753-
};
750+
startProfiling: ({
751+
recordChangeDescriptions: boolean,
752+
recordTimeline: boolean,
753+
}) => void = ({recordChangeDescriptions, recordTimeline}) => {
754+
this._isProfiling = true;
755+
for (const rendererID in this._rendererInterfaces) {
756+
const renderer = ((this._rendererInterfaces[
757+
(rendererID: any)
758+
]: any): RendererInterface);
759+
renderer.startProfiling(recordChangeDescriptions, recordTimeline);
760+
}
761+
this._bridge.send('profilingStatus', this._isProfiling);
762+
};
754763

755764
stopProfiling: () => void = () => {
756765
this._isProfiling = false;

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5035,6 +5035,7 @@ export function attach(
50355035
let isProfiling: boolean = false;
50365036
let profilingStartTime: number = 0;
50375037
let recordChangeDescriptions: boolean = false;
5038+
let recordTimeline: boolean = false;
50385039
let rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null =
50395040
null;
50405041

@@ -5176,12 +5177,16 @@ export function attach(
51765177
}
51775178
}
51785179

5179-
function startProfiling(shouldRecordChangeDescriptions: boolean) {
5180+
function startProfiling(
5181+
shouldRecordChangeDescriptions: boolean,
5182+
shouldRecordTimeline: boolean,
5183+
) {
51805184
if (isProfiling) {
51815185
return;
51825186
}
51835187

51845188
recordChangeDescriptions = shouldRecordChangeDescriptions;
5189+
recordTimeline = shouldRecordTimeline;
51855190

51865191
// Capture initial values as of the time profiling starts.
51875192
// It's important we snapshot both the durations and the id-to-root map,
@@ -5212,7 +5217,7 @@ export function attach(
52125217
rootToCommitProfilingMetadataMap = new Map();
52135218

52145219
if (toggleProfilingStatus !== null) {
5215-
toggleProfilingStatus(true);
5220+
toggleProfilingStatus(true, recordTimeline);
52165221
}
52175222
}
52185223

@@ -5221,13 +5226,18 @@ export function attach(
52215226
recordChangeDescriptions = false;
52225227

52235228
if (toggleProfilingStatus !== null) {
5224-
toggleProfilingStatus(false);
5229+
toggleProfilingStatus(false, recordTimeline);
52255230
}
5231+
5232+
recordTimeline = false;
52265233
}
52275234

52285235
// Automatically start profiling so that we don't miss timing info from initial "mount".
52295236
if (shouldStartProfilingNow) {
5230-
startProfiling(profilingSettings.recordChangeDescriptions);
5237+
startProfiling(
5238+
profilingSettings.recordChangeDescriptions,
5239+
profilingSettings.recordTimeline,
5240+
);
52315241
}
52325242

52335243
function getNearestFiber(devtoolsInstance: DevToolsInstance): null | Fiber {

packages/react-devtools-shared/src/backend/profilingHooks.js

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ export function setPerformanceMock_ONLY_FOR_TESTING(
9797
}
9898

9999
export type GetTimelineData = () => TimelineData | null;
100-
export type ToggleProfilingStatus = (value: boolean) => void;
100+
export type ToggleProfilingStatus = (
101+
value: boolean,
102+
recordTimeline?: boolean,
103+
) => void;
101104

102105
type Response = {
103106
getTimelineData: GetTimelineData,
@@ -839,7 +842,10 @@ export function createProfilingHooks({
839842
}
840843
}
841844

842-
function toggleProfilingStatus(value: boolean) {
845+
function toggleProfilingStatus(
846+
value: boolean,
847+
recordTimeline: boolean = false,
848+
) {
843849
if (isProfiling !== value) {
844850
isProfiling = value;
845851

@@ -875,34 +881,45 @@ export function createProfilingHooks({
875881
currentReactComponentMeasure = null;
876882
currentReactMeasuresStack = [];
877883
currentFiberStacks = new Map();
878-
currentTimelineData = {
879-
// Session wide metadata; only collected once.
880-
internalModuleSourceToRanges,
881-
laneToLabelMap: laneToLabelMap || new Map(),
882-
reactVersion,
883-
884-
// Data logged by React during profiling session.
885-
componentMeasures: [],
886-
schedulingEvents: [],
887-
suspenseEvents: [],
888-
thrownErrors: [],
889-
890-
// Data inferred based on what React logs.
891-
batchUIDToMeasuresMap: new Map(),
892-
duration: 0,
893-
laneToReactMeasureMap,
894-
startTime: 0,
895-
896-
// Data only available in Chrome profiles.
897-
flamechart: [],
898-
nativeEvents: [],
899-
networkMeasures: [],
900-
otherUserTimingMarks: [],
901-
snapshots: [],
902-
snapshotHeight: 0,
903-
};
884+
if (recordTimeline) {
885+
currentTimelineData = {
886+
// Session wide metadata; only collected once.
887+
internalModuleSourceToRanges,
888+
laneToLabelMap: laneToLabelMap || new Map(),
889+
reactVersion,
890+
891+
// Data logged by React during profiling session.
892+
componentMeasures: [],
893+
schedulingEvents: [],
894+
suspenseEvents: [],
895+
thrownErrors: [],
896+
897+
// Data inferred based on what React logs.
898+
batchUIDToMeasuresMap: new Map(),
899+
duration: 0,
900+
laneToReactMeasureMap,
901+
startTime: 0,
902+
903+
// Data only available in Chrome profiles.
904+
flamechart: [],
905+
nativeEvents: [],
906+
networkMeasures: [],
907+
otherUserTimingMarks: [],
908+
snapshots: [],
909+
snapshotHeight: 0,
910+
};
911+
}
904912
nextRenderShouldStartNewBatch = true;
905913
} else {
914+
// This is __EXPENSIVE__.
915+
// We could end up with hundreds of state updated, and for each one of them
916+
// would try to create a component stack with possibly hundreds of Fibers.
917+
// Creating a cache of component stacks won't help, generating a single stack is already expensive enough.
918+
// We should find a way to lazily generate component stacks on demand, when user inspects a specific event.
919+
// If we succeed with moving React DevTools Timeline Profiler to Performance panel, then Timeline Profiler would probably be removed.
920+
// If not, then once enableOwnerStacks is adopted, revisit this again and cache component stacks per Fiber,
921+
// but only return them when needed, sending hundreds of component stacks is beyond the Bridge's bandwidth.
922+
906923
// Postprocess Profile data
907924
if (currentTimelineData !== null) {
908925
currentTimelineData.schedulingEvents.forEach(event => {

packages/react-devtools-shared/src/backend/types.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,10 @@ export type RendererInterface = {
419419
renderer: ReactRenderer | null,
420420
setTraceUpdatesEnabled: (enabled: boolean) => void,
421421
setTrackedPath: (path: Array<PathFrame> | null) => void,
422-
startProfiling: (recordChangeDescriptions: boolean) => void,
422+
startProfiling: (
423+
recordChangeDescriptions: boolean,
424+
recordTimeline: boolean,
425+
) => void,
423426
stopProfiling: () => void,
424427
storeAsGlobal: (
425428
id: number,
@@ -487,6 +490,7 @@ export type DevToolsBackend = {
487490

488491
export type ProfilingSettings = {
489492
recordChangeDescriptions: boolean,
493+
recordTimeline: boolean,
490494
};
491495

492496
export type DevToolsHook = {

packages/react-devtools-shared/src/bridge.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
ProfilingDataBackend,
1717
RendererID,
1818
DevToolsHookSettings,
19+
ProfilingSettings,
1920
} from 'react-devtools-shared/src/backend/types';
2021
import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
2122

@@ -206,6 +207,9 @@ export type BackendEvents = {
206207
hookSettings: [$ReadOnly<DevToolsHookSettings>],
207208
};
208209

210+
type StartProfilingParams = ProfilingSettings;
211+
type ReloadAndProfilingParams = ProfilingSettings;
212+
209213
type FrontendEvents = {
210214
clearErrorsAndWarnings: [{rendererID: RendererID}],
211215
clearErrorsForElementID: [ElementAndRendererID],
@@ -226,13 +230,13 @@ type FrontendEvents = {
226230
overrideSuspense: [OverrideSuspense],
227231
overrideValueAtPath: [OverrideValueAtPath],
228232
profilingData: [ProfilingDataBackend],
229-
reloadAndProfile: [boolean],
233+
reloadAndProfile: [ReloadAndProfilingParams],
230234
renamePath: [RenamePath],
231235
savedPreferences: [SavedPreferencesParams],
232236
setTraceUpdatesEnabled: [boolean],
233237
shutdown: [],
234238
startInspectingHost: [],
235-
startProfiling: [boolean],
239+
startProfiling: [StartProfilingParams],
236240
stopInspectingHost: [boolean],
237241
stopProfiling: [],
238242
storeAsGlobal: [StoreAsGlobalParams],

packages/react-devtools-shared/src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY =
4141
'React::DevTools::parseHookNames';
4242
export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
4343
'React::DevTools::recordChangeDescriptions';
44+
export const SESSION_STORAGE_RECORD_TIMELINE_KEY =
45+
'React::DevTools::recordTimeline';
4446
export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
4547
'React::DevTools::reloadAndProfile';
4648
export const LOCAL_STORAGE_BROWSER_THEME = 'React::DevTools::theme';

packages/react-devtools-shared/src/devtools/ProfilerStore.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,10 @@ export default class ProfilerStore extends EventEmitter<{
191191
}
192192

193193
startProfiling(): void {
194-
this._bridge.send('startProfiling', this._store.recordChangeDescriptions);
194+
this._bridge.send('startProfiling', {
195+
recordChangeDescriptions: this._store.recordChangeDescriptions,
196+
recordTimeline: this._store.supportsTimeline,
197+
});
195198

196199
this._isProfilingBasedOnUserInput = true;
197200
this.emit('isProfiling');

packages/react-devtools-shared/src/devtools/views/Profiler/ReloadAndProfileButton.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ export default function ReloadAndProfileButton({
5454
// For now, let's just skip doing it entirely to avoid paying snapshot costs for data we don't need.
5555
// startProfiling();
5656

57-
bridge.send('reloadAndProfile', recordChangeDescriptions);
58-
}, [bridge, recordChangeDescriptions]);
57+
bridge.send('reloadAndProfile', {
58+
recordChangeDescriptions,
59+
recordTimeline: store.supportsTimeline,
60+
});
61+
}, [bridge, recordChangeDescriptions, store]);
5962

6063
if (!supportsReloadAndProfile) {
6164
return null;

packages/react-devtools-shared/src/hook.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const targetConsole: Object = console;
5252

5353
const defaultProfilingSettings: ProfilingSettings = {
5454
recordChangeDescriptions: false,
55+
recordTimeline: false,
5556
};
5657

5758
export function installHook(

packages/react-devtools-shared/src/utils.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
3939
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
4040
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
41+
SESSION_STORAGE_RECORD_TIMELINE_KEY,
4142
} from './constants';
4243
import {
4344
ComponentFilterElementType,
@@ -1002,18 +1003,28 @@ export function getProfilingSettings(): ProfilingSettings {
10021003
recordChangeDescriptions:
10031004
sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
10041005
'true',
1006+
recordTimeline:
1007+
sessionStorageGetItem(SESSION_STORAGE_RECORD_TIMELINE_KEY) === 'true',
10051008
};
10061009
}
10071010

1008-
export function onReloadAndProfile(recordChangeDescriptions: boolean): void {
1011+
export function onReloadAndProfile(
1012+
recordChangeDescriptions: boolean,
1013+
recordTimeline: boolean,
1014+
): void {
10091015
sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
10101016
sessionStorageSetItem(
10111017
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
10121018
recordChangeDescriptions ? 'true' : 'false',
10131019
);
1020+
sessionStorageSetItem(
1021+
SESSION_STORAGE_RECORD_TIMELINE_KEY,
1022+
recordTimeline ? 'true' : 'false',
1023+
);
10141024
}
10151025

10161026
export function onReloadAndProfileFlagsReset(): void {
10171027
sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
10181028
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
1029+
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_TIMELINE_KEY);
10191030
}

0 commit comments

Comments
 (0)