@samitouri / QOS-React / commits / c111288c54

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;