Store screenshots after each commit when profiling
Brian Vaughn committed
Apr 2, 2019 at 15:04 UTC
c111288c549bea32b2e0760f5aef30aac70ad5d7
10 files changed
+157
-6
shells/browser/chrome/manifest.json
+1
@@ -40,6 +40,7 @@
40
},
41
42
"permissions": [
43
+ "<all_urls>",
44
"background",
45
"downloads",
46
"tabs",
shells/browser/firefox/manifest.json
+1
@@ -46,6 +46,7 @@
46
},
47
48
"permissions": [
49
+ "<all_urls>",
50
"background",
51
"downloads",
52
"tabs",
shells/browser/shared/src/background.js
+17
@@ -120,5 +120,22 @@ chrome.runtime.onMessage.addListener((request, sender) => {
120
const url = URL.createObjectURL(blob);
121
chrome.downloads.download({ filename, saveAs: true, url });
122
}
123
+
124
+ if (request.captureScreenshot) {
125
+ const { commitIndex } = request;
126
+ chrome.tabs.captureVisibleTab(undefined, undefined, dataURL => {
127
+ // TODO For some reason, sending a response using the third param (sendResponse) doesn't work,
128
+ // so we have to use the chrome.tabs API for this instead.
129
+ chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
130
+ chrome.tabs.sendMessage(tabs[0].id, {
131
+ event: 'screenshotCaptured',
132
+ payload: {
133
+ commitIndex,
134
+ dataURL,
135
+ },
136
+ });
137
+ });
138
+ });
139
+ }
140
}
141
});
shells/browser/shared/src/contentScript.js
+12
@@ -75,3 +75,15 @@ if (!backendInitialized) {
75
}
76
}, 500);
77
}
78
+
79
+chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
80
+ if (request.event === 'screenshotCaptured') {
81
+ window.postMessage(
82
+ {
83
+ source: 'react-devtools-content-script',
84
+ payload: request,
85
+ },
86
+ '*'
87
+ );
88
+ }
89
+});
shells/browser/shared/src/main.js
+9
@@ -74,6 +74,15 @@ function createPanelIfReactLoaded() {
74
filename,
75
});
76
});
77
+ bridge.addListener('captureScreenshot', ({ commitIndex }) => {
78
+ chrome.runtime.sendMessage(
79
+ {
80
+ captureScreenshot: true,
81
+ commitIndex,
82
+ },
83
+ response => bridge.send('screenshotCaptured', response)
84
+ );
85
+ });
86
87
// This flag lets us tip the Store off early that we expect to be profiling.
88
// This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,
src/backend/agent.js
+16
@@ -56,6 +56,7 @@ export default class Agent extends EventEmitter {
56
addBridge(bridge: Bridge) {
57
this._bridge = bridge;
58
59
+ bridge.addListener('captureScreenshot', this.captureScreenshot);
60
bridge.addListener('exportProfilingSummary', this.exportProfilingSummary);
61
bridge.addListener('getCommitDetails', this.getCommitDetails);
62
bridge.addListener('getInteractions', this.getInteractions);
@@ -68,6 +69,7 @@ export default class Agent extends EventEmitter {
69
bridge.addListener('overrideProps', this.overrideProps);
70
bridge.addListener('overrideState', this.overrideState);
71
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
72
+ bridge.addListener('screenshotCaptured', this.screenshotCaptured);
73
bridge.addListener('selectElement', this.selectElement);
74
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
75
bridge.addListener('startProfiling', this.startProfiling);
@@ -81,6 +83,10 @@ export default class Agent extends EventEmitter {
83
}
84
}
85
86
+ captureScreenshot = ({ commitIndex }: { commitIndex: number }) => {
87
+ this._bridge.send('captureScreenshot', { commitIndex });
88
+ };
89
+
90
getIDForNode(node: Object): number | null {
91
for (let rendererID in this._rendererInterfaces) {
92
// A renderer will throw if it can't find a fiber for the specified node.
@@ -235,6 +241,16 @@ export default class Agent extends EventEmitter {
241
this._bridge.send('reloadAppForProfiling');
242
};
243
244
+ screenshotCaptured = ({
245
+ commitIndex,
246
+ dataURL,
247
+ }: {|
248
+ commitIndex: number,
249
+ dataURL: string,
250
+ |}) => {
251
+ this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
252
+ };
253
+
254
selectElement = ({ id, rendererID }: InspectSelectParams) => {
255
const renderer = this._rendererInterfaces[rendererID];
256
if (renderer == null) {
src/devtools/store.js
+27
-2
@@ -72,6 +72,9 @@ export default class Store extends EventEmitter {
72
// to reconstruct the state of each root for each commit.
73
_profilingOperations: Map<number, Array<Uint32Array>> = new Map();
74
75
+ // Stores screenshots for each commit (when profiling).
76
+ _profilingScreenshots: Map<number, string> = new Map();
77
+
78
// Snapshot of the state of the main Store (including all roots) when profiling started.
79
// Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
80
// to reconstruct the state of each root for each commit.
@@ -126,6 +129,7 @@ export default class Store extends EventEmitter {
129
this._bridge = bridge;
130
bridge.addListener('operations', this.onBridgeOperations);
131
bridge.addListener('profilingStatus', this.onProfilingStatus);
132
+ bridge.addListener('screenshotCaptured', this.onScreenshotCaptured);
133
bridge.addListener('shutdown', this.onBridgeShutdown);
134
135
// It's possible that profiling has already started (e.g. "reload and start profiling")
@@ -170,6 +174,10 @@ export default class Store extends EventEmitter {
174
return this._profilingOperations;
175
}
176
177
+ get profilingScreenshots(): Map<number, string> {
178
+ return this._profilingScreenshots;
179
+ }
180
+
181
get profilingSnapshot(): Map<number, ProfilingSnapshotNode> {
182
return this._profilingSnapshot;
183
}
@@ -197,6 +205,7 @@ export default class Store extends EventEmitter {
205
clearProfilingData(): void {
206
this._importedProfilingData = null;
207
this._profilingOperations = new Map();
208
+ this._profilingScreenshots = new Map();
209
this._profilingSnapshot = new Map();
210
211
// Invalidate suspense cache if profiling data is being (re-)recorded.
@@ -400,12 +409,17 @@ export default class Store extends EventEmitter {
409
const rootID = operations[1];
410
411
if (this._isProfiling) {
403
- const profilingOperations = this._profilingOperations.get(rootID);
412
+ let profilingOperations = this._profilingOperations.get(rootID);
413
if (profilingOperations == null) {
405
- this._profilingOperations.set(rootID, [operations]);
414
+ profilingOperations = [operations];
415
+ this._profilingOperations.set(rootID, profilingOperations);
416
} else {
417
profilingOperations.push(operations);
418
}
419
+
420
+ const commitIndex = profilingOperations.length - 1;
421
+
422
+ this._bridge.send('captureScreenshot', { commitIndex });
423
}
424
425
let addedElementIDs: Uint32Array = new Uint32Array(0);
@@ -622,6 +636,7 @@ export default class Store extends EventEmitter {
636
if (isProfiling) {
637
this._importedProfilingData = null;
638
this._profilingOperations = new Map();
639
+ this._profilingScreenshots = new Map();
640
this._profilingSnapshot = new Map();
641
this.roots.forEach(this._takeProfilingSnapshotRecursive);
642
}
@@ -632,6 +647,16 @@ export default class Store extends EventEmitter {
647
}
648
};
649
650
+ onScreenshotCaptured = ({
651
+ commitIndex,
652
+ dataURL,
653
+ }: {|
654
+ commitIndex: number,
655
+ dataURL: string,
656
+ |}) => {
657
+ this._profilingScreenshots.set(commitIndex, dataURL);
658
+ };
659
+
660
onBridgeShutdown = () => {
661
debug('onBridgeShutdown', 'unsubscribing from Bridge');
662
src/devtools/views/Profiler/SidebarCommitInfo.css
+22
@@ -52,3 +52,25 @@
52
height: 100%;
53
color: var(--color-dim);
54
}
55
+
56
+.Screenshot {
57
+ width: 100%;
58
+}
59
+
60
+.Modal {
61
+ position: absolute;
62
+ top: 0;
63
+ bottom: 0;
64
+ left: 0;
65
+ right: 0;
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ background-color: var(--color-modal-background);
70
+ padding: 0.5rem;
71
+}
72
+
73
+.ModalImage {
74
+ max-height: 100%;
75
+ max-width: 100%;
76
+}
src/devtools/views/Profiler/SidebarCommitInfo.js
+50
-2
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import React, { Fragment, useContext } from 'react';
3
+import React, { Fragment, useCallback, useContext, useState } from 'react';
4
import { ProfilerContext } from './ProfilerContext';
5
import { formatDuration, formatTime } from './utils';
6
import { StoreContext } from '../context';
@@ -18,7 +18,25 @@ export default function SidebarCommitInfo(_: Props) {
18
selectTab,
19
} = useContext(ProfilerContext);
20
21
- const { profilingCache } = useContext(StoreContext);
21
+ const { profilingCache, profilingScreenshots } = useContext(StoreContext);
22
+
23
+ const screenshot =
24
+ selectedCommitIndex !== null
25
+ ? profilingScreenshots.get(selectedCommitIndex)
26
+ : null;
27
+ const [
28
+ isScreenshotModalVisible,
29
+ setIsScreenshotModalVisible,
30
+ ] = useState<boolean>(false);
31
+
32
+ const hideScreenshotModal = useCallback(
33
+ () => setIsScreenshotModalVisible(false),
34
+ []
35
+ );
36
+ const showScreenshotModal = useCallback(
37
+ () => setIsScreenshotModalVisible(true),
38
+ []
39
+ );
40
41
if (selectedCommitIndex === null) {
42
return <div className={styles.NothingSelected}>Nothing selected</div>;
@@ -79,8 +97,38 @@ export default function SidebarCommitInfo(_: Props) {
97
))}
98
</ul>
99
</li>
100
+ {screenshot != null && (
101
+ <li>
102
+ <img
103
+ alt="Screenshot"
104
+ className={styles.Screenshot}
105
+ onClick={showScreenshotModal}
106
+ src={screenshot}
107
+ />
108
+ </li>
109
+ )}
110
+ {screenshot != null && isScreenshotModalVisible && (
111
+ <ScreenshotModal
112
+ hideScreenshotModal={hideScreenshotModal}
113
+ screenshot={screenshot}
114
+ />
115
+ )}
116
</ul>
117
</div>
118
</Fragment>
119
);
120
}
121
+
122
+function ScreenshotModal({
123
+ hideScreenshotModal,
124
+ screenshot,
125
+}: {|
126
+ hideScreenshotModal: Function,
127
+ screenshot: string,
128
+|}) {
129
+ return (
130
+ <div className={styles.Modal} onClick={hideScreenshotModal}>
131
+ <img alt="Screenshot" className={styles.ModalImage} src={screenshot} />
132
+ </div>
133
+ );
134
+}
src/devtools/views/root.css
+2
-2
@@ -36,7 +36,7 @@
36
--light-color-hover-background: #ebf1fb;
37
--light-color-jsx-arrow-brackets: #333333;
38
--light-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
39
- --light-color-modal-background: rgba(255, 255, 255, 0.25);
39
+ --light-color-modal-background: rgba(255, 255, 255, 0.75);
40
--light-color-record-active: #fc3a4b;
41
--light-color-record-hover: #3578e5;
42
--light-color-record-inactive: #0088fa;
@@ -80,7 +80,7 @@
80
--dark-color-hover-background: #3d424a;
81
--dark-color-jsx-arrow-brackets: #777d88;
82
--dark-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
83
- --dark-color-modal-background: rgba(0, 0, 0, 0.25);
83
+ --dark-color-modal-background: rgba(0, 0, 0, 0.75);
84
--dark-color-record-active: #fc3a4b;
85
--dark-color-record-hover: #a2e9fc;
86
--dark-color-record-inactive: #61dafb;