Persist profiling data after navigation
Brian Vaughn committed
May 22, 2019 at 20:00 UTC
57194543777d966a01e0dfbaee54c68ee2a44c56
8 files changed
+143
-33
shells/browser/chrome/manifest.json
+1
@@ -45,6 +45,7 @@
45
"<all_urls>",
46
"background",
47
"tabs",
48
+ "webNavigation",
49
"file:///*",
50
"http://*/*",
51
"https://*/*"
shells/browser/firefox/manifest.json
+1
@@ -48,6 +48,7 @@
48
"<all_urls>",
49
"activeTab",
50
"tabs",
51
+ "webNavigation",
52
"file:///*",
53
"http://*/*",
54
"https://*/*"
shells/browser/shared/src/main.js
+19
-5
@@ -51,6 +51,8 @@ function createPanelIfReactLoaded() {
51
let bridge = null;
52
let store = null;
53
54
+ let profilingData = null;
55
+
56
let componentsPortalContainer = null;
57
let profilerPortalContainer = null;
58
let settingsPortalContainer = null;
@@ -120,6 +122,7 @@ function createPanelIfReactLoaded() {
122
supportsReloadAndProfile: true,
123
supportsProfiling,
124
});
125
+ store.profilerStore.profilingData = profilingData;
126
127
// Initialize the backend only once the Store has been initialized.
128
// Otherwise the Store may miss important initial tree op codes.
@@ -278,18 +281,29 @@ function createPanelIfReactLoaded() {
281
282
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
283
281
- // Shutdown bridge and re-initialize DevTools panel when a new page is loaded.
284
+ // Shutdown bridge before a new page is loaded.
285
+ chrome.webNavigation.onBeforeNavigate.addListener(
286
+ function onBeforeNavigate(details) {
287
+ // `bridge.shutdown()` will remove all listeners we added, so we don't have to.
288
+ bridge.shutdown();
289
+
290
+ profilingData = store.profilerStore.profilingData;
291
+ }
292
+ );
293
+
294
+ // Re-initialize DevTools panel when a new page is loaded.
295
chrome.devtools.network.onNavigated.addListener(function onNavigated() {
296
// Re-initialize saved filters on navigation,
297
// since global values stored on window get reset in this case.
298
initializeSavedComponentFilters();
299
287
- // `bridge.shutdown()` will remove all listeners we added, so we don't have to.
288
- bridge.shutdown();
289
-
300
// It's easiest to recreate the DevTools panel (to clean up potential stale state).
301
// We can revisit this in the future as a small optimization.
292
- flushSync(() => root.unmount(initBridgeAndStore));
302
+ flushSync(() => {
303
+ root.unmount(() => {
304
+ initBridgeAndStore();
305
+ });
306
+ });
307
});
308
}
309
);
src/__tests__/__snapshots__/profilerContext-test.js.snap
new
+6
@@ -0,0 +1,6 @@
1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`ProfilerContext should gracefully handle an empty profiling session: 1: mount 1`] = `
4
+[root]
5
+ <Example>
6
+`;
src/__tests__/profilerContext-test.js
new
+78
@@ -0,0 +1,78 @@
1
+// @flow
2
+
3
+import typeof ReactTestRenderer from 'react-test-renderer';
4
+import type { Element } from 'src/devtools/views/Components/types';
5
+import type Bridge from 'src/bridge';
6
+import type Store from 'src/devtools/store';
7
+
8
+describe('ProfilerContext', () => {
9
+ let React;
10
+ let ReactDOM;
11
+ let TestRenderer: ReactTestRenderer;
12
+ let bridge: Bridge;
13
+ let store: Store;
14
+ let utils;
15
+
16
+ let BridgeContext;
17
+ let ProfilerContext;
18
+ let ProfilerContextController;
19
+ let StoreContext;
20
+ let TreeContextController;
21
+
22
+ beforeEach(() => {
23
+ utils = require('./utils');
24
+ utils.beforeEachProfiling();
25
+
26
+ bridge = global.bridge;
27
+ store = global.store;
28
+ store.collapseNodesByDefault = false;
29
+
30
+ React = require('react');
31
+ ReactDOM = require('react-dom');
32
+ TestRenderer = utils.requireTestRenderer();
33
+
34
+ BridgeContext = require('src/devtools/views/context').BridgeContext;
35
+ ProfilerContext = require('src/devtools/views/Profiler/ProfilerContext')
36
+ .ProfilerContext;
37
+ ProfilerContextController = require('src/devtools/views/Profiler/ProfilerContext')
38
+ .ProfilerContextController;
39
+ StoreContext = require('src/devtools/views/context').StoreContext;
40
+ TreeContextController = require('src/devtools/views/Components/TreeContext')
41
+ .TreeContextController;
42
+ });
43
+
44
+ const Contexts = ({
45
+ children = null,
46
+ defaultSelectedElementID = null,
47
+ defaultSelectedElementIndex = null,
48
+ }) => (
49
+ <BridgeContext.Provider value={bridge}>
50
+ <StoreContext.Provider value={store}>
51
+ <TreeContextController
52
+ defaultSelectedElementID={defaultSelectedElementID}
53
+ defaultSelectedElementIndex={defaultSelectedElementIndex}
54
+ >
55
+ <ProfilerContextController>{children}</ProfilerContextController>
56
+ </TreeContextController>
57
+ </StoreContext.Provider>
58
+ </BridgeContext.Provider>
59
+ );
60
+
61
+ it('should gracefully handle an empty profiling session', () => {
62
+ const Example = () => {
63
+ const [count] = React.useState(1);
64
+ return count;
65
+ };
66
+
67
+ const container = document.createElement('div');
68
+ utils.act(() => ReactDOM.render(<Example foo={1} bar="abc" />, container));
69
+ expect(store).toMatchSnapshot('1: mount');
70
+
71
+ utils.act(() => store.profilerStore.startProfiling());
72
+ utils.act(() => store.profilerStore.stopProfiling());
73
+
74
+ utils.act(() => {
75
+ TestRenderer.create(<Contexts />);
76
+ });
77
+ });
78
+});
src/bridge.js
+8
-3
@@ -57,9 +57,6 @@ export default class Bridge extends EventEmitter {
57
return;
58
}
59
60
- // Mark this bridge as destroyed, i.e. disable its public API.
61
- this._isShutdown = true;
62
-
60
// Disable the API inherited from EventEmitter that can add more listeners and send more messages.
61
this.addListener = function() {};
62
this.emit = function() {};
@@ -86,9 +83,17 @@ export default class Bridge extends EventEmitter {
83
// Make sure once again that there is no dangling timer.
84
clearTimeout(this._timeoutID);
85
this._timeoutID = null;
86
+
87
+ // Mark this bridge as destroyed, i.e. disable its public API.
88
+ this._isShutdown = true;
89
}
90
91
_flush = () => {
92
+ if (this._isShutdown) {
93
+ console.warn(`Cannot flush a Bridge that has been shutdown.`);
94
+ return;
95
+ }
96
+
97
// This method is used after the bridge is marked as destroyed in shutdown sequence,
98
// so we do not bail out if the bridge marked as destroyed.
99
// It is a private method that the bridge ensures is only called at the right times.
src/devtools/views/Profiler/Profiler.js
+9
-9
@@ -38,15 +38,7 @@ function Profiler({ supportsProfiling }: Props) {
38
} = useContext(ProfilerContext);
39
40
let view = null;
41
- if (!supportsProfiling) {
42
- view = <ProfilingNotSupported />;
43
- } else if (isProfiling) {
44
- view = <RecordingInProgress />;
45
- } else if (isProcessingData) {
46
- view = <ProcessingData />;
47
- } else if (!hasProfilingData) {
48
- view = <NoProfilingData />;
49
- } else {
41
+ if (hasProfilingData) {
42
switch (selectedTabID) {
43
case 'flame-chart':
44
view = <CommitFlamegraph />;
@@ -60,6 +52,14 @@ function Profiler({ supportsProfiling }: Props) {
52
default:
53
break;
54
}
55
+ } else if (isProfiling) {
56
+ view = <RecordingInProgress />;
57
+ } else if (isProcessingData) {
58
+ view = <ProcessingData />;
59
+ } else if (supportsProfiling) {
60
+ view = <NoProfilingData />;
61
+ } else {
62
+ view = <ProfilingNotSupported />;
63
}
64
65
let sidebar = null;
src/devtools/views/Profiler/ProfilerContext.js
+21
-16
@@ -118,25 +118,30 @@ function ProfilerContextController({ children }: Props) {
118
profilingData,
119
} = useSubscription<StoreProfilingState, Store>(subscription);
120
121
+ const [prevProfilingData, setPrevProfilingData] = useState();
122
const [rootID, setRootID] = useState<number | null>(null);
123
123
- const dataForRoots =
124
- profilingData !== null ? profilingData.dataForRoots : null;
125
- if (dataForRoots != null) {
126
- const firstRootID = dataForRoots.keys().next().value || null;
124
+ if (prevProfilingData !== profilingData) {
125
+ setPrevProfilingData(profilingData);
126
128
- if (rootID === null || !dataForRoots.has(rootID)) {
129
- let selectedElementRootID = null;
130
- if (selectedElementID !== null) {
131
- selectedElementRootID = store.getRootIDForElement(selectedElementID);
132
- }
133
- if (
134
- selectedElementRootID !== null &&
135
- dataForRoots.has(selectedElementRootID)
136
- ) {
137
- setRootID(selectedElementRootID);
138
- } else {
139
- setRootID(firstRootID);
127
+ const dataForRoots =
128
+ profilingData !== null ? profilingData.dataForRoots : null;
129
+ if (dataForRoots != null) {
130
+ const firstRootID = dataForRoots.keys().next().value || null;
131
+
132
+ if (rootID === null || !dataForRoots.has(rootID)) {
133
+ let selectedElementRootID = null;
134
+ if (selectedElementID !== null) {
135
+ selectedElementRootID = store.getRootIDForElement(selectedElementID);
136
+ }
137
+ if (
138
+ selectedElementRootID !== null &&
139
+ dataForRoots.has(selectedElementRootID)
140
+ ) {
141
+ setRootID(selectedElementRootID);
142
+ } else {
143
+ setRootID(firstRootID);
144
+ }
145
}
146
}
147
}