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"