@samitouri / QOS-React / commits / 2c14f3e88e

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