Skip to content

Commit 664d65b

Browse files
author
Brian Vaughn
committed
Merge legacy profiler and timeline data. Record timeline data in memory.
1 parent 05c655f commit 664d65b

33 files changed

+1048
-404
lines changed

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

Lines changed: 527 additions & 66 deletions
Large diffs are not rendered by default.

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';
9696
import {getStyleXData} from './StyleX/utils';
9797
import {createProfilingHooks} from './profilingHooks';
9898

99-
import type {ToggleProfilingStatus} from './profilingHooks';
99+
import type {GetTimelineData, ToggleProfilingStatus} from './profilingHooks';
100100
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
101101
import type {
102102
ChangeDescription,
@@ -630,6 +630,7 @@ export function attach(
630630
};
631631
}
632632

633+
let getTimelineData: null | GetTimelineData = null;
633634
let toggleProfilingStatus: null | ToggleProfilingStatus = null;
634635
if (typeof injectProfilingHooks === 'function') {
635636
const response = createProfilingHooks({
@@ -643,6 +644,7 @@ export function attach(
643644
injectProfilingHooks(response.profilingHooks);
644645

645646
// Hang onto this toggle so we can notify the external methods of profiling status changes.
647+
getTimelineData = response.getTimelineData;
646648
toggleProfilingStatus = response.toggleProfilingStatus;
647649
}
648650

@@ -3978,9 +3980,39 @@ export function attach(
39783980
},
39793981
);
39803982

3983+
let timelineData = null;
3984+
if (typeof getTimelineData === 'function') {
3985+
const currentTimelineData = getTimelineData();
3986+
if (currentTimelineData) {
3987+
const {
3988+
batchUIDToMeasuresMap,
3989+
internalModuleSourceToRanges,
3990+
laneToLabelMap,
3991+
laneToReactMeasureMap,
3992+
...rest
3993+
} = currentTimelineData;
3994+
3995+
timelineData = {
3996+
...rest,
3997+
3998+
// Most of the data is safe to parse as-is,
3999+
// but we need to convert the nested Arrays back to Maps.
4000+
// Most of the data is safe to serialize as-is,
4001+
// but we need to convert the Maps to nested Arrays.
4002+
batchUIDToMeasuresMap: Array.from(batchUIDToMeasuresMap.entries()),
4003+
internalModuleSourceToRanges: Array.from(
4004+
internalModuleSourceToRanges.entries(),
4005+
),
4006+
laneToLabelMap: Array.from(laneToLabelMap.entries()),
4007+
laneToReactMeasureMap: Array.from(laneToReactMeasureMap.entries()),
4008+
};
4009+
}
4010+
}
4011+
39814012
return {
39824013
dataForRoots,
39834014
rendererID,
4015+
timelineData,
39844016
};
39854017
}
39864018

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
Plugins,
1717
} from 'react-devtools-shared/src/types';
1818
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
19+
import type {TimelineDataExport} from 'react-devtools-timeline/src/types';
1920

