Skip to content

Commit d9b913b

Browse files
committed
fix[react-devtools]: record timeline data only when supported
1 parent 7a49d46 commit d9b913b

11 files changed

Lines changed: 126 additions & 63 deletions

File tree

packages/react-devtools-core/src/backend.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,26 @@ function debug(methodName: string, ...args: Array<mixed>) {
6060
}
6161
}
6262

63+
type ReactNativeProfilingSettings = {
64+
recordChangeDescriptions: boolean,
65+
};
6366
export function initialize(
6467
maybeSettingsOrSettingsPromise?:
6568
| DevToolsHookSettings
6669
| Promise<DevToolsHookSettings>,
6770
shouldStartProfilingRightNow: boolean = false,
68-
profilingSettings?: ProfilingSettings,
71+
profilingSettings?: ReactNativeProfilingSettings,
6972
) {
73+
const mergedProfilingSettings: ProfilingSettings | void =
74+
typeof profilingSettings === 'object' && profilingSettings != null
75+
? {...profilingSettings, recordTimeline: false}
76+
: undefined;
77+
7078
installHook(
7179
window,
7280
maybeSettingsOrSettingsPromise,
7381
shouldStartProfilingRightNow,
74-
profilingSettings,
82+
mergedProfilingSettings,
7583
);
7684
}
7785

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from 'react-devtools-shared/src/storage';
99
import {
1010
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
11+
SESSION_STORAGE_RECORD_TIMELINE_KEY,
1112
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
1213
} from 'react-devtools-shared/src/constants';
1314
import type {ProfilingSettings} from 'react-devtools-shared/src/backend/types';
@@ -46,18 +47,28 @@ export function getProfilingSettings(): ProfilingSettings {
4647
recordChangeDescriptions:
4748
sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
4849
'true',
50+
recordTimeline:
51+
sessionStorageGetItem(SESSION_STORAGE_RECORD_TIMELINE_KEY) === 'true',
4952
};
5053
}
5154

52-
export function onReloadAndProfile(recordChangeDescriptions: boolean): void {
55+
export function onReloadAndProfile(
56+
recordChangeDescriptions: boolean,
57+
recordTimeline: boolean,
58+
): void {
5359
sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
5460
sessionStorageSetItem(
5561
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
5662
recordChangeDescriptions ? 'true' : 'false',
5763
);
64+
sessionStorageSetItem(
65+
SESSION_STORAGE_RECORD_TIMELINE_KEY,
66+
recordTimeline ? 'true' : 'false',
67+
);
5868
}
5969

6070
export function onReloadAndProfileFlagsReset(): void {
6171
sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
6272
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
73+
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_TIMELINE_KEY);
6374
}

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

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ 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,
@@ -658,17 +660,19 @@ export default class Agent extends EventEmitter<{
658660
this._bridge.send('isReloadAndProfileSupportedByBackend', true);
659661
};
660662

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

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-
};
671+
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
672+
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
673+
// The agent can't do this in a way that is renderer agnostic.
674+
this._bridge.send('reloadAppForProfiling');
675+
};
672676

673677
renamePath: RenamePathParams => void = ({
674678
hookID,
@@ -737,17 +741,19 @@ export default class Agent extends EventEmitter<{
737741
this.emit('shutdown');
738742
};
739743

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

752758
stopProfiling: () => void = () => {
753759
this._isProfiling = false;

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5176,7 +5176,10 @@ export function attach(
51765176
}
51775177
}
51785178

5179-
function startProfiling(shouldRecordChangeDescriptions: boolean) {
5179+
function startProfiling(
5180+
shouldRecordChangeDescriptions: boolean,
5181+
recordTimeline: boolean,
5182+
) {
51805183
if (isProfiling) {
51815184
return;
51825185
}
@@ -5212,7 +5215,7 @@ export function attach(
52125215
rootToCommitProfilingMetadataMap = new Map();
52135216

52145217
if (toggleProfilingStatus !== null) {
5215-
toggleProfilingStatus(true);
5218+
toggleProfilingStatus(true, profilingSettings.recordTimeline);
52165219
}
52175220
}
52185221

@@ -5221,13 +5224,16 @@ export function attach(
52215224
recordChangeDescriptions = false;
52225225

52235226
if (toggleProfilingStatus !== null) {
5224-
toggleProfilingStatus(false);
5227+
toggleProfilingStatus(false, profilingSettings.recordTimeline);
52255228
}
52265229
}
52275230

52285231
// Automatically start profiling so that we don't miss timing info from initial "mount".
52295232
if (shouldStartProfilingRightNow) {
5230-
startProfiling(profilingSettings.recordChangeDescriptions);
5233+
startProfiling(
5234+
profilingSettings.recordChangeDescriptions,
5235+
profilingSettings.recordTimeline,
5236+
);
52315237
}
52325238

52335239
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ type FrontendEvents = {
226226
overrideSuspense: [OverrideSuspense],
227227
overrideValueAtPath: [OverrideValueAtPath],
228228
profilingData: [ProfilingDataBackend],
229-
reloadAndProfile: [boolean],
229+
reloadAndProfile: [boolean, boolean],
230230
renamePath: [RenamePath],
231231
savedPreferences: [SavedPreferencesParams],
232232
setTraceUpdatesEnabled: [boolean],
233233
shutdown: [],
234234
startInspectingHost: [],
235-
startProfiling: [boolean],
235+
startProfiling: [boolean, boolean],
236236
stopInspectingHost: [boolean],
237237
stopProfiling: [],
238238
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ export default class ProfilerStore extends EventEmitter<{
191191
}
192192

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

196200
this._isProfilingBasedOnUserInput = true;
197201
this.emit('isProfiling');

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ 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(
58+
'reloadAndProfile',
59+
recordChangeDescriptions,
60+
store.supportsTimeline,
61+
);
62+
}, [bridge, recordChangeDescriptions, store]);
5963

6064
if (!supportsReloadAndProfile) {
6165
return null;

0 commit comments

Comments
 (0)