Skip to content

Commit 2b2d55e

Browse files
committed
fix[react-devtools]: record timeline data only when supported
1 parent bfe91fb commit 2b2d55e

10 files changed

Lines changed: 126 additions & 62 deletions

File tree

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)