Implemented reload-and-profile. Also fixed an couple of minor profiling bugs along the way
Brian Vaughn committed
Mar 27, 2019 at 09:41 UTC
7d24e83989f76e941885a1eaee5efab592b24a3a
7 files changed
+219
-90
shells/browser/shared/src/contentScript.js
+11
-7
@@ -61,13 +61,17 @@ port.onDisconnect.addListener(handleDisconnect);
61
62
window.addEventListener('message', handleMessageFromPage);
63
64
+sayHelloToBackend();
65
+
66
// The backend waits to install the global hook until notified by the content script.
67
// In the event of a page reload, the content script might be loaded before the backend is injected.
68
// Because of this we need to poll the backend until it has been initialized.
67
-const intervalID = setInterval(() => {
68
- if (backendInitialized || backendDisconnected) {
69
- clearInterval(intervalID);
70
- } else {
71
- sayHelloToBackend();
72
- }
73
-}, 500);
69
+if (!backendInitialized) {
70
+ const intervalID = setInterval(() => {
71
+ if (backendInitialized || backendDisconnected) {
72
+ clearInterval(intervalID);
73
+ } else {
74
+ sayHelloToBackend();
75
+ }
76
+ }, 500);
77
+}
shells/browser/shared/src/main.js
+19
-1
@@ -12,6 +12,8 @@ import {
12
} from './utils';
13
import DevTools from 'src/devtools/views/DevTools';
14
15
+const SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling';
16
+
17
let panelCreated = false;
18
19
function createPanelIfReactLoaded() {
@@ -61,8 +63,24 @@ function createPanelIfReactLoaded() {
63
}
64
},
65
});
66
+ bridge.addListener('reloadAppForProfiling', () => {
67
+ localStorage.setItem(SUPPORTS_PROFILING_KEY, 'true');
68
+ chrome.devtools.inspectedWindow.eval('window.location.reload();');
69
+ });
70
65
- store = new Store(bridge);
71
+ // This flag lets us tip the Store off early that we expect to be profiling.
72
+ // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,
73
+ // after a user has clicked the "reload and profile" button.
74
+ let supportsProfiling = false;
75
+ if (localStorage.getItem(SUPPORTS_PROFILING_KEY) === 'true') {
76
+ supportsProfiling = true;
77
+ localStorage.removeItem(SUPPORTS_PROFILING_KEY);
78
+ }
79
+
80
+ store = new Store(bridge, {
81
+ supportsReloadAndProfile: true,
82
+ supportsProfiling,
83
+ });
84
85
// Initialize the backend only once the Store has been initialized.
86
// Otherwise the Store may miss important initial tree op codes.
src/backend/agent.js
+30
@@ -38,11 +38,23 @@ type SetInParams = {|
38
value: any,
39
|};
40
41
+const RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile';
42
+
43
export default class Agent extends EventEmitter {
44
_bridge: Bridge = ((null: any): Bridge);
45
_isProfiling: boolean = false;
46
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
47
48
+ constructor() {
49
+ super();
50
+
51
+ if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') {
52
+ this._isProfiling = true;
53
+
54
+ localStorage.removeItem(RELOAD_AND_PROFILE_KEY);
55
+ }
56
+ }
57
+
58
addBridge(bridge: Bridge) {
59
this._bridge = bridge;
60
@@ -56,6 +68,7 @@ export default class Agent extends EventEmitter {
68
bridge.addListener('overrideHookState', this.overrideHookState);
69
bridge.addListener('overrideProps', this.overrideProps);
70
bridge.addListener('overrideState', this.overrideState);
71
+ bridge.addListener('reloadAndProfile', this.reloadAndProfile);
72
bridge.addListener('selectElement', this.selectElement);
73
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
74
bridge.addListener('startProfiling', this.startProfiling);
@@ -63,6 +76,10 @@ export default class Agent extends EventEmitter {
76
bridge.addListener('stopProfiling', this.stopProfiling);
77
bridge.addListener('shutdown', this.shutdown);
78
bridge.addListener('viewElementSource', this.viewElementSource);
79
+
80
+ if (this._isProfiling) {
81
+ this._bridge.send('profilingStatus', true);
82
+ }
83
}
84
85
getIDForNode(node: Object): number | null {
@@ -179,6 +196,15 @@ export default class Agent extends EventEmitter {
196
}
197
};
198
199
+ reloadAndProfile = () => {
200
+ localStorage.setItem(RELOAD_AND_PROFILE_KEY, 'true');
201
+
202
+ // This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
203
+ // In that case, the shell must also listen for this specific message to know when it needs to reload the app.
204
+ // The agent can't do this in a way that is renderer agnostic.
205
+ this._bridge.send('reloadAppForProfiling');
206
+ };
207
+
208
selectElement = ({ id, rendererID }: InspectSelectParams) => {
209
const renderer = this._rendererInterfaces[rendererID];
210
if (renderer == null) {
@@ -235,6 +261,10 @@ export default class Agent extends EventEmitter {
261
rendererInterface: RendererInterface
262
) {
263
this._rendererInterfaces[rendererID] = rendererInterface;
264
+
265
+ if (this._isProfiling) {
266
+ rendererInterface.startProfiling();
267
+ }
268
}
269
270
shutdown = () => {
src/backend/renderer.js
+28
@@ -639,6 +639,17 @@ export function attach(
639
operation[1] = id;
640
operation[2] = treeBaseDuration;
641
addOperation(operation);
642
+
643
+ const { actualDuration } = fiber;
644
+ if (actualDuration > 0) {
645
+ // If profiling is active, store durations for elements that were rendered during the commit.
646
+ const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData);
647
+ metadata.actualDurations.push(id, actualDuration);
648
+ metadata.maxActualDuration = Math.max(
649
+ metadata.maxActualDuration,
650
+ actualDuration
651
+ );
652
+ }
653
}
654
}
655
@@ -866,6 +877,23 @@ export function attach(
877
// Hydrate all the roots for the first time.
878
hook.getFiberRoots(rendererID).forEach(root => {
879
currentRootID = getFiberID(getPrimaryFiber(root.current));
880
+
881
+ if (isProfiling) {
882
+ // If profiling is active, store commit time and duration, and the current interactions.
883
+ // The frontend may request this information after profiling has stopped.
884
+ currentCommitProfilingMetadata = {
885
+ actualDurations: [],
886
+ commitTime: performance.now() - profilingStartTime,
887
+ interactions: Array.from(root.memoizedInteractions).map(
888
+ (interaction: Interaction) => ({
889
+ ...interaction,
890
+ timestamp: interaction.timestamp - profilingStartTime,
891
+ })
892
+ ),
893
+ maxActualDuration: 0,
894
+ };
895
+ }
896
+
897
mountFiber(root.current, null);
898
flushPendingEvents(root);
899
currentRootID = -1;
src/devtools/store.js
+21
-1
@@ -27,6 +27,11 @@ const debug = (methodName, ...args) => {
27
}
28
};
29
30
+type Config = {|
31
+ supportsReloadAndProfile?: boolean,
32
+ supportsProfiling?: boolean,
33
+|};
34
+
35
type ProfilingSnapshotNode = {|
36
id: number,
37
children: Array<number>,
@@ -86,11 +91,22 @@ export default class Store extends EventEmitter {
91
92
_supportsProfiling: boolean = false;
93
89
- constructor(bridge: Bridge) {
94
+ _supportsReloadAndProfile: boolean = false;
95
+
96
+ constructor(bridge: Bridge, config?: Config) {
97
super();
98
99
debug('constructor', 'subscribing to Bridge');
100
101
+ if (config != null) {
102
+ if (config.supportsProfiling) {
103
+ this._supportsProfiling = true;
104
+ }
105
+ if (config.supportsReloadAndProfile) {
106
+ this._supportsReloadAndProfile = true;
107
+ }
108
+ }
109
+
110
this._bridge = bridge;
111
bridge.addListener('operations', this.onBridgeOperations);
112
bridge.addListener('profilingStatus', this.onProfilingStatus);
@@ -140,6 +156,10 @@ export default class Store extends EventEmitter {
156
return this._supportsProfiling;
157
}
158
159
+ get supportsReloadAndProfile(): boolean {
160
+ return this._supportsReloadAndProfile;
161
+ }
162
+
163
getElementAtIndex(index: number): Element | null {
164
if (index < 0 || index >= this.numElements) {
165
console.warn(
src/devtools/views/Profiler/CommitTreeBuilder.js
+96
-78
@@ -44,71 +44,69 @@ export function getCommitTree({
44
rootID: number,
45
store: Store,
46
|}): CommitTree {
47
- if (store.profilingSnapshot.has(rootID)) {
48
- if (!rootToCommitTreeMap.has(rootID)) {
49
- rootToCommitTreeMap.set(rootID, []);
50
- }
51
-
52
- const commitTrees = ((rootToCommitTreeMap.get(
53
- rootID
54
- ): any): Array<CommitTree>);
47
+ if (!rootToCommitTreeMap.has(rootID)) {
48
+ rootToCommitTreeMap.set(rootID, []);
49
+ }
50
56
- if (commitIndex < commitTrees.length) {
57
- return commitTrees[commitIndex];
58
- }
51
+ const commitTrees = ((rootToCommitTreeMap.get(
52
+ rootID
53
+ ): any): Array<CommitTree>);
54
60
- // Commits are generated sequentially and cached.
61
- // If this is the very first commit, start with the cached snapshot and apply the first mutation.
62
- // Otherwise load (or generate) the previous commit and append a mutation to it.
63
- if (commitIndex === 0) {
64
- const nodes = new Map();
55
+ if (commitIndex < commitTrees.length) {
56
+ return commitTrees[commitIndex];
57
+ }
58
66
- // Construct the initial tree.
67
- recursivelyIniitliazeTree(
68
- rootID,
69
- 0,
70
- nodes,
71
- profilingSummary.initialTreeBaseDurations,
72
- store
73
- );
59
+ // Commits are generated sequentially and cached.
60
+ // If this is the very first commit, start with the cached snapshot and apply the first mutation.
61
+ // Otherwise load (or generate) the previous commit and append a mutation to it.
62
+ if (commitIndex === 0) {
63
+ const nodes = new Map();
64
75
- // Mutate the tree
76
- const commitOperations = store.profilingOperations.get(rootID);
77
- if (commitOperations != null && commitIndex < commitOperations.length) {
78
- const commitTree = updateTree(
79
- { nodes, rootID },
80
- commitOperations[commitIndex]
81
- );
65
+ // Construct the initial tree.
66
+ recursivelyIniitliazeTree(
67
+ rootID,
68
+ 0,
69
+ nodes,
70
+ profilingSummary.initialTreeBaseDurations,
71
+ store
72
+ );
73
83
- if (__DEBUG__) {
84
- __printTree(commitTree);
85
- }
74
+ // Mutate the tree
75
+ const commitOperations = store.profilingOperations.get(rootID);
76
+ if (commitOperations != null && commitIndex < commitOperations.length) {
77
+ const commitTree = updateTree(
78
+ { nodes, rootID },
79
+ commitOperations[commitIndex]
80
+ );
81
87
- commitTrees.push(commitTree);
88
- return commitTree;
82
+ if (__DEBUG__) {
83
+ __printTree(commitTree);
84
}
90
- } else {
91
- const previousCommitTree = getCommitTree({
92
- commitIndex: commitIndex - 1,
93
- profilingSummary,
94
- rendererID,
95
- rootID,
96
- store,
97
- });
98
- const commitOperations = store.profilingOperations.get(rootID);
99
- if (commitOperations != null && commitIndex < commitOperations.length) {
100
- const commitTree = updateTree(
101
- previousCommitTree,
102
- commitOperations[commitIndex]
103
- );
85
105
- if (__DEBUG__) {
106
- __printTree(commitTree);
107
- }
86
+ commitTrees.push(commitTree);
87
+ return commitTree;
88
+ }
89
+ } else {
90
+ const previousCommitTree = getCommitTree({
91
+ commitIndex: commitIndex - 1,
92
+ profilingSummary,
93
+ rendererID,
94
+ rootID,
95
+ store,
96
+ });
97
+ const commitOperations = store.profilingOperations.get(rootID);
98
+ if (commitOperations != null && commitIndex < commitOperations.length) {
99
+ const commitTree = updateTree(
100
+ previousCommitTree,
101
+ commitOperations[commitIndex]
102
+ );
103
109
- commitTrees.push(commitTree);
110
- return commitTree;
104
+ if (__DEBUG__) {
105
+ __printTree(commitTree);
106
}
107
+
108
+ commitTrees.push(commitTree);
109
+ return commitTree;
110
}
111
}
112
@@ -129,26 +127,27 @@ function recursivelyIniitliazeTree(
127
initialTreeBaseDurations: Map<number, number>,
128
store: Store
129
): void {
132
- const node = ((store.profilingSnapshot.get(id): any): Node);
133
-
134
- nodes.set(id, {
135
- id,
136
- children: node.children,
137
- displayName: node.displayName,
138
- key: node.key,
139
- parentID,
140
- treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number),
141
- });
142
-
143
- node.children.forEach(childID =>
144
- recursivelyIniitliazeTree(
145
- childID,
130
+ const node = store.profilingSnapshot.get(id);
131
+ if (node != null) {
132
+ nodes.set(id, {
133
id,
147
- nodes,
148
- initialTreeBaseDurations,
149
- store
150
- )
151
- );
134
+ children: node.children,
135
+ displayName: node.displayName,
136
+ key: node.key,
137
+ parentID,
138
+ treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number),
139
+ });
140
+
141
+ node.children.forEach(childID =>
142
+ recursivelyIniitliazeTree(
143
+ childID,
144
+ id,
145
+ nodes,
146
+ initialTreeBaseDurations,
147
+ store
148
+ )
149
+ );
150
+ }
151
}
152
153
function updateTree(
@@ -183,7 +182,26 @@ function updateTree(
182
i = i + 3;
183
184
if (type === ElementTypeRoot) {
186
- // No-op
185
+ i++; // supportsProfiling flag
186
+
187
+ debug('Add', `new root fiber ${id}`);
188
+
189
+ if (nodes.has(id)) {
190
+ // The renderer's tree walking approach sometimes mounts the same Fiber twice with Suspense and Lazy.
191
+ // For now, we avoid adding it to the tree twice by checking if it's already been mounted.
192
+ // Maybe in the future we'll revisit this.
193
+ } else {
194
+ const node: Node = {
195
+ children: [],
196
+ displayName: null,
197
+ id,
198
+ key: null,
199
+ parentID: 0,
200
+ treeBaseDuration: 0, // This will be updated by a subsequent operation
201
+ };
202
+
203
+ nodes.set(id, node);
204
+ }
205
} else {
206
parentID = ((operations[i]: any): number);
207
i++;
@@ -213,14 +231,14 @@ function updateTree(
231
// For now, we avoid adding it to the tree twice by checking if it's already been mounted.
232
// Maybe in the future we'll revisit this.
233
} else {
216
- parentNode = getClonedNode(parentID);
217
- parentNode.children = parentNode.children.concat(id);
218
-
234
debug(
235
'Add',
236
`fiber ${id} (${displayName || 'null'}) as child of ${parentID}`
237
);
238
239
+ parentNode = getClonedNode(parentID);
240
+ parentNode.children = parentNode.children.concat(id);
241
+
242
const node: Node = {
243
children: [],
244
displayName,
src/devtools/views/Profiler/ReloadAndProfileButton.js
+14
-3
@@ -1,13 +1,24 @@
1
// @flow
2
3
-import React from 'react';
3
+import React, { useCallback, useContext } from 'react';
4
import Button from '../Button';
5
import ButtonIcon from '../ButtonIcon';
6
+import { BridgeContext, StoreContext } from '../context';
7
8
export default function ReloadAndProfileButton() {
8
- // TODO (profiling) Wire up reload button
9
+ const bridge = useContext(BridgeContext);
10
+ const store = useContext(StoreContext);
11
+
12
+ const reloadAndProfile = useCallback(() => bridge.send('reloadAndProfile'), [
13
+ bridge,
14
+ ]);
15
+
16
+ if (!store.supportsReloadAndProfile) {
17
+ return null;
18
+ }
19
+
20
return (
10
- <Button disabled title="Reload and start profiling">
21
+ <Button onClick={reloadAndProfile} title="Reload and start profiling">
22
<ButtonIcon type="reload" />
23
</Button>
24
);