Store screenshots after each commit when profiling
This commit is contained in:
parent
f415e2447e
commit
c111288c54
|
@ -40,6 +40,7 @@
|
|||
},
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"background",
|
||||
"downloads",
|
||||
"tabs",
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
},
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"background",
|
||||
"downloads",
|
||||
"tabs",
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue