mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Implemented reload-and-profile. Also fixed an couple of minor profiling bugs along the way
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user