@samitouri / QOS-React / commits / e5400f76f0

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 );