Store screenshots after each commit when profiling

This commit is contained in:
Brian Vaughn 2019-04-02 15:04:04 -07:00
parent f415e2447e
commit c111288c54
10 changed files with 157 additions and 6 deletions

View File

@ -40,6 +40,7 @@
},
"permissions": [
"<all_urls>",
"background",
"downloads",
"tabs",

View File

@ -46,6 +46,7 @@
},
"permissions": [
"<all_urls>",
"background",
"downloads",
"tabs",

View File

@ -120,5 +120,22 @@ chrome.runtime.onMessage.addListener((request, sender) => {
const url = URL.createObjectURL(blob);
chrome.downloads.download({ filename, saveAs: true, url });
}
if (request.captureScreenshot) {
const { commitIndex } = request;
chrome.tabs.captureVisibleTab(undefined, undefined, dataURL => {
// TODO For some reason, sending a response using the third param (sendResponse) doesn't work,
// so we have to use the chrome.tabs API for this instead.
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
chrome.tabs.sendMessage(tabs[0].id, {
event: 'screenshotCaptured',
payload: {
commitIndex,
dataURL,
},
});
});
});
}
}
});

View File

@ -75,3 +75,15 @@ if (!backendInitialized) {
}
}, 500);
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.event === 'screenshotCaptured') {
window.postMessage(
{
source: 'react-devtools-content-script',
payload: request,
},
'*'
);
}
});

View File

@ -74,6 +74,15 @@ function createPanelIfReactLoaded() {
filename,
});
});
bridge.addListener('captureScreenshot', ({ commitIndex }) => {
chrome.runtime.sendMessage(
{
captureScreenshot: true,
commitIndex,
},
response => bridge.send('screenshotCaptured', response)
);
});
// This flag lets us tip the Store off early that we expect to be profiling.
// This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,

View File

