@samitouri / QOS-React / commits / ed6e34da8e

Added option to disable screenshot capturing while profiling

Brian Vaughn committed Apr 3, 2019 at 14:32 UTC ed6e34da8e839bf63edb9085086b4e2567771e46
12 files changed +128 -16
package.json
+1
@@ -56,6 +56,7 @@
56 "cli-spinners": "^1.0.0",
57 "clipboard-js": "^0.3.6",
58 "css-loader": "^1.0.1",
59 + "html2canvas": "^1.0.0-alpha.12",
60 "error-stack-parser": "^2.0.2",
61 "es6-symbol": "3.0.2",
62 "escape-string-regexp": "^1.0.5",
shells/browser/shared/src/injectGlobalHook.js
+2 -2
@@ -2,7 +2,7 @@
2
3 import nullthrows from 'nullthrows';
4 import { installHook } from 'src/hook';
5 -import { RELOAD_AND_PROFILE_KEY } from 'src/constants';
5 +import { LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY } from 'src/constants';
6
7 function injectCode(code) {
8 const script = document.createElement('script');
@@ -63,7 +63,7 @@ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set;
63 `;
64
65 // If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
66 -if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') {
66 +if (localStorage.getItem(LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
67 const rendererURL = chrome.runtime.getURL('build/renderer.js');
68 let rendererCode;
69
shells/browser/shared/src/main.js
+8 -4
@@ -12,7 +12,8 @@ import {
12 } from './utils';
13 import DevTools from 'src/devtools/views/DevTools';
14
15 -const SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling';
15 +const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
16 + 'React::DevTools::supportsProfiling';
17
18 let panelCreated = false;
19
@@ -64,7 +65,7 @@ function createPanelIfReactLoaded() {
65 },
66 });
67 bridge.addListener('reloadAppForProfiling', () => {
67 - localStorage.setItem(SUPPORTS_PROFILING_KEY, 'true');
68 + localStorage.setItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
69 chrome.devtools.inspectedWindow.eval('window.location.reload();');
70 });
71 bridge.addListener('exportFile', ({ contents, filename }) => {
@@ -89,16 +90,19 @@ function createPanelIfReactLoaded() {
90 // after a user has clicked the "reload and profile" button.
91 let isProfiling = false;
92 let supportsProfiling = false;
92 - if (localStorage.getItem(SUPPORTS_PROFILING_KEY) === 'true') {
93 + if (
94 + localStorage.getItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true'
95 + ) {
96 supportsProfiling = true;
97 isProfiling = true;
95 - localStorage.removeItem(SUPPORTS_PROFILING_KEY);
98 + localStorage.removeItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY);
99 }
100
101 const browserName = getBrowserName();
102
103 store = new Store(bridge, {
104 isProfiling,
105 + supportsCaptureScreenshots: true,
106 supportsFileDownloads: browserName === 'Chrome',
107 supportsReloadAndProfile: true,
108 supportsProfiling,
shells/dev/src/backend.js
+10
@@ -1,5 +1,6 @@
1 /** @flow */
2
3 +import html2canvas from 'html2canvas';
4 import Agent from 'src/backend/agent';
5 import Bridge from 'src/bridge';
6 import { initBackend } from 'src/backend';
@@ -15,6 +16,15 @@ const bridge = new Bridge({
16 },
17 });
18
19 +bridge.addListener('captureScreenshot', ({ commitIndex }) => {
20 + html2canvas(document.body, { logging: false }).then(canvas => {
21 + bridge.send('screenshotCaptured', {
22 + commitIndex,
23 + dataURL: canvas.toDataURL(),
24 + });
25 + });
26 +});
27 +
28 const agent = new Agent();
29 agent.addBridge(bridge);
30
shells/dev/src/devtools.js
+1 -1
@@ -54,7 +54,7 @@ inject('./build/app.js', () => {
54
55 cb(bridge);
56
57 - const store = new Store(bridge);
57 + const store = new Store(bridge, { supportsCaptureScreenshots: true });
58
59 const root = createRoot(container);
60 const batch = root.createBatch();
src/backend/agent.js
+4 -4
@@ -1,7 +1,7 @@
1 // @flow
2
3 import EventEmitter from 'events';
4 -import { RELOAD_AND_PROFILE_KEY, __DEBUG__ } from '../constants';
4 +import { LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY, __DEBUG__ } from '../constants';
5 import { hideOverlay, showOverlay } from './views/Highlighter';
6
7 import type { RendererID, RendererInterface } from './types';
@@ -46,10 +46,10 @@ export default class Agent extends EventEmitter {
46 constructor() {
47 super();
48
49 - if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') {
49 + if (localStorage.getItem(LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
50 this._isProfiling = true;
51
52 - localStorage.removeItem(RELOAD_AND_PROFILE_KEY);
52 + localStorage.removeItem(LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY);
53 }
54 }
55
@@ -233,7 +233,7 @@ export default class Agent extends EventEmitter {
233 };
234
235 reloadAndProfile = () => {
236 - localStorage.setItem(RELOAD_AND_PROFILE_KEY, 'true');
236 + localStorage.setItem(LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
237
238 // This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
239 // In that case, the shell must also listen for this specific message to know when it needs to reload the app.
src/backend/renderer.js
+2 -2
@@ -18,7 +18,7 @@ import { getDisplayName, utfEncodeString } from '../utils';
18 import { cleanForBridge, copyWithSet, setInObject } from './utils';
19 import {
20 __DEBUG__,
21 - RELOAD_AND_PROFILE_KEY,
21 + LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY,
22 TREE_OPERATION_ADD,
23 TREE_OPERATION_REMOVE,
24 TREE_OPERATION_RESET_CHILDREN,
@@ -1642,7 +1642,7 @@ export function attach(
1642 }
1643
1644 // Automatically start profiling so that we don't miss timing info from initial "mount".
1645 - if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') {
1645 + if (localStorage.getItem(LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
1646 startProfiling();
1647 }
1648
src/constants.js
+2 -1
@@ -5,6 +5,7 @@ export const TREE_OPERATION_REMOVE = 2;
5 export const TREE_OPERATION_RESET_CHILDREN = 3;
6 export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
7
8 -export const RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile';
8 +export const LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY =
9 + 'React::DevTools::reloadAndProfile';
10
11 export const __DEBUG__ = false;
src/devtools/store.js
+35 -1
@@ -31,8 +31,12 @@ const debug = (methodName, ...args) => {
31 }
32 };
33
34 +const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY =
35 + 'React::DevTools::captureScreenshots';
36 +
37 type Config = {|
38 isProfiling?: boolean,
39 + supportsCaptureScreenshots?: boolean,
40 supportsFileDownloads?: boolean,
41 supportsReloadAndProfile?: boolean,
42 supportsProfiling?: boolean,
@@ -49,6 +53,8 @@ export type Capabilities = {|
53 export default class Store extends EventEmitter {
54 _bridge: Bridge;
55
56 + _captureScreenshots: boolean = false;
57 +
58 // Map of ID to Element.
59 // Elements are mutable (for now) to avoid excessive cloning during tree updates.
60 _idToElement: Map<number, Element> = new Map();
@@ -96,6 +102,7 @@ export default class Store extends EventEmitter {
102
103 // These options may be initially set by a confiugraiton option when constructing the Store.
104 // In the case of "supportsProfiling", the option may be updated based on the injected renderers.
105 + _supportsCaptureScreenshots: boolean = false;
106 _supportsFileDownloads: boolean = false;
107 _supportsProfiling: boolean = false;
108 _supportsReloadAndProfile: boolean = false;
@@ -108,6 +115,7 @@ export default class Store extends EventEmitter {
115 if (config != null) {
116 const {
117 isProfiling,
118 + supportsCaptureScreenshots,
119 supportsFileDownloads,
120 supportsProfiling,
121 supportsReloadAndProfile,
@@ -115,6 +123,12 @@ export default class Store extends EventEmitter {
123 if (isProfiling) {
124 this._isProfiling = true;
125 }
126 + if (supportsCaptureScreenshots) {
127 + this._supportsCaptureScreenshots = true;
128 + this._captureScreenshots =
129 + localStorage.getItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) !==
130 + 'false';
131 + }
132 if (supportsFileDownloads) {
133 this._supportsFileDownloads = true;
134 }
@@ -139,6 +153,20 @@ export default class Store extends EventEmitter {
153 this._profilingCache = new ProfilingCache(bridge, this);
154 }
155
156 + get captureScreenshots(): boolean {
157 + return this._captureScreenshots;
158 + }
159 + set captureScreenshots(value: boolean): void {
160 + this._captureScreenshots = value;
161 +
162 + localStorage.setItem(
163 + LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY,
164 + value ? 'true' : 'false'
165 + );
166 +
167 + this.emit('captureScreenshots');
168 + }
169 +
170 // Profiling data has been recorded for at least one root.
171 get hasProfilingData(): boolean {
172 return (
@@ -190,6 +218,10 @@ export default class Store extends EventEmitter {
218 return this._roots;
219 }
220
221 + get supportsCaptureScreenshots(): boolean {
222 + return this._supportsCaptureScreenshots;
223 + }
224 +
225 get supportsFileDownloads(): boolean {
226 return this._supportsFileDownloads;
227 }
@@ -419,7 +451,9 @@ export default class Store extends EventEmitter {
451
452 const commitIndex = profilingOperations.length - 1;
453
422 - this._bridge.send('captureScreenshot', { commitIndex });
454 + if (this._captureScreenshots) {
455 + this._bridge.send('captureScreenshot', { commitIndex });
456 + }
457 }
458
459 let addedElementIDs: Uint32Array = new Uint32Array(0);
src/devtools/views/Profiler/SidebarCommitInfo.css
+5
@@ -55,6 +55,9 @@
55
56 .Screenshot {
57 width: 100%;
58 + border: 1px solid var(--color-border);
59 + border-radius: 0.25rem;
60 + cursor: pointer;
61 }
62
63 .Modal {
@@ -73,4 +76,6 @@
76 .ModalImage {
77 max-height: 100%;
78 max-width: 100%;
79 + border: 1px solid var(--color-border);
80 + border-radius: 0.5rem;
81 }
src/devtools/views/Settings/Settings.js
+39 -1
@@ -1,8 +1,11 @@
1 // @flow
2
3 -import React, { useCallback, useContext } from 'react';
3 +import React, { useCallback, useContext, useMemo } from 'react';
4 import { createPortal } from 'react-dom';
5 +import { useSubscription } from '../hooks';
6 +import { StoreContext } from '../context';
7 import { SettingsContext } from './SettingsContext';
8 +import Store from 'src/devtools/store';
9
10 import styles from './Settings.css';
11
@@ -11,10 +14,23 @@ export type Props = {|
14 |};
15
16 export default function Settings({ portalContainer }: Props) {
17 + const store = useContext(StoreContext);
18 const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
19 SettingsContext
20 );
21
22 + const subscription = useMemo(
23 + () => ({
24 + getCurrentValue: () => store.captureScreenshots,
25 + subscribe: (callback: Function) => {
26 + store.addListener('captureScreenshots', callback);
27 + return () => store.removeListener('captureScreenshots', callback);
28 + },
29 + }),
30 + [store]
31 + );
32 + const captureScreenshots = useSubscription<boolean, Store>(subscription);
33 +
34 const updateDisplayDensity = useCallback(
35 ({ currentTarget }) => {
36 setDisplayDensity(currentTarget.value);
@@ -29,6 +45,13 @@ export default function Settings({ portalContainer }: Props) {
45 [setTheme]
46 );
47
48 + const updateCaptureScreenshotsWhileProfiling = useCallback(
49 + ({ currentTarget }) => {
50 + store.captureScreenshots = currentTarget.checked;
51 + },
52 + [store]
53 + );
54 +
55 const children = (
56 <div className={styles.Settings}>
57 <div className={styles.Section}>
@@ -91,6 +114,21 @@ export default function Settings({ portalContainer }: Props) {
114 </label>
115 </div>
116 </div>
117 + {store.supportsCaptureScreenshots && (
118 + <div className={styles.Section}>
119 + <div className={styles.Header}>Profiler</div>
120 + <div className={styles.OptionGroup}>
121 + <label className={styles.Option}>
122 + <input
123 + type="checkbox"
124 + checked={captureScreenshots}
125 + onChange={updateCaptureScreenshotsWhileProfiling}
126 + />{' '}
127 + Capture screenshots while profiling
128 + </label>
129 + </div>
130 + </div>
131 + )}
132 </div>
133 );
134
yarn.lock
+19
@@ -2038,6 +2038,11 @@ balanced-match@^1.0.0:
2038 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
2039 integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
2040
2041 +base64-arraybuffer@^0.1.5:
2042 + version "0.1.5"
2043 + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
2044 + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
2045 +
2046 base64-js@0.0.8:
2047 version "0.0.8"
2048 resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
@@ -3259,6 +3264,13 @@ crypto-random-string@^1.0.0:
3264 version "1.0.0"
3265 resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
3266
3267 +css-line-break@1.0.1:
3268 + version "1.0.1"
3269 + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.0.1.tgz#19f2063a33e95fb2831b86446c0b80c188af450a"
3270 + integrity sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=
3271 + dependencies:
3272 + base64-arraybuffer "^0.1.5"
3273 +
3274 css-loader@^1.0.1:
3275 version "1.0.1"
3276 resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
@@ -5392,6 +5404,13 @@ html-encoding-sniffer@^1.0.2:
5404 dependencies:
5405 whatwg-encoding "^1.0.1"
5406
5407 +html2canvas@^1.0.0-alpha.12:
5408 + version "1.0.0-alpha.12"
5409 + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-alpha.12.tgz#3b1992e3c9b3f56063c35fd620494f37eba88513"
5410 + integrity sha1-OxmS48mz9WBjw1/WIElPN+uohRM=
5411 + dependencies:
5412 + css-line-break "1.0.1"
5413 +
5414 htmlparser2@^3.9.1:
5415 version "3.9.2"
5416 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"