2021
type BundleType =
2122
| 0 // PROD
@@ -195,7 +196,7 @@ export type ProfilingDataForRootBackend = {|
195196
export type ProfilingDataBackend = {|
196197
dataForRoots: Array<ProfilingDataForRootBackend>,
197198
rendererID: number,
198-
// TODO (timeline) Add (optional) Timeline data.
199+
timelineData: TimelineDataExport | null,
199200
|};
200201

201202
export type PathFrame = {|

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,23 @@ export default function ClearProfilingDataButton() {
2121
const {file, setFile} = useContext(TimelineContext);
2222
const {profilerStore} = store;
2323

24-
const doesHaveLegacyData = didRecordCommits;
25-
const doesHaveTimelineData = file !== null;
24+
const doesHaveInMemoryData = didRecordCommits;
25+
const doesHaveUserTimingData = file !== null;
2626

2727
const clear = () => {
28-
if (doesHaveLegacyData) {
28+
if (doesHaveInMemoryData) {
2929
profilerStore.clear();
3030
}
31-
if (doesHaveTimelineData) {
31+
if (doesHaveUserTimingData) {
3232
setFile(null);
3333
}
3434
};
3535

3636
return (
3737
<Button
38-
disabled={isProfiling || !(doesHaveLegacyData || doesHaveTimelineData)}
38+
disabled={
39+
isProfiling || !(doesHaveInMemoryData || doesHaveUserTimingData)
40+
}
3941
onClick={clear}
4042
title="Clear profiling data">
4143
<ButtonIcon type="clear" />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
12+
import styles from './Profiler.css';
13+
14+
export default function ProcessingData() {
15+
return (
16+
<div className={styles.Column}>
17+
<div className={styles.Header}>Processing data...</div>
18+
<div className={styles.Row}>This should only take a minute.</div>
19+
</div>
20+
);
21+
}

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import ProfilingImportExportButtons from './ProfilingImportExportButtons';
2323
import SnapshotSelector from './SnapshotSelector';
2424
import SidebarCommitInfo from './SidebarCommitInfo';
2525
import NoProfilingData from './NoProfilingData';
26+
import RecordingInProgress from './RecordingInProgress';
27+
import ProcessingData from './ProcessingData';
2628
import ProfilingNotSupported from './ProfilingNotSupported';
2729
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
2830
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
@@ -46,7 +48,9 @@ function Profiler(_: {||}) {
4648
supportsProfiling,
4749
} = useContext(ProfilerContext);
4850

49-
const {searchInputContainerRef} = useContext(TimelineContext);
51+
const {file: timelineTraceEventData, searchInputContainerRef} = useContext(
52+
TimelineContext,
53+
);
5054

5155
const {supportsTimeline} = useContext(StoreContext);
5256

@@ -71,6 +75,8 @@ function Profiler(_: {||}) {
7175
view = <RecordingInProgress />;
7276
} else if (isProcessingData) {
7377
view = <ProcessingData />;
78+
} else if (timelineTraceEventData) {
79+
view = <OnlyTimelineData />;
7480
} else if (supportsProfiling) {
7581
view = <NoProfilingData />;
7682
} else {
@@ -150,6 +156,15 @@ function Profiler(_: {||}) {
150156
);
151157
}
152158

159+
const OnlyTimelineData = () => (
160+
<div className={styles.Column}>
161+
<div className={styles.Header}>Timeline only</div>
162+
<div className={styles.Row}>
163+
The current profile contains only Timeline data.
164+
</div>
165+
</div>
166+
);
167+
153168
const tabs = [
154169
{
155170
id: 'flame-chart',
@@ -175,20 +190,5 @@ const tabsWithTimeline = [
175190
title: 'Timeline',
176191
},
177192
];
178-
const ProcessingData = () => (
179-
<div className={styles.Column}>
180-
<div className={styles.Header}>Processing data...</div>
181-
<div className={styles.Row}>This should only take a minute.</div>
182-
</div>
183-
);
184-
185-
const RecordingInProgress = () => (
186-
<div className={styles.Column}>
187-
<div className={styles.Header}>Profiling is in progress...</div>
188-
<div className={styles.Row}>
189-
Click the record button <RecordToggle /> to stop recording.
190-
</div>
191-
</div>
192-
);
193193

194194
export default portaledContent(Profiler);

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {StoreContext} from '../context';
1919

2020
import type {ProfilingDataFrontend} from './types';
2121

22-
// TODO (timeline) Should this be its own context?
2322
export type TabID = 'flame-chart' | 'ranked-chart' | 'timeline';
2423

2524
export type Context = {|

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

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import {
2020
} from './utils';
2121
import {downloadFile} from '../utils';
2222
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
23+
import isArray from 'shared/isArray';
24+
import hasOwnProperty from 'shared/hasOwnProperty';
2325

2426
import styles from './ProfilingImportExportButtons.css';
2527

2628
import type {ProfilingDataExport} from './types';
2729

2830
export default function ProfilingImportExportButtons() {
29-
const {isProfiling, profilingData, rootID, selectedTabID} = useContext(
30-
ProfilerContext,
31-
);
31+
const {isProfiling, profilingData, rootID} = useContext(ProfilerContext);
3232
const {setFile} = useContext(TimelineContext);
3333
const store = useContext(StoreContext);
3434
const {profilerStore} = store;
@@ -38,6 +38,8 @@ export default function ProfilingImportExportButtons() {
3838

3939
const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
4040

41+
const doesHaveInMemoryData = profilerStore.didRecordCommits;
42+
4143
const downloadData = useCallback(() => {
4244
if (rootID === null) {
4345
return;
@@ -74,45 +76,54 @@ export default function ProfilingImportExportButtons() {
7476
}
7577
}, []);
7678

77-
const importProfilerData = useCallback(() => {
79+
// TODO (profiling) We should probably use a transition for this and suspend while loading the file.
80+
// Local files load so fast it's probably not very noticeable though.
81+
const handleChange = () => {
7882
const input = inputRef.current;
7983
if (input !== null && input.files.length > 0) {
84+
const file = input.files[0];
85+
86+
// TODO (profiling) Handle fileReader errors.
8087
const fileReader = new FileReader();
8188
fileReader.addEventListener('load', () => {
82-
try {
83-
const raw = ((fileReader.result: any): string);
84-
const profilingDataExport = ((JSON.parse(
85-
raw,
86-
): any): ProfilingDataExport);
87-
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
88-
profilingDataExport,
89-
);
90-
} catch (error) {
91-
modalDialogDispatch({
92-
id: 'ProfilingImportExportButtons',
93-
type: 'SHOW',
94-
title: 'Import failed',
95-
content: (
96-
<Fragment>
97-
<div>The profiling data you selected cannot be imported.</div>
98-
{error !== null && (
99-
<div className={styles.ErrorMessage}>{error.message}</div>
100-
)}
101-
</Fragment>
102-
),
103-
});
89+
const raw = ((fileReader.result: any): string);
90+
const json = JSON.parse(raw);
91+
92+
if (!isArray(json) && hasOwnProperty.call(json, 'version')) {
93+
// This looks like React profiling data.
94+
// But first, clear any User Timing marks; we should only have one type open at a time.
95+
setFile(null);
96+
97+
try {
98+
const profilingDataExport = ((json: any): ProfilingDataExport);
99+
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
100+
profilingDataExport,
101+
);
102+
} catch (error) {
103+
modalDialogDispatch({
104+
id: 'ProfilingImportExportButtons',
105+
type: 'SHOW',
106+
title: 'Import failed',
107+
content: (
108+
<Fragment>
109+
<div>The profiling data you selected cannot be imported.</div>
110+
{error !== null && (
111+
<div className={styles.ErrorMessage}>{error.message}</div>
112+
)}
113+
</Fragment>
114+
),
115+
});
116+
}
117+
} else {
118+
// Otherwise let's assume this is Trace Event data and pass it to the Timeline preprocessor.
119+
// But first, clear React profiling data; we should only have one type open at a time.
120+
profilerStore.clear();
121+
122+
// TODO (timeline) We shouldn't need to re-open the File but we'll need to refactor to avoid this.
123+
setFile(file);
104124
}
105125
});
106-
// TODO (profiling) Handle fileReader errors.
107-
fileReader.readAsText(input.files[0]);
108-
}
109-
}, [modalDialogDispatch, profilerStore]);
110-
111-
const importTimelineDataWrapper = event => {
112-
const input = inputRef.current;
113-
if (input !== null && input.files.length > 0) {
114-
const file = input.files[0];
115-
setFile(file);
126+
fileReader.readAsText(file);
116127
}
117128
};
118129

@@ -124,11 +135,7 @@ export default function ProfilingImportExportButtons() {
124135
className={styles.Input}
125136
type="file"
126137
accept=".json"
127-
onChange={
128-
selectedTabID === 'timeline'
129-
? importTimelineDataWrapper
130-
: importProfilerData
131-
}
138+
onChange={handleChange}
132139
tabIndex={-1}
133140
/>
134141
<a ref={downloadRef} className={styles.Input} />
@@ -139,11 +146,7 @@ export default function ProfilingImportExportButtons() {
139146
<ButtonIcon type="import" />
140147
</Button>
141148
<Button
142-
disabled={
143-
isProfiling ||
144-
!profilerStore.didRecordCommits ||
145-
selectedTabID === 'timeline'
146-
}
149+
disabled={isProfiling || !doesHaveInMemoryData}
147150
onClick={downloadData}
148151
title="Save profile...">
149152
<ButtonIcon type="export" />
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
import RecordToggle from './RecordToggle';
12+
13+
import styles from './Profiler.css';
14+
15+
export default function RecordingInProgress() {
16+
return (
17+
<div className={styles.Column}>
18+
<div className={styles.Header}>Profiling is in progress...</div>
19+
<div className={styles.Row}>
20+
Click the record button <RecordToggle /> to stop recording.
21+
</div>
22+
</div>
23+
);
24+
}

0 commit comments

Comments
 (0)