@ -56,6 +56,7 @@ export default class Agent extends EventEmitter {
addBridge(bridge: Bridge) {
this._bridge = bridge;
bridge.addListener('captureScreenshot', this.captureScreenshot);
bridge.addListener('exportProfilingSummary', this.exportProfilingSummary);
bridge.addListener('getCommitDetails', this.getCommitDetails);
bridge.addListener('getInteractions', this.getInteractions);
@ -68,6 +69,7 @@ export default class Agent extends EventEmitter {
bridge.addListener('overrideProps', this.overrideProps);
bridge.addListener('overrideState', this.overrideState);
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
bridge.addListener('screenshotCaptured', this.screenshotCaptured);
bridge.addListener('selectElement', this.selectElement);
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
bridge.addListener('startProfiling', this.startProfiling);
@ -81,6 +83,10 @@ export default class Agent extends EventEmitter {
}
}
captureScreenshot = ({ commitIndex }: { commitIndex: number }) => {
this._bridge.send('captureScreenshot', { commitIndex });
};
getIDForNode(node: Object): number | null {
for (let rendererID in this._rendererInterfaces) {
// 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 {
this._bridge.send('reloadAppForProfiling');
};
screenshotCaptured = ({
commitIndex,
dataURL,
}: {|
commitIndex: number,
dataURL: string,
|}) => {
this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
};
selectElement = ({ id, rendererID }: InspectSelectParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {

View File

@ -72,6 +72,9 @@ export default class Store extends EventEmitter {
// to reconstruct the state of each root for each commit.
_profilingOperations: Map<number, Array<Uint32Array>> = new Map();
// Stores screenshots for each commit (when profiling).
_profilingScreenshots: Map<number, string> = new Map();
// Snapshot of the state of the main Store (including all roots) when profiling started.
// Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
// to reconstruct the state of each root for each commit.
@ -126,6 +129,7 @@ export default class Store extends EventEmitter {
this._bridge = bridge;
bridge.addListener('operations', this.onBridgeOperations);
bridge.addListener('profilingStatus', this.onProfilingStatus);
bridge.addListener('screenshotCaptured', this.onScreenshotCaptured);
bridge.addListener('shutdown', this.onBridgeShutdown);
// It's possible that profiling has already started (e.g. "reload and start profiling")
@ -170,6 +174,10 @@ export default class Store extends EventEmitter {
return this._profilingOperations;
}
get profilingScreenshots(): Map<number, string> {
return this._profilingScreenshots;
}
get profilingSnapshot(): Map<number, ProfilingSnapshotNode> {
return this._profilingSnapshot;
}
@ -197,6 +205,7 @@ export default class Store extends EventEmitter {
clearProfilingData(): void {
this._importedProfilingData = null;
this._profilingOperations = new Map();
this._profilingScreenshots = new Map();
this._profilingSnapshot = new Map();
// Invalidate suspense cache if profiling data is being (re-)recorded.
@ -400,12 +409,17 @@ export default class Store extends EventEmitter {
const rootID = operations[1];
if (this._isProfiling) {
const profilingOperations = this._profilingOperations.get(rootID);
let profilingOperations = this._profilingOperations.get(rootID);
if (profilingOperations == null) {
this._profilingOperations.set(rootID, [operations]);
profilingOperations = [operations];
this._profilingOperations.set(rootID, profilingOperations);
} else {
profilingOperations.push(operations);
}
const commitIndex = profilingOperations.length - 1;
this._bridge.send('captureScreenshot', { commitIndex });
}
let addedElementIDs: Uint32Array = new Uint32Array(0);
@ -622,6 +636,7 @@ export default class Store extends EventEmitter {
if (isProfiling) {
this._importedProfilingData = null;
this._profilingOperations = new Map();
this._profilingScreenshots = new Map();
this._profilingSnapshot = new Map();
this.roots.forEach(this._takeProfilingSnapshotRecursive);
}
@ -632,6 +647,16 @@ export default class Store extends EventEmitter {
}
};
onScreenshotCaptured = ({
commitIndex,
dataURL,
}: {|
commitIndex: number,
dataURL: string,
|}) => {
this._profilingScreenshots.set(commitIndex, dataURL);
};
onBridgeShutdown = () => {
debug('onBridgeShutdown', 'unsubscribing from Bridge');

View File

@ -52,3 +52,25 @@
height: 100%;
color: var(--color-dim);
}
.Screenshot {
width: 100%;
}
.Modal {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-modal-background);
padding: 0.5rem;
}
.ModalImage {
max-height: 100%;
max-width: 100%;
}

View File

@ -1,6 +1,6 @@
// @flow
import React, { Fragment, useContext } from 'react';
import React, { Fragment, useCallback, useContext, useState } from 'react';
import { ProfilerContext } from './ProfilerContext';
import { formatDuration, formatTime } from './utils';
import { StoreContext } from '../context';
@ -18,7 +18,25 @@ export default function SidebarCommitInfo(_: Props) {
selectTab,
} = useContext(ProfilerContext);
const { profilingCache } = useContext(StoreContext);
const { profilingCache, profilingScreenshots } = useContext(StoreContext);
const screenshot =
selectedCommitIndex !== null
? profilingScreenshots.get(selectedCommitIndex)
: null;
const [
isScreenshotModalVisible,
setIsScreenshotModalVisible,
] = useState<boolean>(false);
const hideScreenshotModal = useCallback(
() => setIsScreenshotModalVisible(false),
[]
);
const showScreenshotModal = useCallback(
() => setIsScreenshotModalVisible(true),
[]
);
if (selectedCommitIndex === null) {
return <div className={styles.NothingSelected}>Nothing selected</div>;
@ -79,8 +97,38 @@ export default function SidebarCommitInfo(_: Props) {
))}
</ul>
</li>
{screenshot != null && (
<li>
<img
alt="Screenshot"
className={styles.Screenshot}
onClick={showScreenshotModal}
src={screenshot}
/>
</li>
)}
{screenshot != null && isScreenshotModalVisible && (
<ScreenshotModal
hideScreenshotModal={hideScreenshotModal}
screenshot={screenshot}
/>
)}
</ul>
</div>
</Fragment>
);
}
function ScreenshotModal({
hideScreenshotModal,
screenshot,
}: {|
hideScreenshotModal: Function,
screenshot: string,
|}) {
return (
<div className={styles.Modal} onClick={hideScreenshotModal}>
<img alt="Screenshot" className={styles.ModalImage} src={screenshot} />
</div>
);
}

View File

@ -36,7 +36,7 @@
--light-color-hover-background: #ebf1fb;
--light-color-jsx-arrow-brackets: #333333;
--light-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
--light-color-modal-background: rgba(255, 255, 255, 0.25);
--light-color-modal-background: rgba(255, 255, 255, 0.75);
--light-color-record-active: #fc3a4b;
--light-color-record-hover: #3578e5;
--light-color-record-inactive: #0088fa;
@ -80,7 +80,7 @@
--dark-color-hover-background: #3d424a;
--dark-color-jsx-arrow-brackets: #777d88;
--dark-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
--dark-color-modal-background: rgba(0, 0, 0, 0.25);
--dark-color-modal-background: rgba(0, 0, 0, 0.75);
--dark-color-record-active: #fc3a4b;
--dark-color-record-hover: #a2e9fc;
--dark-color-record-inactive: #61dafb;