Skip to content

Commit

Permalink
Store screenshots after each commit when profiling
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed Apr 2, 2019
1 parent f415e24 commit c111288
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 6 deletions.
1 change: 1 addition & 0 deletions shells/browser/chrome/manifest.json
Expand Up @@ -40,6 +40,7 @@
},

"permissions": [
"<all_urls>",
"background",
"downloads",
"tabs",
Expand Down
1 change: 1 addition & 0 deletions shells/browser/firefox/manifest.json
Expand Up @@ -46,6 +46,7 @@
},

"permissions": [
"<all_urls>",
"background",
"downloads",
"tabs",
Expand Down
17 changes: 17 additions & 0 deletions shells/browser/shared/src/background.js
Expand Up @@ -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,
},
});
});
});
}
}
});
12 changes: 12 additions & 0 deletions shells/browser/shared/src/contentScript.js
Expand Up @@ -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,
},
'*'
);
}
});
9 changes: 9 additions & 0 deletions shells/browser/shared/src/main.js
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions src/backend/agent.js
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
29 changes: 27 additions & 2 deletions src/devtools/store.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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');

Expand Down
22 changes: 22 additions & 0 deletions src/devtools/views/Profiler/SidebarCommitInfo.css
Expand Up @@ -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%;
}
52 changes: 50 additions & 2 deletions src/devtools/views/Profiler/SidebarCommitInfo.js
@@ -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';
Expand All @@ -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>;
Expand Down Expand Up @@ -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>
);
}
4 changes: 2 additions & 2 deletions src/devtools/views/root.css
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit c111288

Please sign in to comment.