Implemented reload-and-profile. Also fixed an couple of minor profiling bugs along the way

This commit is contained in:
Brian Vaughn
2019-03-27 09:41:12 -07:00
parent ea5f310fe1
commit 7d24e83989
7 changed files with 219 additions and 90 deletions

View File

@@ -61,13 +61,17 @@ port.onDisconnect.addListener(handleDisconnect);
window.addEventListener('message', handleMessageFromPage);
sayHelloToBackend();
// The backend waits to install the global hook until notified by the content script.
// In the event of a page reload, the content script might be loaded before the backend is injected.
// Because of this we need to poll the backend until it has been initialized.
const intervalID = setInterval(() => {
if (backendInitialized || backendDisconnected) {
clearInterval(intervalID);
} else {
sayHelloToBackend();
}
}, 500);
if (!backendInitialized) {
const intervalID = setInterval(() => {
if (backendInitialized || backendDisconnected) {
clearInterval(intervalID);
} else {
sayHelloToBackend();
}
}, 500);
}

View File

@@ -12,6 +12,8 @@ import {
} from './utils';
import DevTools from 'src/devtools/views/DevTools';
const SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling';
let panelCreated = false;
function createPanelIfReactLoaded() {
@@ -61,8 +63,24 @@ function createPanelIfReactLoaded() {
}
},
});
bridge.addListener('reloadAppForProfiling', () => {
localStorage.setItem(SUPPORTS_PROFILING_KEY, 'true');
chrome.devtools.inspectedWindow.eval('window.location.reload();');
});
store = new Store(bridge);
// 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,
// after a user has clicked the "reload and profile" button.
let supportsProfiling = false;
if (localStorage.getItem(SUPPORTS_PROFILING_KEY) === 'true') {
supportsProfiling = true;
localStorage.removeItem(SUPPORTS_PROFILING_KEY);
}
store = new Store(bridge, {
supportsReloadAndProfile: true,
supportsProfiling,
});
// Initialize the backend only once the Store has been initialized.
// Otherwise the Store may miss important initial tree op codes.

View File

