Import/export feature
Brian Vaughn committed
Mar 31, 2019 at 17:02 UTC
bfb152f1608918020ae8356f682b37a362d3f045
25 files changed
+327
-160
shells/browser/shared/src/background.js
+1
-1
@@ -110,7 +110,7 @@ chrome.runtime.onMessage.addListener((request, sender) => {
110
setIconAndPopup(reactBuildType, sender.tab.id);
111
}
112
113
- if (request.downloadFile) {
113
+ if (request.exportFile) {
114
let { contents, filename } = request;
115
if (!Array.isArray(contents)) {
116
contents = [contents];
shells/browser/shared/src/main.js
+3
-3
@@ -67,9 +67,9 @@ function createPanelIfReactLoaded() {
67
localStorage.setItem(SUPPORTS_PROFILING_KEY, 'true');
68
chrome.devtools.inspectedWindow.eval('window.location.reload();');
69
});
70
- bridge.addListener('downloadFile', ({ contents, filename }) => {
70
+ bridge.addListener('exportFile', ({ contents, filename }) => {
71
chrome.runtime.sendMessage({
72
- downloadFile: true,
72
+ exportFile: true,
73
contents,
74
filename,
75
});
@@ -85,7 +85,7 @@ function createPanelIfReactLoaded() {
85
}
86
87
store = new Store(bridge, {
88
- supportsDownloads: true,
88
+ supportsFileDownloads: true,
89
supportsReloadAndProfile: true,
90
supportsProfiling,
91
});
src/backend/agent.js
+17
-7
@@ -58,10 +58,7 @@ export default class Agent extends EventEmitter {
58
addBridge(bridge: Bridge) {
59
this._bridge = bridge;
60
61
- bridge.addListener(
62
- 'downloadProfilingSummary',
63
- this.downloadProfilingSummary
64
- );
61
+ bridge.addListener('exportProfilingSummary', this.exportProfilingSummary);
62
bridge.addListener('getCommitDetails', this.getCommitDetails);
63
bridge.addListener('getInteractions', this.getInteractions);
64
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
@@ -99,10 +96,14 @@ export default class Agent extends EventEmitter {
96
return null;
97
}
98
102
- downloadProfilingSummary = ({
99
+ exportProfilingSummary = ({
100
+ profilingOperations,
101
+ profilingSnapshot,
102
rendererID,
103
rootID,
104
}: {
105
+ profilingOperations: Array<any>,
106
+ profilingSnapshot: Array<any>,
107
rendererID: number,
108
rootID: number,
109
}) => {
@@ -110,8 +111,17 @@ export default class Agent extends EventEmitter {
111
if (renderer == null) {
112
console.warn(`Invalid renderer id "${rendererID}"`);
113
} else {
113
- this._bridge.send('downloadFile', {
114
- contents: renderer.getProfilingSummaryForDownload(rootID),
114
+ const rendererData = renderer.getProfilingDataForDownload(rootID);
115
+ this._bridge.send('exportFile', {
116
+ contents: JSON.stringify(
117
+ {
118
+ ...rendererData,
119
+ profilingOperations,
120
+ profilingSnapshot,
121
+ },
122
+ null,
123
+ 2
124
+ ),
125
filename: 'profile-data.json',
126
});
127
}
src/backend/renderer.js
+8
-11
@@ -1536,7 +1536,7 @@ export function attach(
1536
};
1537
}
1538
1539
- function getProfilingSummaryForDownload(rootID: number): string {
1539
+ function getProfilingDataForDownload(rootID: number): Object {
1540
const commitDetails = [];
1541
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
1542
rootID
@@ -1546,15 +1546,12 @@ export function attach(
1546
commitDetails.push(getCommitDetails(rootID, index));
1547
}
1548
}
1549
- return JSON.stringify(
1550
- {
1551
- summary: getProfilingSummary(rootID),
1552
- interactions: getInteractions(rootID),
1553
- commitDetails,
1554
- },
1555
- null,
1556
- 2
1557
- );
1549
+ return {
1550
+ version: 1,
1551
+ profilingSummary: getProfilingSummary(rootID),
1552
+ commitDetails,
1553
+ interactions: getInteractions(rootID),
1554
+ };
1555
}
1556
1557
function getProfilingSummary(rootID: number): ProfilingSummary {
@@ -1620,7 +1617,7 @@ export function attach(
1617
getFiberIDFromNative,
1618
getInteractions,
1619
getNativeFromReactElement,
1623
- getProfilingSummaryForDownload,
1620
+ getProfilingDataForDownload,
1621
getProfilingSummary,
1622
handleCommitFiberRoot,
1623
handleCommitFiberUnmount,
src/backend/types.js
+1
-1
@@ -89,7 +89,7 @@ export type RendererInterface = {
89
findNearestUnfilteredAncestor?: boolean
90
) => number | null,
91
getInteractions: (rootID: number) => Interactions,
92
- getProfilingSummaryForDownload: (rootID: number) => string,
92
+ getProfilingDataForDownload: (rootID: number) => Object,
93
getProfilingSummary: (rootID: number) => ProfilingSummary,
94
handleCommitFiberRoot: (fiber: Object) => void,
95
handleCommitFiberUnmount: (fiber: Object) => void,
src/devtools/ProfilingCache.js
+61
-38
@@ -51,8 +51,6 @@ type InteractionsParams = {|
51
type GetCommitTreeParams = {|
52
commitIndex: number,
53
profilingSummary: ProfilingSummaryFrontend,
54
- rendererID: number,
55
- rootID: number,
54
|};
55
56
type ProfilingSummaryParams = {|
@@ -85,13 +83,19 @@ export default class ProfilingCache {
83
> = createResource(
84
({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
85
return new Promise(resolve => {
88
- if (!this._store.profilingOperations.has(rootID)) {
89
- // If no profiling data was recorded for this root, skip the round trip.
90
- resolve({
91
- actualDurations: new Map(),
92
- interactions: [],
93
- });
94
- } else {
86
+ const importedProfilingData = this._store.importedProfilingData;
87
+ if (importedProfilingData !== null) {
88
+ const { commitDetails } = (importedProfilingData: any);
89
+ if (commitDetails != null && commitIndex < commitDetails.length) {
90
+ const response = commitDetails[commitIndex];
91
+ this._pendingCommitDetailsMap.set(
92
+ `${response.rootID}-${commitIndex}`,
93
+ resolve
94
+ );
95
+ this.onCommitDetails(response);
96
+ return;
97
+ }
98
+ } else if (this._store.profilingOperations.has(rootID)) {
99
this._pendingCommitDetailsMap.set(
100
`${rootID}-${commitIndex}`,
101
resolve
@@ -101,7 +105,16 @@ export default class ProfilingCache {
105
rendererID,
106
rootID,
107
});
108
+ return;
109
}
110
+
111
+ // If no profiling data was recorded for this root, skip the round trip.
112
+ resolve({
113
+ rootID,
114
+ commitIndex,
115
+ actualDurations: new Map(),
116
+ interactions: [],
117
+ });
118
});
119
},
120
({ commitIndex, rendererID, rootID }: CommitDetailsParams) =>
@@ -114,16 +127,25 @@ export default class ProfilingCache {
127
> = createResource(
128
({ rendererID, rootID }: InteractionsParams) => {
129
return new Promise(resolve => {
117
- if (!this._store.profilingOperations.has(rootID)) {
118
- // If no profiling data was recorded for this root, skip the round trip.
119
- resolve([]);
120
- } else {
130
+ const importedProfilingData = this._store.importedProfilingData;
131
+ if (importedProfilingData !== null) {
132
+ const { interactions } = (importedProfilingData: any);
133
+ if (interactions != null) {
134
+ this._pendingInteractionsMap.set(interactions.rootID, resolve);
135
+ this.onInteractions(interactions);
136
+ return;
137
+ }
138
+ } else if (this._store.profilingOperations.has(rootID)) {
139
this._pendingInteractionsMap.set(rootID, resolve);
140
this._bridge.send('getInteractions', {
141
rendererID,
142
rootID,
143
});
144
+ return;
145
}
146
+
147
+ // If no profiling data was recorded for this root, skip the round trip.
148
+ resolve([]);
149
});
150
},
151
({ rendererID, rootID }: ProfilingSummaryParams) => rootID
@@ -135,18 +157,31 @@ export default class ProfilingCache {
157
> = createResource(
158
({ rendererID, rootID }: ProfilingSummaryParams) => {
159
return new Promise(resolve => {
138
- if (!this._store.profilingOperations.has(rootID)) {
139
- // If no profiling data was recorded for this root, skip the round trip.
140
- resolve({
141
- commitDurations: [],
142
- commitTimes: [],
143
- initialTreeBaseDurations: new Map(),
144
- interactionCount: 0,
145
- });
146
- } else {
160
+ const importedProfilingData = this._store.importedProfilingData;
161
+ if (importedProfilingData !== null) {
162
+ const { profilingSummary } = (importedProfilingData: any);
163
+ if (profilingSummary != null) {
164
+ this._pendingProfileSummaryMap.set(
165
+ profilingSummary.rootID,
166
+ resolve
167
+ );
168
+ this.onProfileSummary(profilingSummary);
169
+ return;
170
+ }
171
+ } else if (this._store.profilingOperations.has(rootID)) {
172
this._pendingProfileSummaryMap.set(rootID, resolve);
173
this._bridge.send('getProfilingSummary', { rendererID, rootID });
174
+ return;
175
}
176
+
177
+ // If no profiling data was recorded for this root, skip the round trip.
178
+ resolve({
179
+ rootID,
180
+ commitDurations: [],
181
+ commitTimes: [],
182
+ initialTreeBaseDurations: new Map(),
183
+ interactionCount: 0,
184
+ });
185
});
186
},
187
({ rendererID, rootID }: ProfilingSummaryParams) => rootID
@@ -161,17 +196,10 @@ export default class ProfilingCache {
196
bridge.addListener('profilingSummary', this.onProfileSummary);
197
}
198
164
- getCommitTree = ({
165
- commitIndex,
166
- profilingSummary,
167
- rendererID,
168
- rootID,
169
- }: GetCommitTreeParams) =>
199
+ getCommitTree = ({ commitIndex, profilingSummary }: GetCommitTreeParams) =>
200
getCommitTree({
201
commitIndex,
202
profilingSummary,
173
- rendererID,
174
- rootID,
203
store: this._store,
204
});
205
@@ -179,18 +207,15 @@ export default class ProfilingCache {
207
commitDetails,
208
commitIndex,
209
commitTree,
182
- rootID,
210
}: {|
211
commitDetails: CommitDetailsFrontend,
212
commitIndex: number,
213
commitTree: CommitTreeFrontend,
187
- rootID: number,
214
|}): FlamegraphChartData =>
215
getFlamegraphChartData({
216
commitDetails,
217
commitIndex,
218
commitTree,
193
- rootID,
219
});
220
221
getInteractionsChartData = ({
@@ -200,30 +225,25 @@ export default class ProfilingCache {
225
}: {|
226
interactions: Array<InteractionWithCommits>,
227
profilingSummary: ProfilingSummaryFrontend,
203
- rootID: number,
228
|}): InteractionsChartData =>
229
getInteractionsChartData({
230
interactions,
231
profilingSummary,
208
- rootID,
232
});
233
234
getRankedChartData = ({
235
commitDetails,
236
commitIndex,
237
commitTree,
215
- rootID,
238
}: {|
239
commitDetails: CommitDetailsFrontend,
240
commitIndex: number,
241
commitTree: CommitTreeFrontend,
220
- rootID: number,
242
|}): RankedChartData =>
243
getRankedChartData({
244
commitDetails,
245
commitIndex,
246
commitTree,
226
- rootID,
247
});
248
249
invalidate() {
@@ -257,6 +277,8 @@ export default class ProfilingCache {
277
}
278
279
resolve({
280
+ rootID,
281
+ commitIndex,
282
actualDurations: actualDurationsMap,
283
interactions,
284
});
@@ -291,6 +313,7 @@ export default class ProfilingCache {
313
}
314
315
resolve({
316
+ rootID,
317
commitDurations,
318
commitTimes,
319
initialTreeBaseDurations: initialTreeBaseDurationsMap,
src/devtools/store.js
+31
-15
@@ -14,6 +14,10 @@ import ProfilingCache from './ProfilingCache';
14
15
import type { ElementType } from './types';
16
import type { Element } from './views/Elements/types';
17
+import type {
18
+ ImportedProfilingData,
19
+ ProfilingSnapshotNode,
20
+} from './views/Profiler/types';
21
import type { Bridge } from '../types';
22
23
const debug = (methodName, ...args) => {
@@ -28,18 +32,11 @@ const debug = (methodName, ...args) => {
32
};
33
34
type Config = {|
31
- supportsDownloads?: boolean,
35
+ supportsFileDownloads?: boolean,
36
supportsReloadAndProfile?: boolean,
37
supportsProfiling?: boolean,
38
|};
39
36
-type ProfilingSnapshotNode = {|
37
- id: number,
38
- children: Array<number>,
39
- displayName: string | null,
40
- key: number | string | null,
41
-|};
42
-
40
export type Capabilities = {|
41
supportsProfiling: boolean,
42
|};
@@ -55,6 +52,9 @@ export default class Store extends EventEmitter {
52
// Elements are mutable (for now) to avoid excessive cloning during tree updates.
53
_idToElement: Map<number, Element> = new Map();
54
55
+ // The user has imported a previously exported profiling session.
56
+ _importedProfilingData: ImportedProfilingData | null = null;
57
+
58
// The backend is currently profiling.
59
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
60
_isProfiling: boolean = false;
@@ -92,7 +92,7 @@ export default class Store extends EventEmitter {
92
93
// These options may be initially set by a confiugraiton option when constructing the Store.
94
// In the case of "supportsProfiling", the option may be updated based on the injected renderers.
95
- _supportsDownloads: boolean = false;
95
+ _supportsFileDownloads: boolean = false;
96
_supportsProfiling: boolean = false;
97
_supportsReloadAndProfile: boolean = false;
98
@@ -103,12 +103,12 @@ export default class Store extends EventEmitter {
103
104
if (config != null) {
105
const {
106
- supportsDownloads,
106
+ supportsFileDownloads,
107
supportsProfiling,
108
supportsReloadAndProfile,
109
} = config;
110
- if (supportsDownloads) {
111
- this._supportsDownloads = true;
110
+ if (supportsFileDownloads) {
111
+ this._supportsFileDownloads = true;
112
}
113
if (supportsProfiling) {
114
this._supportsProfiling = true;
@@ -132,7 +132,21 @@ export default class Store extends EventEmitter {
132
133
// Profiling data has been recorded for at least one root.
134
get hasProfilingData(): boolean {
135
- return this._profilingOperations.size > 0;
135
+ return (
136
+ this._importedProfilingData !== null || this._profilingOperations.size > 0
137
+ );
138
+ }
139
+
140
+ get importedProfilingData(): ImportedProfilingData | null {
141
+ return this._importedProfilingData;
142
+ }
143
+ set importedProfilingData(value: ImportedProfilingData | null): void {
144
+ this._importedProfilingData = value;
145
+ this._profilingOperations = new Map();
146
+ this._profilingSnapshot = new Map();
147
+ this._profilingCache.invalidate();
148
+
149
+ this.emit('importedProfilingData');
150
}
151
152
get isProfiling(): boolean {
@@ -163,8 +177,8 @@ export default class Store extends EventEmitter {
177
return this._roots;
178
}
179
166
- get supportsDownloads(): boolean {
167
- return this._supportsDownloads;
180
+ get supportsFileDownloads(): boolean {
181
+ return this._supportsFileDownloads;
182
}
183
184
get supportsProfiling(): boolean {
@@ -176,6 +190,7 @@ export default class Store extends EventEmitter {
190
}
191
192
clearProfilingData(): void {
193
+ this._importedProfilingData = null;
194
this._profilingOperations = new Map();
195
this._profilingSnapshot = new Map();
196
@@ -600,6 +615,7 @@ export default class Store extends EventEmitter {
615
616
onProfilingStatus = (isProfiling: boolean) => {
617
if (isProfiling) {
618
+ this._importedProfilingData = null;
619
this._profilingOperations = new Map();
620
this._profilingSnapshot = new Map();
621
this.roots.forEach(this._takeProfilingSnapshotRecursive);
src/devtools/views/ButtonIcon.js
+10
-7
@@ -9,8 +9,9 @@ export type IconType =
9
| 'close'
10
| 'copy'
11
| 'down'
12
- | 'download'
12
+ | 'export'
13
| 'filter'
14
+ | 'import'
15
| 'more'
16
| 'next'
17
| 'previous'
@@ -44,12 +45,15 @@ export default function ButtonIcon({ type }: Props) {
45
case 'down':
46
pathData = PATH_DOWN;
47
break;
47
- case 'download':
48
- pathData = PATH_DOWNLOAD;
48
+ case 'export':
49
+ pathData = PATH_EXPORT;
50
break;
51
case 'filter':
52
pathData = PATH_FILTER;
53
break;
54
+ case 'import':
55
+ pathData = PATH_IMPORT;
56
+ break;
57
case 'more':
58
pathData = PATH_MORE;
59
break;
@@ -120,13 +124,12 @@ const PATH_COPY = `
124
125
const PATH_DOWN = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z';
126
123
-const PATH_DOWNLOAD = `
124
- M 21.855469 8.460938 L 16.214844 8.460938 L 16.214844 0 L 7.753906 0 L 7.753906 8.460938 L 2.113281 8.460938 L 11.984375
125
- 18.332031 Z M 2.113281 21.152344 L 2.113281 23.972656 L 21.855469 23.972656 L 21.855469 21.152344 Z M 2.113281 21.152344
126
-`;
127
+const PATH_EXPORT = 'M15.82,2.14v7H21l-9,9L3,9.18H8.18v-7ZM3,20.13H21v1.73H3Z';
128
129
const PATH_FILTER = 'M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z';
130
131
+const PATH_IMPORT = 'M8.18,18.13v-7H3l9-8.95,9,9H15.82v7ZM3,20.13H21v1.73H3Z';
132
+
133
const PATH_MORE = `
134
M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.97.89 1.66.89H22c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9
135
13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm5 0c-.83
src/devtools/views/Profiler/CommitFlamegraph.js
-3
@@ -56,15 +56,12 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
56
commitTree = profilingCache.getCommitTree({
57
commitIndex: selectedCommitIndex,
58
profilingSummary,
59
- rendererID: ((rendererID: any): number),
60
- rootID: ((rootID: any): number),
59
});
60
61
chartData = profilingCache.getFlamegraphChartData({
62
commitDetails,
63
commitIndex: selectedCommitIndex,
64
commitTree,
67
- rootID: ((rootID: any): number),
65
});
66
}
67
src/devtools/views/Profiler/CommitRanked.js
-3
@@ -56,15 +56,12 @@ export default function CommitRankedAutoSizer(_: {||}) {
56
commitTree = profilingCache.getCommitTree({
57
commitIndex: selectedCommitIndex,
58
profilingSummary,
59
- rendererID: ((rendererID: any): number),
60
- rootID: ((rootID: any): number),
59
});
60
61
chartData = profilingCache.getRankedChartData({
62
commitDetails,
63
commitIndex: selectedCommitIndex,
64
commitTree,
67
- rootID: ((rootID: any): number),
65
});
66
}
67
src/devtools/views/Profiler/CommitTreeBuilder.js
+16
-9
@@ -34,16 +34,14 @@ const rootToCommitTreeMap: Map<number, Array<CommitTree>> = new Map();
34
export function getCommitTree({
35
commitIndex,
36
profilingSummary,
37
- rendererID,
38
- rootID,
37
store,
38
}: {|
39
commitIndex: number,
40
profilingSummary: ProfilingSummaryFrontend,
43
- rendererID: number,
44
- rootID: number,
41
store: Store,
42
|}): CommitTree {
43
+ const { rootID } = profilingSummary;
44
+
45
if (!rootToCommitTreeMap.has(rootID)) {
46
rootToCommitTreeMap.set(rootID, []);
47
}
@@ -56,6 +54,12 @@ export function getCommitTree({
54
return commitTrees[commitIndex];
55
}
56
57
+ const { importedProfilingData } = store;
58
+ const profilingOperations =
59
+ importedProfilingData != null
60
+ ? importedProfilingData.profilingOperations
61
+ : store.profilingOperations;
62
+
63
// Commits are generated sequentially and cached.
64
// If this is the very first commit, start with the cached snapshot and apply the first mutation.
65
// Otherwise load (or generate) the previous commit and append a mutation to it.
@@ -72,7 +76,7 @@ export function getCommitTree({
76
);
77
78
// Mutate the tree
75
- const commitOperations = store.profilingOperations.get(rootID);
79
+ const commitOperations = profilingOperations.get(rootID);
80
if (commitOperations != null && commitIndex < commitOperations.length) {
81
const commitTree = updateTree(
82
{ nodes, rootID },
@@ -90,11 +94,9 @@ export function getCommitTree({
94
const previousCommitTree = getCommitTree({
95
commitIndex: commitIndex - 1,
96
profilingSummary,
93
- rendererID,
94
- rootID,
97
store,
98
});
97
- const commitOperations = store.profilingOperations.get(rootID);
99
+ const commitOperations = profilingOperations.get(rootID);
100
if (commitOperations != null && commitIndex < commitOperations.length) {
101
const commitTree = updateTree(
102
previousCommitTree,
@@ -127,7 +129,12 @@ function recursivelyIniitliazeTree(
129
initialTreeBaseDurations: Map<number, number>,
130
store: Store
131
): void {
130
- const node = store.profilingSnapshot.get(id);
132
+ const { importedProfilingData } = store;
133
+ const profilingSnapshot =
134
+ importedProfilingData != null
135
+ ? importedProfilingData.profilingSnapshot
136
+ : store.profilingSnapshot;
137
+ const node = profilingSnapshot.get(id);
138
if (node != null) {
139
nodes.set(id, {
140
id,
src/devtools/views/Profiler/FlamegraphChartBuilder.js
+3
-6
@@ -28,22 +28,19 @@ export function getChartData({
28
commitDetails,
29
commitIndex,
30
commitTree,
31
- rootID,
31
}: {|
32
commitDetails: CommitDetails,
33
commitIndex: number,
34
commitTree: CommitTree,
36
- rootID: number,
35
|}): ChartData {
38
- const key = `${rootID}-${commitIndex}`;
36
+ const { actualDurations, rootID } = commitDetails;
37
+ const { nodes } = commitTree;
38
39
+ const key = `${rootID}-${commitIndex}`;
40
if (cachedChartData.has(key)) {
41
return ((cachedChartData.get(key): any): ChartData);
42
}
43
44
- const { nodes } = commitTree;
45
- const { actualDurations } = commitDetails;
46
-
44
const idToDepthMap: Map<number, number> = new Map();
45
const rows: Array<Array<ChartNode>> = [];
46
src/devtools/views/Profiler/Interactions.js
-1
@@ -61,7 +61,6 @@ function Interactions({ height, width }: {| height: number, width: number |}) {
61
const chartData = profilingCache.getInteractionsChartData({
62
interactions,
63
profilingSummary,
64
- rootID: ((rootID: any): number),
64
});
65
66
const handleKeyDown = useCallback(
src/devtools/views/Profiler/InteractionsChartBuilder.js
+2
-2
@@ -12,12 +12,12 @@ const cachedChartData: Map<number, ChartData> = new Map();
12
export function getChartData({
13
interactions,
14
profilingSummary,
15
- rootID,
15
}: {|
16
interactions: Array<InteractionWithCommits>,
17
profilingSummary: ProfilingSummary,
19
- rootID: number,
18
|}): ChartData {
19
+ const { rootID } = profilingSummary;
20
+
21
if (cachedChartData.has(rootID)) {
22
return ((cachedChartData.get(rootID): any): ChartData);
23
}
src/devtools/views/Profiler/Profiler.css
+3
-2
@@ -66,8 +66,9 @@
66
.VRule {
67
height: 20px;
68
width: 1px;
69
- background-color: var(--color-border);
70
- margin: 0 0.25rem;
69
+ border-left: 1px solid var(--color-border);
70
+ padding-left: 0.25rem;
71
+ margin-left: 0.25rem;
72
}
73
74
.Spacer {
src/devtools/views/Profiler/Profiler.js
+3
-3
@@ -15,7 +15,7 @@ import FilterModal from './FilterModal';
15
import Interactions from './Interactions';
16
import RecordToggle from './RecordToggle';
17
import ReloadAndProfileButton from './ReloadAndProfileButton';
18
-import SaveProfilingDataButton from './SaveProfilingDataButton';
18
+import ProfilingImportExportButtons from './ProfilingImportExportButtons';
19
import SnapshotSelector from './SnapshotSelector';
20
import SidebarCommitInfo from './SidebarCommitInfo';
21
import SidebarInteractions from './SidebarInteractions';
@@ -89,7 +89,7 @@ function NonSuspendingProfiler({
89
<RecordToggle disabled={!supportsProfiling} />
90
<ReloadAndProfileButton />
91
<ClearProfilingDataButton />
92
- <SaveProfilingDataButton />
92
+ <ProfilingImportExportButtons />
93
<div className={styles.VRule} />
94
<TabBar
95
currentTab={null}
@@ -160,7 +160,7 @@ function SuspendingProfiler() {
160
<RecordToggle />
161
<ReloadAndProfileButton />
162
<ClearProfilingDataButton />
163
- <SaveProfilingDataButton />
163
+ <ProfilingImportExportButtons />
164
<div className={styles.VRule} />
165
<TabBar
166
currentTab={selectedTabID}
src/devtools/views/Profiler/ProfilerContext.js
+17
-6
@@ -13,6 +13,8 @@ import { TreeContext } from '../Elements/TreeContext';
13
import { StoreContext } from '../context';
14
import Store from '../../store';
15
16
+import type { ImportedProfilingData } from './types';
17
+
18
export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions';
19
20
type Context = {|
@@ -66,6 +68,7 @@ ProfilerContext.displayName = 'ProfilerContext';
68
69
type StoreProfilingState = {|
70
hasProfilingData: boolean,
71
+ importedProfilingData: ImportedProfilingData | null,
72
isProfiling: boolean,
73
|};
74
@@ -81,24 +84,32 @@ function ProfilerContextController({ children }: Props) {
84
() => ({
85
getCurrentValue: () => ({
86
hasProfilingData: store.hasProfilingData,
87
+ importedProfilingData: store.importedProfilingData,
88
isProfiling: store.isProfiling,
89
}),
90
subscribe: (callback: Function) => {
91
+ store.addListener('importedProfilingData', callback);
92
store.addListener('isProfiling', callback);
88
- return () => store.removeListener('isProfiling', callback);
93
+ return () => {
94
+ store.removeListener('importedProfilingData', callback);
95
+ store.removeListener('isProfiling', callback);
96
+ };
97
},
98
}),
99
[store]
100
);
93
- const { isProfiling, hasProfilingData } = useSubscription<
94
- StoreProfilingState,
95
- Store
96
- >(subscription);
101
+ const {
102
+ isProfiling,
103
+ hasProfilingData,
104
+ importedProfilingData,
105
+ } = useSubscription<StoreProfilingState, Store>(subscription);
106
107
let rendererID = null;
108
let rootID = null;
109
let rootHasProfilingData = false;
101
- if (selectedElementID) {
110
+ if (importedProfilingData !== null) {
111
+ rootHasProfilingData = true;
112
+ } else if (selectedElementID) {
113
rendererID = store.getRendererIDForElement(
114
((selectedElementID: any): number)
115
);
src/devtools/views/Profiler/ProfilingImportExportButtons.css
new
+18
@@ -0,0 +1,18 @@
1
+.VRule {
2
+ height: 20px;
3
+ width: 1px;
4
+ border-left: 1px solid var(--color-border);
5
+ padding-left: 0.25rem;
6
+ margin-left: 0.25rem;
7
+}
8
+
9
+/**
10
+ * https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications
11
+ */
12
+.Input {
13
+ position: absolute !important;
14
+ height: 1px;
15
+ width: 1px;
16
+ overflow: hidden;
17
+ clip: rect(1px, 1px, 1px, 1px);
18
+}
src/devtools/views/Profiler/ProfilingImportExportButtons.js
new
+107
@@ -0,0 +1,107 @@
1
+// @flow
2
+
3
+import React, { Fragment, useContext, useCallback, useRef } from 'react';
4
+import { ProfilerContext } from './ProfilerContext';
5
+import Button from '../Button';
6
+import ButtonIcon from '../ButtonIcon';
7
+import { BridgeContext, StoreContext } from '../context';
8
+
9
+import styles from './ProfilingImportExportButtons.css';
10
+
11
+import type { ImportedProfilingData } from './types';
12
+
13
+export default function ProfilingImportExportButtons() {
14
+ const bridge = useContext(BridgeContext);
15
+ const { isProfiling, rendererID, rootHasProfilingData, rootID } = useContext(
16
+ ProfilerContext
17
+ );
18
+ const store = useContext(StoreContext);
19
+
20
+ const inputRef = useRef<HTMLInputElement | null>(null);
21
+
22
+ const downloadData = useCallback(() => {
23
+ if (rootID === null) {
24
+ return;
25
+ }
26
+
27
+ const profilingOperationsForRoot = [];
28
+ const operations = store.profilingOperations.get(rootID);
29
+ if (operations != null) {
30
+ operations.forEach(operations => {
31
+ // Convert typed Array before JSON serialization, or it will be converted to an Object.
32
+ profilingOperationsForRoot.push(Array.from(operations));
33
+ });
34
+ }
35
+
36
+ const profilingSnapshotForRoot = [];
37
+ const queue = [rootID];
38
+ while (queue.length) {
39
+ const id = queue.pop();
40
+ profilingSnapshotForRoot.push([id, store.profilingSnapshot.get(id)]);
41
+ }
42
+
43
+ bridge.send('exportProfilingSummary', {
44
+ profilingOperations: [[rootID, profilingOperationsForRoot]],
45
+ profilingSnapshot: profilingSnapshotForRoot,
46
+ rendererID,
47
+ rootID,
48
+ });
49
+ }, [
50
+ bridge,
51
+ rendererID,
52
+ rootID,
53
+ store.profilingOperations,
54
+ store.profilingSnapshot,
55
+ ]);
56
+
57
+ const uploadData = useCallback(() => {
58
+ if (inputRef.current !== null) {
59
+ inputRef.current.click();
60
+ }
61
+ }, []);
62
+
63
+ const handleFiles = useCallback(() => {
64
+ const input = inputRef.current;
65
+ if (input !== null && input.files.length > 0) {
66
+ const fileReader = new FileReader();
67
+ fileReader.addEventListener('load', () => {
68
+ const data = JSON.parse((fileReader.result: any));
69
+ data.profilingOperations = new Map(data.profilingOperations);
70
+ data.profilingSnapshot = new Map(data.profilingSnapshot);
71
+
72
+ // TODO (profiling) Version check; warn if older version.
73
+
74
+ store.importedProfilingData = ((data: any): ImportedProfilingData);
75
+ });
76
+ fileReader.readAsText(input.files[0]);
77
+ }
78
+ }, [store]);
79
+
80
+ return (
81
+ <Fragment>
82
+ <div className={styles.VRule} />
83
+ <input
84
+ ref={inputRef}
85
+ className={styles.Input}
86
+ type="file"
87
+ onChange={handleFiles}
88
+ />
89
+ <Button
90
+ disabled={isProfiling}
91
+ onClick={uploadData}
92
+ title="Load profile..."
93
+ >
94
+ <ButtonIcon type="import" />
95
+ </Button>
96
+ {store.supportsFileDownloads && (
97
+ <Button
98
+ disabled={isProfiling || !rootHasProfilingData}
99
+ onClick={downloadData}
100
+ title="Save profile..."
101
+ >
102
+ <ButtonIcon type="export" />
103
+ </Button>
104
+ )}
105
+ </Fragment>
106
+ );
107
+}
src/devtools/views/Profiler/RankedChartBuilder.js
+4
-6
@@ -22,25 +22,23 @@ export function getChartData({
22
commitDetails,
23
commitIndex,
24
commitTree,
25
- rootID,
25
}: {|
26
commitDetails: CommitDetails,
27
commitIndex: number,
28
commitTree: CommitTree,
30
- rootID: number,
29
|}): ChartData {
32
- const key = `${rootID}-${commitIndex}`;
30
+ const { actualDurations, rootID } = commitDetails;
31
+ const { nodes } = commitTree;
32
33
+ const key = `${rootID}-${commitIndex}`;
34
if (cachedChartData.has(key)) {
35
return ((cachedChartData.get(key): any): ChartData);
36
}
37
38
- const { nodes } = commitTree;
39
-
38
let maxSelfDuration = 0;
39
40
const chartNodes: Array<ChartNode> = [];
43
- commitDetails.actualDurations.forEach((actualDuration, id) => {
41
+ actualDurations.forEach((actualDuration, id) => {
42
const node = ((nodes.get(id): any): Node);
43
44
// Don't show the root node in this chart.
src/devtools/views/Profiler/SaveProfilingDataButton.js
deleted
-33
@@ -1,33 +0,0 @@
1
-// @flow
2
-
3
-import React, { useContext, useCallback } from 'react';
4
-import { ProfilerContext } from './ProfilerContext';
5
-import Button from '../Button';
6
-import ButtonIcon from '../ButtonIcon';
7
-import { BridgeContext, StoreContext } from '../context';
8
-
9
-export default function SaveProfilingDataButton() {
10
- const bridge = useContext(BridgeContext);
11
- const { isProfiling, rendererID, rootHasProfilingData, rootID } = useContext(
12
- ProfilerContext
13
- );
14
- const store = useContext(StoreContext);
15
-
16
- const saveProfiler = useCallback(() => {
17
- bridge.send('downloadProfilingSummary', { rendererID, rootID });
18
- }, [bridge, rendererID, rootID]);
19
-
20
- if (!store.supportsDownloads) {
21
- return null;
22
- }
23
-
24
- return (
25
- <Button
26
- disabled={isProfiling || !rootHasProfilingData}
27
- onClick={saveProfiler}
28
- title="Save profile..."
29
- >
30
- <ButtonIcon type="download" />
31
- </Button>
32
- );
33
-}
src/devtools/views/Profiler/SidebarInteractions.js
-1
@@ -40,7 +40,6 @@ export default function SidebarInteractions(_: Props) {
40
const { maxCommitDuration } = profilingCache.getInteractionsChartData({
41
interactions,
42
profilingSummary,
43
- rootID: ((rootID: any): number),
43
});
44
45
const { commitDurations, commitTimes } = profilingSummary;
src/devtools/views/Profiler/SnapshotCommitList.css
+1
-1
@@ -5,5 +5,5 @@
5
* There are two alternative navigation mechanisms (arrow keys and arrow buttons).
6
* If necessary, we could also expand the click-and-drag mechanism to scroll.
7
*/
8
- overflow-x: hidden;
8
+ overflow-x: hidden !important;
9
}
src/devtools/views/Profiler/SnapshotSelector.js
+1
-1
@@ -112,7 +112,7 @@ export default function SnapshotSelector(_: Props) {
112
[viewNextCommit, viewPrevCommit]
113
);
114
115
- if (rendererID === null || rootID === null) {
115
+ if (commitDurations.length === 0) {
116
return null;
117
}
118
src/devtools/views/Profiler/types.js
+20
@@ -28,11 +28,15 @@ export type InteractionWithCommits = {|
28
export type Interactions = Array<InteractionWithCommits>;
29
30
export type CommitDetails = {|
31
+ rootID: number,
32
+ commitIndex: number,
33
actualDurations: Map<number, number>,
34
interactions: Array<Interaction>,
35
|};
36
37
export type ProfilingSummary = {|
38
+ rootID: number,
39
+
40
// Commit durations
41
commitDurations: Array<number>,
42
@@ -44,3 +48,19 @@ export type ProfilingSummary = {|
48
49
interactionCount: number,
50
|};
51
+
52
+export type ProfilingSnapshotNode = {|
53
+ id: number,
54
+ children: Array<number>,
55
+ displayName: string | null,
56
+ key: number | string | null,
57
+|};
58
+
59
+export type ImportedProfilingData = {|
60
+ version: number,
61
+ profilingOperations: Map<number, Array<Uint32Array>>,
62
+ profilingSnapshot: Map<number, ProfilingSnapshotNode>,
63
+ commitDetails: CommitDetails,
64
+ interactions: Interactions,
65
+ profilingSummary: ProfilingSummary,
66
+|};