Added save profiling data button to Profiler
Brian Vaughn committed
Mar 30, 2019 at 14:16 UTC
e5400f76f04a58714b41a70b6148104fb8d17cf4
10 files changed
+142
-26
shells/browser/chrome/manifest.json
+8
-1
@@ -34,7 +34,14 @@
34
"persistent": false
35
},
36
37
- "permissions": ["file:///*", "http://*/*", "https://*/*"],
37
+ "permissions": [
38
+ "background",
39
+ "downloads",
40
+ "tabs",
41
+ "file:///*",
42
+ "http://*/*",
43
+ "https://*/*"
44
+ ],
45
46
"content_scripts": [
47
{
shells/browser/firefox/manifest.json
+8
-1
@@ -40,7 +40,14 @@
40
"persistent": false
41
},
42
43
- "permissions": ["file:///*", "http://*/*", "https://*/*"],
43
+ "permissions": [
44
+ "background",
45
+ "downloads",
46
+ "tabs",
47
+ "file:///*",
48
+ "http://*/*",
49
+ "https://*/*"
50
+ ],
51
52
"content_scripts": [
53
{
shells/browser/shared/src/background.js
+30
-17
@@ -88,24 +88,37 @@ if (IS_FIREFOX) {
88
});
89
}
90
91
-chrome.runtime.onMessage.addListener((req, sender) => {
92
- // This is sent from the hook content script.
93
- // It tells us a renderer has attached.
94
- if (req.hasDetectedReact && sender.tab) {
95
- // We use browserAction instead of pageAction because this lets us
96
- // display a custom default popup when React is *not* detected.
97
- // It is specified in the manifest.
98
- let reactBuildType = req.reactBuildType;
99
- if (sender.url.indexOf('facebook.github.io/react') !== -1) {
100
- // Cheat: We use the development version on the website because
101
- // it is better for interactive examples. However we're going
102
- // to get misguided bug reports if the extension highlights it
103
- // as using the dev version. We're just going to special case
104
- // our own documentation and cheat. It is acceptable to use dev
105
- // version of React in React docs, but not in any other case.
106
- reactBuildType = 'production';
91
+chrome.runtime.onMessage.addListener((request, sender) => {
92
+ if (sender.tab) {
93
+ // This is sent from the hook content script.
94
+ // It tells us a renderer has attached.
95
+ if (request.hasDetectedReact) {
96
+ // We use browserAction instead of pageAction because this lets us
97
+ // display a custom default popup when React is *not* detected.
98
+ // It is specified in the manifest.
99
+ let reactBuildType = request.reactBuildType;
100
+ if (sender.url.indexOf('facebook.github.io/react') !== -1) {
101
+ // Cheat: We use the development version on the website because
102
+ // it is better for interactive examples. However we're going
103
+ // to get misguided bug reports if the extension highlights it
104
+ // as using the dev version. We're just going to special case
105
+ // our own documentation and cheat. It is acceptable to use dev
106
+ // version of React in React docs, but not in any other case.
107
+ reactBuildType = 'production';
108
+ }
109
+
110
+ setIconAndPopup(reactBuildType, sender.tab.id);
111
}
112
109
- setIconAndPopup(reactBuildType, sender.tab.id);
113
+ if (request.downloadFile) {
114
+ let { contents, filename } = request;
115
+ if (!Array.isArray(contents)) {
116
+ contents = [contents];
117
+ }
118
+
119
+ const blob = new Blob(contents, { type: 'text/plain' });
120
+ const url = URL.createObjectURL(blob);
121
+ chrome.downloads.download({ filename, saveAs: true, url });
122
+ }
123
}
124
});
shells/browser/shared/src/main.js
+8
@@ -67,6 +67,13 @@ function createPanelIfReactLoaded() {
67
localStorage.setItem(SUPPORTS_PROFILING_KEY, 'true');
68
chrome.devtools.inspectedWindow.eval('window.location.reload();');
69
});
70
+ bridge.addListener('downloadFile', ({ contents, filename }) => {
71
+ chrome.runtime.sendMessage({
72
+ downloadFile: true,
73
+ contents,
74
+ filename,
75
+ });
76
+ });
77
78
// This flag lets us tip the Store off early that we expect to be profiling.
79
// This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,
@@ -78,6 +85,7 @@ function createPanelIfReactLoaded() {
85
}
86
87
store = new Store(bridge, {
88
+ supportsDownloads: true,
89
supportsReloadAndProfile: true,
90
supportsProfiling,
91
});
src/backend/agent.js
+22
@@ -58,6 +58,10 @@ export default class Agent extends EventEmitter {
58
addBridge(bridge: Bridge) {
59
this._bridge = bridge;
60
61
+ bridge.addListener(
62
+ 'downloadProfilingSummary',
63
+ this.downloadProfilingSummary
64
+ );
65
bridge.addListener('getCommitDetails', this.getCommitDetails);
66
bridge.addListener('getInteractions', this.getInteractions);
67
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
@@ -95,6 +99,24 @@ export default class Agent extends EventEmitter {
99
return null;
100
}
101
102
+ downloadProfilingSummary = ({
103
+ rendererID,
104
+ rootID,
105
+ }: {
106
+ rendererID: number,
107
+ rootID: number,
108
+ }) => {
109
+ const renderer = this._rendererInterfaces[rendererID];
110
+ if (renderer == null) {
111
+ console.warn(`Invalid renderer id "${rendererID}"`);
112
+ } else {
113
+ this._bridge.send('downloadFile', {
114
+ contents: renderer.getProfilingSummaryForDownload(rootID),
115
+ filename: 'profile-data.json',
116
+ });
117
+ }
118
+ };
119
+
120
getCommitDetails = ({
121
commitIndex,
122
rendererID,
src/backend/renderer.js
+22
@@ -1536,6 +1536,27 @@ export function attach(
1536
};
1537
}
1538
1539
+ function getProfilingSummaryForDownload(rootID: number): string {
1540
+ const commitDetails = [];
1541
+ const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
1542
+ rootID
1543
+ );
1544
+ if (commitProfilingMetadata != null) {
1545
+ for (let index = 0; index < commitProfilingMetadata.length; index++) {
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
+ );
1558
+ }
1559
+
1560
function getProfilingSummary(rootID: number): ProfilingSummary {
1561
const interactions = new Set();
1562
const commitDurations = [];
@@ -1599,6 +1620,7 @@ export function attach(
1620
getFiberIDFromNative,
1621
getInteractions,
1622
getNativeFromReactElement,
1623
+ getProfilingSummaryForDownload,
1624
getProfilingSummary,
1625
handleCommitFiberRoot,
1626
handleCommitFiberUnmount,
src/backend/types.js
+1
@@ -89,6 +89,7 @@ export type RendererInterface = {
89
findNearestUnfilteredAncestor?: boolean
90
) => number | null,
91
getInteractions: (rootID: number) => Interactions,
92
+ getProfilingSummaryForDownload: (rootID: number) => string,
93
getProfilingSummary: (rootID: number) => ProfilingSummary,
94
handleCommitFiberRoot: (fiber: Object) => void,
95
handleCommitFiberUnmount: (fiber: Object) => void,
src/devtools/store.js
+18
-3
@@ -28,6 +28,7 @@ const debug = (methodName, ...args) => {
28
};
29
30
type Config = {|
31
+ supportsDownloads?: boolean,
32
supportsReloadAndProfile?: boolean,
33
supportsProfiling?: boolean,
34
|};
@@ -89,8 +90,10 @@ export default class Store extends EventEmitter {
90
// Renderer ID is needed to support inspection fiber props, state, and hooks.
91
_rootIDToRendererID: Map<number, number> = new Map();
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;
96
_supportsProfiling: boolean = false;
93
-
97
_supportsReloadAndProfile: boolean = false;
98
99
constructor(bridge: Bridge, config?: Config) {
@@ -99,10 +102,18 @@ export default class Store extends EventEmitter {
102
debug('constructor', 'subscribing to Bridge');
103
104
if (config != null) {
102
- if (config.supportsProfiling) {
105
+ const {
106
+ supportsDownloads,
107
+ supportsProfiling,
108
+ supportsReloadAndProfile,
109
+ } = config;
110
+ if (supportsDownloads) {
111
+ this._supportsDownloads = true;
112
+ }
113
+ if (supportsProfiling) {
114
this._supportsProfiling = true;
115
}
105
- if (config.supportsReloadAndProfile) {
116
+ if (supportsReloadAndProfile) {
117
this._supportsReloadAndProfile = true;
118
}
119
}
@@ -152,6 +163,10 @@ export default class Store extends EventEmitter {
163
return this._roots;
164
}
165
166
+ get supportsDownloads(): boolean {
167
+ return this._supportsDownloads;
168
+ }
169
+
170
get supportsProfiling(): boolean {
171
return this._supportsProfiling;
172
}
src/devtools/views/Profiler/ClearProfilingDataButton.js
+4
-1
@@ -1,17 +1,20 @@
1
// @flow
2
3
import React, { useCallback, useContext } from 'react';
4
+import { ProfilerContext } from './ProfilerContext';
5
import Button from '../Button';
6
import ButtonIcon from '../ButtonIcon';
7
import { StoreContext } from '../context';
8
9
export default function ClearProfilingDataButton() {
10
const store = useContext(StoreContext);
11
+ const { isProfiling } = useContext(ProfilerContext);
12
+
13
const clear = useCallback(() => store.clearProfilingData(), [store]);
14
15
return (
16
<Button
14
- disabled={!store.hasProfilingData}
17
+ disabled={isProfiling || !store.hasProfilingData}
18
onClick={clear}
19
title="Clear profiling data"
20
>
src/devtools/views/Profiler/SaveProfilingDataButton.js
+21
-3
@@ -1,14 +1,32 @@
1
// @flow
2
3
-import React from 'react';
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() {
8
- // TODO (profiling) Support export
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 (
11
- <Button disabled title="Save profile...">
25
+ <Button
26
+ disabled={isProfiling || !rootHasProfilingData}
27
+ onClick={saveProfiler}
28
+ title="Save profile..."
29
+ >
30
<ButtonIcon type="download" />
31
</Button>
32
);