@@ -38,11 +38,23 @@ type SetInParams = {|
value: any,
|};
const RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile';
export default class Agent extends EventEmitter {
_bridge: Bridge = ((null: any): Bridge);
_isProfiling: boolean = false;
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
constructor() {
super();
if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') {
this._isProfiling = true;
localStorage.removeItem(RELOAD_AND_PROFILE_KEY);
}
}
addBridge(bridge: Bridge) {
this._bridge = bridge;
@@ -56,6 +68,7 @@ export default class Agent extends EventEmitter {
bridge.addListener('overrideHookState', this.overrideHookState);
bridge.addListener('overrideProps', this.overrideProps);
bridge.addListener('overrideState', this.overrideState);
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
bridge.addListener('selectElement', this.selectElement);
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
bridge.addListener('startProfiling', this.startProfiling);
@@ -63,6 +76,10 @@ export default class Agent extends EventEmitter {
bridge.addListener('stopProfiling', this.stopProfiling);
bridge.addListener('shutdown', this.shutdown);
bridge.addListener('viewElementSource', this.viewElementSource);
if (this._isProfiling) {
this._bridge.send('profilingStatus', true);
}
}
getIDForNode(node: Object): number | null {
@@ -179,6 +196,15 @@ export default class Agent extends EventEmitter {
}
};
reloadAndProfile = () => {
localStorage.setItem(RELOAD_AND_PROFILE_KEY, 'true');
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
// The agent can't do this in a way that is renderer agnostic.
this._bridge.send('reloadAppForProfiling');
};
selectElement = ({ id, rendererID }: InspectSelectParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
@@ -235,6 +261,10 @@ export default class Agent extends EventEmitter {
rendererInterface: RendererInterface
) {
this._rendererInterfaces[rendererID] = rendererInterface;
if (this._isProfiling) {
rendererInterface.startProfiling();
}
}
shutdown = () => {

View File

@@ -639,6 +639,17 @@ export function attach(
operation[1] = id;
operation[2] = treeBaseDuration;
addOperation(operation);
const { actualDuration } = fiber;
if (actualDuration > 0) {
// If profiling is active, store durations for elements that were rendered during the commit.
const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData);
metadata.actualDurations.push(id, actualDuration);
metadata.maxActualDuration = Math.max(
metadata.maxActualDuration,
actualDuration
);
}
}
}
@@ -866,6 +877,23 @@ export function attach(
// Hydrate all the roots for the first time.
hook.getFiberRoots(rendererID).forEach(root => {
currentRootID = getFiberID(getPrimaryFiber(root.current));
if (isProfiling) {
// If profiling is active, store commit time and duration, and the current interactions.
// The frontend may request this information after profiling has stopped.
currentCommitProfilingMetadata = {
actualDurations: [],
commitTime: performance.now() - profilingStartTime,
interactions: Array.from(root.memoizedInteractions).map(
(interaction: Interaction) => ({
...interaction,
timestamp: interaction.timestamp - profilingStartTime,
})
),
maxActualDuration: 0,
};
}
mountFiber(root.current, null);
flushPendingEvents(root);
currentRootID = -1;

View File

@@ -27,6 +27,11 @@ const debug = (methodName, ...args) => {
}
};
type Config = {|
supportsReloadAndProfile?: boolean,
supportsProfiling?: boolean,
|};
type ProfilingSnapshotNode = {|
id: number,
children: Array<number>,
@@ -86,11 +91,22 @@ export default class Store extends EventEmitter {
_supportsProfiling: boolean = false;
constructor(bridge: Bridge) {
_supportsReloadAndProfile: boolean = false;
constructor(bridge: Bridge, config?: Config) {
super();
debug('constructor', 'subscribing to Bridge');
if (config != null) {
if (config.supportsProfiling) {
this._supportsProfiling = true;
}
if (config.supportsReloadAndProfile) {
this._supportsReloadAndProfile = true;
}
}
this._bridge = bridge;
bridge.addListener('operations', this.onBridgeOperations);
bridge.addListener('profilingStatus', this.onProfilingStatus);
@@ -140,6 +156,10 @@ export default class Store extends EventEmitter {
return this._supportsProfiling;
}
get supportsReloadAndProfile(): boolean {
return this._supportsReloadAndProfile;
}
getElementAtIndex(index: number): Element | null {
if (index < 0 || index >= this.numElements) {
console.warn(

View File

@@ -44,71 +44,69 @@ export function getCommitTree({
rootID: number,
store: Store,
|}): CommitTree {
if (store.profilingSnapshot.has(rootID)) {
if (!rootToCommitTreeMap.has(rootID)) {
rootToCommitTreeMap.set(rootID, []);
}
if (!rootToCommitTreeMap.has(rootID)) {
rootToCommitTreeMap.set(rootID, []);
}
const commitTrees = ((rootToCommitTreeMap.get(
rootID
): any): Array<CommitTree>);
const commitTrees = ((rootToCommitTreeMap.get(
rootID
): any): Array<CommitTree>);
if (commitIndex < commitTrees.length) {
return commitTrees[commitIndex];
}
if (commitIndex < commitTrees.length) {
return commitTrees[commitIndex];
}
// Commits are generated sequentially and cached.
// If this is the very first commit, start with the cached snapshot and apply the first mutation.
// Otherwise load (or generate) the previous commit and append a mutation to it.
if (commitIndex === 0) {
const nodes = new Map();
// Commits are generated sequentially and cached.
// If this is the very first commit, start with the cached snapshot and apply the first mutation.
// Otherwise load (or generate) the previous commit and append a mutation to it.
if (commitIndex === 0) {
const nodes = new Map();
// Construct the initial tree.
recursivelyIniitliazeTree(
rootID,
0,
nodes,
profilingSummary.initialTreeBaseDurations,
store
// Construct the initial tree.
recursivelyIniitliazeTree(
rootID,
0,
nodes,
profilingSummary.initialTreeBaseDurations,
store
);
// Mutate the tree
const commitOperations = store.profilingOperations.get(rootID);
if (commitOperations != null && commitIndex < commitOperations.length) {
const commitTree = updateTree(
{ nodes, rootID },
commitOperations[commitIndex]
);
// Mutate the tree
const commitOperations = store.profilingOperations.get(rootID);
if (commitOperations != null && commitIndex < commitOperations.length) {
const commitTree = updateTree(
{ nodes, rootID },
commitOperations[commitIndex]
);
if (__DEBUG__) {
__printTree(commitTree);
}
commitTrees.push(commitTree);
return commitTree;
if (__DEBUG__) {
__printTree(commitTree);
}
} else {
const previousCommitTree = getCommitTree({
commitIndex: commitIndex - 1,
profilingSummary,
rendererID,
rootID,
store,
});
const commitOperations = store.profilingOperations.get(rootID);
if (commitOperations != null && commitIndex < commitOperations.length) {
const commitTree = updateTree(
previousCommitTree,
commitOperations[commitIndex]
);
if (__DEBUG__) {
__printTree(commitTree);
}
commitTrees.push(commitTree);
return commitTree;
}
} else {
const previousCommitTree = getCommitTree({
commitIndex: commitIndex - 1,
profilingSummary,
rendererID,
rootID,
store,
});
const commitOperations = store.profilingOperations.get(rootID);
if (commitOperations != null && commitIndex < commitOperations.length) {
const commitTree = updateTree(
previousCommitTree,
commitOperations[commitIndex]
);
commitTrees.push(commitTree);
return commitTree;
if (__DEBUG__) {
__printTree(commitTree);
}
commitTrees.push(commitTree);
return commitTree;
}
}
@@ -129,26 +127,27 @@ function recursivelyIniitliazeTree(
initialTreeBaseDurations: Map<number, number>,
store: Store
): void {
const node = ((store.profilingSnapshot.get(id): any): Node);
nodes.set(id, {
id,
children: node.children,
displayName: node.displayName,
key: node.key,
parentID,
treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number),
});
node.children.forEach(childID =>
recursivelyIniitliazeTree(
childID,
const node = store.profilingSnapshot.get(id);
if (node != null) {
nodes.set(id, {
id,
nodes,
initialTreeBaseDurations,
store
)
);
children: node.children,
displayName: node.displayName,
key: node.key,
parentID,
treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number),
});
node.children.forEach(childID =>
recursivelyIniitliazeTree(
childID,
id,
nodes,
initialTreeBaseDurations,
store
)
);
}
}
function updateTree(
@@ -183,7 +182,26 @@ function updateTree(
i = i + 3;
if (type === ElementTypeRoot) {
// No-op
i++; // supportsProfiling flag
debug('Add', `new root fiber ${id}`);
if (nodes.has(id)) {
// The renderer's tree walking approach sometimes mounts the same Fiber twice with Suspense and Lazy.
// For now, we avoid adding it to the tree twice by checking if it's already been mounted.
// Maybe in the future we'll revisit this.
} else {
const node: Node = {
children: [],
displayName: null,
id,
key: null,
parentID: 0,
treeBaseDuration: 0, // This will be updated by a subsequent operation
};
nodes.set(id, node);
}
} else {
parentID = ((operations[i]: any): number);
i++;
@@ -213,14 +231,14 @@ function updateTree(
// For now, we avoid adding it to the tree twice by checking if it's already been mounted.
// Maybe in the future we'll revisit this.
} else {
parentNode = getClonedNode(parentID);
parentNode.children = parentNode.children.concat(id);
debug(
'Add',
`fiber ${id} (${displayName || 'null'}) as child of ${parentID}`
);
parentNode = getClonedNode(parentID);
parentNode.children = parentNode.children.concat(id);
const node: Node = {
children: [],
displayName,

View File

@@ -1,13 +1,24 @@
// @flow
import React from 'react';
import React, { useCallback, useContext } from 'react';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import { BridgeContext, StoreContext } from '../context';
export default function ReloadAndProfileButton() {
// TODO (profiling) Wire up reload button
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const reloadAndProfile = useCallback(() => bridge.send('reloadAndProfile'), [
bridge,
]);
if (!store.supportsReloadAndProfile) {
return null;
}
return (
<Button disabled title="Reload and start profiling">
<Button onClick={reloadAndProfile} title="Reload and start profiling">
<ButtonIcon type="reload" />
</Button>
);