@samitouri / QOS-React / commits / 7d24e83989

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 );