Inject early on when reloading-and-profiling
Brian Vaughn committed
Apr 1, 2019 at 07:48 UTC
2c14f3e88e00a99ad81ba74eecbcda2afc8d8d94
10 files changed
+107
-20
shells/browser/chrome/manifest.json
+6
-1
@@ -27,7 +27,12 @@
27
"devtools_page": "main.html",
28
29
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
30
- "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
30
+ "web_accessible_resources": [
31
+ "main.html",
32
+ "panel.html",
33
+ "build/backend.js",
34
+ "build/renderer.js"
35
+ ],
36
37
"background": {
38
"scripts": ["build/background.js"],
shells/browser/firefox/manifest.json
+6
-1
@@ -33,7 +33,12 @@
33
"devtools_page": "main.html",
34
35
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
36
- "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
36
+ "web_accessible_resources": [
37
+ "main.html",
38
+ "panel.html",
39
+ "build/backend.js",
40
+ "build/renderer.js"
41
+ ],
42
43
"background": {
44
"scripts": ["build/background.js"],
shells/browser/shared/src/GlobalHook.js
+37
-13
@@ -2,13 +2,24 @@
2
3
import nullthrows from 'nullthrows';
4
import { installHook } from 'src/hook';
5
+import { RELOAD_AND_PROFILE_KEY } from 'src/constants';
6
+
7
+function injectCode(code) {
8
+ const script = document.createElement('script');
9
+ script.textContent = code;
10
+
11
+ // This script runs before the <head> element is created,
12
+ // so we add the script to <html> instead.
13
+ nullthrows(document.documentElement).appendChild(script);
14
+ nullthrows(script.parentNode).removeChild(script);
15
+}
16
17
let lastDetectionResult;
18
8
-// We want to detect when a renderer attaches, and notify the "background
9
-// page" (which is shared between tabs and can highlight the React icon).
10
-// Currently we are in "content script" context, so we can't listen
11
-// to the hook directly (it will be injected directly into the page).
19
+// We want to detect when a renderer attaches, and notify the "background page"
20
+// (which is shared between tabs and can highlight the React icon).
21
+// Currently we are in "content script" context, so we can't listen to the hook directly
22
+// (it will be injected directly into the page).
23
// So instead, the hook will use postMessage() to pass message to us here.
24
// And when this happens, we'll send a message to the "background page".
25
window.addEventListener('message', function(evt) {
@@ -51,14 +62,27 @@ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap;
62
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set;
63
`;
64
65
+// If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
66
+if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') {
67
+ const rendererURL = chrome.runtime.getURL('build/renderer.js');
68
+ let rendererCode;
69
+
70
+ // We need to inject in time to catch the initial mount.
71
+ // This means we need to synchronously read the renderer code itself,
72
+ // and synchronously inject it into the page.
73
+ // There are very few ways to actually do this.
74
+ // This seems to be the best approach.
75
+ const request = new XMLHttpRequest();
76
+ request.addEventListener('load', function() {
77
+ rendererCode = this.responseText;
78
+ });
79
+ request.open('GET', rendererURL, false);
80
+ request.send();
81
+ injectCode(rendererCode);
82
+}
83
+
84
// Inject a `__REACT_DEVTOOLS_GLOBAL_HOOK__` global so that React can detect that the
85
// devtools are installed (and skip its suggestion to install the devtools).
56
-const js =
57
- ';(' + installHook.toString() + '(window))' + saveNativeValues + detectReact;
58
-
59
-// This script runs before the <head> element is created, so we add the script
60
-// to <html> instead.
61
-const script = document.createElement('script');
62
-script.textContent = js;
63
-nullthrows(document.documentElement).appendChild(script);
64
-nullthrows(script.parentNode).removeChild(script);
86
+injectCode(
87
+ ';(' + installHook.toString() + '(window))' + saveNativeValues + detectReact
88
+);
shells/browser/shared/src/renderer.js
new
+21
@@ -0,0 +1,21 @@
1
+/**
2
+ * Install the hook on window, which is an event emitter.
3
+ * Note because Chrome content scripts cannot directly modify the window object,
4
+ * we are evaling this function by inserting a script tag.
5
+ * That's why we have to inline the whole event emitter implementation here.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import { attach } from 'src/backend/renderer';
11
+
12
+Object.defineProperty(
13
+ window,
14
+ '__REACT_DEVTOOLS_ATTACH__',
15
+ ({
16
+ enumerable: false,
17
+ get() {
18
+ return attach;
19
+ },
20
+ }: Object)
21
+);
shells/browser/shared/webpack.config.js
+1
@@ -18,6 +18,7 @@ module.exports = {
18
inject: './src/GlobalHook.js',
19
main: './src/main.js',
20
panel: './src/panel.js',
21
+ renderer: './src/renderer.js',
22
},
23
output: {
24
path: __dirname + '/build',
src/backend/agent.js
+1
-3
@@ -1,7 +1,7 @@
1
// @flow
2
3
import EventEmitter from 'events';
4
-import { __DEBUG__ } from '../constants';
4
+import { RELOAD_AND_PROFILE_KEY, __DEBUG__ } from '../constants';
5
import { hideOverlay, showOverlay } from './views/Highlighter';
6
7
import type { RendererID, RendererInterface } from './types';
@@ -38,8 +38,6 @@ type SetInParams = {|
38
value: any,
39
|};
40
41
-const RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile';
42
-
41
export default class Agent extends EventEmitter {
42
_bridge: Bridge = ((null: any): Bridge);
43
_isProfiling: boolean = false;
src/backend/index.js
+10
-2
@@ -33,8 +33,16 @@ export function initBackend(
33
];
34
35
const attachRenderer = (id: number, renderer: ReactRenderer) => {
36
- const rendererInterface = attach(hook, id, renderer, global);
37
- hook.rendererInterfaces.set(id, rendererInterface);
36
+ let rendererInterface = hook.rendererInterfaces.get(id);
37
+
38
+ // Inject any not-yet-injected renderers (if we didn't reload-and-profile)
39
+ if (!rendererInterface) {
40
+ rendererInterface = attach(hook, id, renderer, global);
41
+
42
+ hook.rendererInterfaces.set(id, rendererInterface);
43
+ }
44
+
45
+ // Notify the DevTools frontend about any renderers that were attached early.
46
hook.emit('renderer-attached', {
47
id,
48
renderer,
src/backend/renderer.js
+9
@@ -1595,6 +1595,10 @@ export function attach(
1595
}
1596
1597
function startProfiling() {
1598
+ if (isProfiling) {
1599
+ return;
1600
+ }
1601
+
1602
// Capture initial values as of the time profiling starts.
1603
// It's important we snapshot both the durations and the id-to-root map,
1604
// since either of these may change during the profiling session
@@ -1611,6 +1615,11 @@ export function attach(
1615
isProfiling = false;
1616
}
1617
1618
+ // Automatically start profiling so that we don't miss timing info from initial "mount".
1619
+ if (localStorage.getItem('React::DevTools::reloadAndProfile') === 'true') {
1620
+ startProfiling();
1621
+ }
1622
+
1623
return {
1624
cleanup,
1625
getCommitDetails,
src/constants.js
+2
@@ -5,4 +5,6 @@ export const TREE_OPERATION_REMOVE = 2;
5
export const TREE_OPERATION_RESET_CHILDREN = 3;
6
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
7
8
+export const RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile';
9
+
10
export const __DEBUG__ = false;
src/hook.js
+14
@@ -79,6 +79,20 @@ export function installHook(target: any): DevToolsHook | null {
79
80
hook.emit('renderer', { id, renderer, reactBuildType });
81
82
+ // If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
83
+ // Otherwise the renderer won't yet exist and we can skip this step.
84
+ const attach = target.__REACT_DEVTOOLS_ATTACH__;
85
+ if (typeof attach === 'function') {
86
+ const rendererInterface = attach(hook, id, renderer, target);
87
+ hook.rendererInterfaces.set(id, rendererInterface);
88
+
89
+ /*hook.emit('renderer-attached', {
90
+ id,
91
+ renderer,
92
+ rendererInterface,
93
+ });*/
94
+ }
95
+
96
return id;
97
}
98