From 2c14f3e88e00a99ad81ba74eecbcda2afc8d8d94 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 1 Apr 2019 07:48:04 -0700 Subject: [PATCH] Inject early on when reloading-and-profiling --- shells/browser/chrome/manifest.json | 7 +++- shells/browser/firefox/manifest.json | 7 +++- shells/browser/shared/src/GlobalHook.js | 50 ++++++++++++++++++------- shells/browser/shared/src/renderer.js | 21 +++++++++++ shells/browser/shared/webpack.config.js | 1 + src/backend/agent.js | 4 +- src/backend/index.js | 12 +++++- src/backend/renderer.js | 9 +++++ src/constants.js | 2 + src/hook.js | 14 +++++++ 10 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 shells/browser/shared/src/renderer.js diff --git a/shells/browser/chrome/manifest.json b/shells/browser/chrome/manifest.json index 2458a4a0f5..6a26c7b21a 100644 --- a/shells/browser/chrome/manifest.json +++ b/shells/browser/chrome/manifest.json @@ -27,7 +27,12 @@ "devtools_page": "main.html", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", - "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"], + "web_accessible_resources": [ + "main.html", + "panel.html", + "build/backend.js", + "build/renderer.js" + ], "background": { "scripts": ["build/background.js"], diff --git a/shells/browser/firefox/manifest.json b/shells/browser/firefox/manifest.json index 0cff52c594..7f6a1281b5 100644 --- a/shells/browser/firefox/manifest.json +++ b/shells/browser/firefox/manifest.json @@ -33,7 +33,12 @@ "devtools_page": "main.html", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", - "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"], + "web_accessible_resources": [ + "main.html", + "panel.html", + "build/backend.js", + "build/renderer.js" + ], "background": { "scripts": ["build/background.js"], diff --git a/shells/browser/shared/src/GlobalHook.js b/shells/browser/shared/src/GlobalHook.js index 9ccab3bb8a..074b289db6 100644 --- a/shells/browser/shared/src/GlobalHook.js +++ b/shells/browser/shared/src/GlobalHook.js @@ -2,13 +2,24 @@ import nullthrows from 'nullthrows'; import { installHook } from 'src/hook'; +import { RELOAD_AND_PROFILE_KEY } from 'src/constants'; + +function injectCode(code) { + const script = document.createElement('script'); + script.textContent = code; + + // This script runs before the element is created, + // so we add the script to instead. + nullthrows(document.documentElement).appendChild(script); + nullthrows(script.parentNode).removeChild(script); +} let lastDetectionResult; -// We want to detect when a renderer attaches, and notify the "background -// page" (which is shared between tabs and can highlight the React icon). -// Currently we are in "content script" context, so we can't listen -// to the hook directly (it will be injected directly into the page). +// We want to detect when a renderer attaches, and notify the "background page" +// (which is shared between tabs and can highlight the React icon). +// Currently we are in "content script" context, so we can't listen to the hook directly +// (it will be injected directly into the page). // So instead, the hook will use postMessage() to pass message to us here. // And when this happens, we'll send a message to the "background page". window.addEventListener('message', function(evt) { @@ -51,14 +62,27 @@ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap; window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set; `; +// If we have just reloaded to profile, we need to inject the renderer interface before the app loads. +if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') { + const rendererURL = chrome.runtime.getURL('build/renderer.js'); + let rendererCode; + + // We need to inject in time to catch the initial mount. + // This means we need to synchronously read the renderer code itself, + // and synchronously inject it into the page. + // There are very few ways to actually do this. + // This seems to be the best approach. + const request = new XMLHttpRequest(); + request.addEventListener('load', function() { + rendererCode = this.responseText; + }); + request.open('GET', rendererURL, false); + request.send(); + injectCode(rendererCode); +} + // Inject a `__REACT_DEVTOOLS_GLOBAL_HOOK__` global so that React can detect that the // devtools are installed (and skip its suggestion to install the devtools). -const js = - ';(' + installHook.toString() + '(window))' + saveNativeValues + detectReact; - -// This script runs before the element is created, so we add the script -// to instead. -const script = document.createElement('script'); -script.textContent = js; -nullthrows(document.documentElement).appendChild(script); -nullthrows(script.parentNode).removeChild(script); +injectCode( + ';(' + installHook.toString() + '(window))' + saveNativeValues + detectReact +); diff --git a/shells/browser/shared/src/renderer.js b/shells/browser/shared/src/renderer.js new file mode 100644 index 0000000000..5fcc38a8ca --- /dev/null +++ b/shells/browser/shared/src/renderer.js @@ -0,0 +1,21 @@ +/** + * Install the hook on window, which is an event emitter. + * Note because Chrome content scripts cannot directly modify the window object, + * we are evaling this function by inserting a script tag. + * That's why we have to inline the whole event emitter implementation here. + * + * @flow + */ + +import { attach } from 'src/backend/renderer'; + +Object.defineProperty( + window, + '__REACT_DEVTOOLS_ATTACH__', + ({ + enumerable: false, + get() { + return attach; + }, + }: Object) +); diff --git a/shells/browser/shared/webpack.config.js b/shells/browser/shared/webpack.config.js index 5c11458ac7..b33c9248dd 100644 --- a/shells/browser/shared/webpack.config.js +++ b/shells/browser/shared/webpack.config.js @@ -18,6 +18,7 @@ module.exports = { inject: './src/GlobalHook.js', main: './src/main.js', panel: './src/panel.js', + renderer: './src/renderer.js', }, output: { path: __dirname + '/build', diff --git a/src/backend/agent.js b/src/backend/agent.js index ef363dbaae..dab48def13 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -1,7 +1,7 @@ // @flow import EventEmitter from 'events'; -import { __DEBUG__ } from '../constants'; +import { RELOAD_AND_PROFILE_KEY, __DEBUG__ } from '../constants'; import { hideOverlay, showOverlay } from './views/Highlighter'; import type { RendererID, RendererInterface } from './types'; @@ -38,8 +38,6 @@ type SetInParams = {| value: any, |}; -const RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile'; - export default class Agent extends EventEmitter { _bridge: Bridge = ((null: any): Bridge); _isProfiling: boolean = false; diff --git a/src/backend/index.js b/src/backend/index.js index 4b08474056..a45c843c13 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -33,8 +33,16 @@ export function initBackend( ]; const attachRenderer = (id: number, renderer: ReactRenderer) => { - const rendererInterface = attach(hook, id, renderer, global); - hook.rendererInterfaces.set(id, rendererInterface); + let rendererInterface = hook.rendererInterfaces.get(id); + + // Inject any not-yet-injected renderers (if we didn't reload-and-profile) + if (!rendererInterface) { + rendererInterface = attach(hook, id, renderer, global); + + hook.rendererInterfaces.set(id, rendererInterface); + } + + // Notify the DevTools frontend about any renderers that were attached early. hook.emit('renderer-attached', { id, renderer, diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 6a43c969ef..f0a3db897a 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -1595,6 +1595,10 @@ export function attach( } function startProfiling() { + if (isProfiling) { + return; + } + // Capture initial values as of the time profiling starts. // It's important we snapshot both the durations and the id-to-root map, // since either of these may change during the profiling session @@ -1611,6 +1615,11 @@ export function attach( isProfiling = false; } + // Automatically start profiling so that we don't miss timing info from initial "mount". + if (localStorage.getItem('React::DevTools::reloadAndProfile') === 'true') { + startProfiling(); + } + return { cleanup, getCommitDetails, diff --git a/src/constants.js b/src/constants.js index 1800fa8001..3523aa5214 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,4 +5,6 @@ export const TREE_OPERATION_REMOVE = 2; export const TREE_OPERATION_RESET_CHILDREN = 3; export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4; +export const RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile'; + export const __DEBUG__ = false; diff --git a/src/hook.js b/src/hook.js index e5d66c40ac..5eaf9b63fd 100644 --- a/src/hook.js +++ b/src/hook.js @@ -79,6 +79,20 @@ export function installHook(target: any): DevToolsHook | null { hook.emit('renderer', { id, renderer, reactBuildType }); + // If we have just reloaded to profile, we need to inject the renderer interface before the app loads. + // Otherwise the renderer won't yet exist and we can skip this step. + const attach = target.__REACT_DEVTOOLS_ATTACH__; + if (typeof attach === 'function') { + const rendererInterface = attach(hook, id, renderer, target); + hook.rendererInterfaces.set(id, rendererInterface); + + /*hook.emit('renderer-attached', { + id, + renderer, + rendererInterface, + });*/ + } + return id; }