@samitouri / QOS-React-2 / commits / 5719454377

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 }