@samitouri / QOS-React / commits / bfb152f160

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 +|};