mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #35 from bvaughn/reload-and-profile
Eagerly inject renderer (before document) to support reload-and-profile
This commit is contained in:
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 <head> element is created,
|
||||
// so we add the script to <html> 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 <head> element is created, so we add the script
|
||||
// to <html> 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
|
||||
);
|
||||
|
||||
@@ -78,15 +78,18 @@ function createPanelIfReactLoaded() {
|
||||
// This flag lets us tip the Store off early that we expect to be profiling.
|
||||
// This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,
|
||||
// after a user has clicked the "reload and profile" button.
|
||||
let isProfiling = false;
|
||||
let supportsProfiling = false;
|
||||
if (localStorage.getItem(SUPPORTS_PROFILING_KEY) === 'true') {
|
||||
supportsProfiling = true;
|
||||
isProfiling = true;
|
||||
localStorage.removeItem(SUPPORTS_PROFILING_KEY);
|
||||
}
|
||||
|
||||
const browserName = getBrowserName();
|
||||
|
||||
store = new Store(bridge, {
|
||||
isProfiling,
|
||||
supportsFileDownloads: browserName === 'Chrome',
|
||||
supportsReloadAndProfile: true,
|
||||
supportsProfiling,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* In order to support reload-and-profile functionality, the renderer needs to be injected before any other scripts.
|
||||
* Since it is a complex file (with imports) we can't just toString() it like we do with the hook itself,
|
||||
* So this entry point (one of the web_accessible_resources) provcides a way to eagerly inject it.
|
||||
* The hook will look for the presence of a global __REACT_DEVTOOLS_ATTACH__ and attach an injected renderer early.
|
||||
* The normal case (not a reload-and-profile) will not make use of this entry point though.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { attach } from 'src/backend/renderer';
|
||||
|
||||
Object.defineProperty(
|
||||
window,
|
||||
'__REACT_DEVTOOLS_ATTACH__',
|
||||
({
|
||||
enumerable: false,
|
||||
get() {
|
||||
return attach;
|
||||
},
|
||||
}: Object)
|
||||
);
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
+15
-3
@@ -23,7 +23,10 @@ export function initBackend(
|
||||
rendererInterface: RendererInterface,
|
||||
}) => {
|
||||
agent.setRendererInterface(id, rendererInterface);
|
||||
rendererInterface.walkTree();
|
||||
|
||||
// Now that the Store and the renderer interface are connected,
|
||||
// it's time to flush the pending operation codes to the frontend.
|
||||
rendererInterface.flushInitialOperations();
|
||||
}
|
||||
),
|
||||
|
||||
@@ -33,8 +36,17 @@ 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 new renderers.
|
||||
// This includes any that were attached early (via __REACT_DEVTOOLS_ATTACH__).
|
||||
hook.emit('renderer-attached', {
|
||||
id,
|
||||
renderer,
|
||||
|
||||
+60
-25
@@ -18,6 +18,7 @@ import { getDisplayName, utfEncodeString } from '../utils';
|
||||
import { cleanForBridge, copyWithSet, setInObject } from './utils';
|
||||
import {
|
||||
__DEBUG__,
|
||||
RELOAD_AND_PROFILE_KEY,
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_RESET_CHILDREN,
|
||||
@@ -566,6 +567,7 @@ export function attach(
|
||||
}
|
||||
|
||||
let pendingOperations: Uint32Array = new Uint32Array(0);
|
||||
let pendingOperationsQueue: Array<Uint32Array> | null = [];
|
||||
|
||||
function addOperation(
|
||||
newAction: Uint32Array,
|
||||
@@ -603,7 +605,16 @@ export function attach(
|
||||
// Let the frontend know about tree operations.
|
||||
// The first value in this array will identify which root it corresponds to,
|
||||
// so we do no longer need to dispatch a separate root-committed event.
|
||||
hook.emit('operations', pendingOperations);
|
||||
if (pendingOperationsQueue !== null) {
|
||||
// Until the frontend has been connected, store the tree operations.
|
||||
// This will let us avoid walking the tree later when the frontend connects,
|
||||
// and it enables the Profiler's reload-and-profile functionality to work as well.
|
||||
pendingOperationsQueue.push(pendingOperations);
|
||||
} else {
|
||||
// If we've already connected to the frontend, just pass the operations through.
|
||||
hook.emit('operations', pendingOperations);
|
||||
}
|
||||
|
||||
pendingOperations = new Uint32Array(0);
|
||||
}
|
||||
|
||||
@@ -913,31 +924,46 @@ export function attach(
|
||||
// We don't patch any methods so there is no cleanup.
|
||||
}
|
||||
|
||||
function walkTree() {
|
||||
// Hydrate all the roots for the first time.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
currentRootID = getFiberID(getPrimaryFiber(root.current));
|
||||
function flushInitialOperations() {
|
||||
const localPendingOperationsQueue = pendingOperationsQueue;
|
||||
|
||||
if (isProfiling) {
|
||||
// If profiling is active, store commit time and duration, and the current interactions.
|
||||
// The frontend may request this information after profiling has stopped.
|
||||
currentCommitProfilingMetadata = {
|
||||
actualDurations: [],
|
||||
commitTime: performance.now() - profilingStartTime,
|
||||
interactions: Array.from(root.memoizedInteractions).map(
|
||||
(interaction: Interaction) => ({
|
||||
...interaction,
|
||||
timestamp: interaction.timestamp - profilingStartTime,
|
||||
})
|
||||
),
|
||||
maxActualDuration: 0,
|
||||
};
|
||||
}
|
||||
pendingOperationsQueue = null;
|
||||
|
||||
mountFiber(root.current, null);
|
||||
flushPendingEvents(root);
|
||||
currentRootID = -1;
|
||||
});
|
||||
if (
|
||||
localPendingOperationsQueue !== null &&
|
||||
localPendingOperationsQueue.length > 0
|
||||
) {
|
||||
// We may have already queued up some operations before the frontend connected
|
||||
// If so, let the frontend know about them.
|
||||
localPendingOperationsQueue.forEach(pendingOperations => {
|
||||
hook.emit('operations', pendingOperations);
|
||||
});
|
||||
} else {
|
||||
// If we have not been profiling, then we can just walk the tree and build up its current state as-is.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
currentRootID = getFiberID(getPrimaryFiber(root.current));
|
||||
|
||||
if (isProfiling) {
|
||||
// If profiling is active, store commit time and duration, and the current interactions.
|
||||
// The frontend may request this information after profiling has stopped.
|
||||
currentCommitProfilingMetadata = {
|
||||
actualDurations: [],
|
||||
commitTime: performance.now() - profilingStartTime,
|
||||
interactions: Array.from(root.memoizedInteractions).map(
|
||||
(interaction: Interaction) => ({
|
||||
...interaction,
|
||||
timestamp: interaction.timestamp - profilingStartTime,
|
||||
})
|
||||
),
|
||||
maxActualDuration: 0,
|
||||
};
|
||||
}
|
||||
|
||||
mountFiber(root.current, null);
|
||||
flushPendingEvents(root);
|
||||
currentRootID = -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommitFiberUnmount(fiber) {
|
||||
@@ -1595,6 +1621,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,8 +1641,14 @@ export function attach(
|
||||
isProfiling = false;
|
||||
}
|
||||
|
||||
// Automatically start profiling so that we don't miss timing info from initial "mount".
|
||||
if (localStorage.getItem(RELOAD_AND_PROFILE_KEY) === 'true') {
|
||||
startProfiling();
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
flushInitialOperations,
|
||||
getCommitDetails,
|
||||
getFiberIDFromNative,
|
||||
getInteractions,
|
||||
@@ -1631,6 +1667,5 @@ export function attach(
|
||||
setInState,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
walkTree,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export type ProfilingSummary = {|
|
||||
|
||||
export type RendererInterface = {
|
||||
cleanup: () => void,
|
||||
flushInitialOperations: () => void,
|
||||
getCommitDetails: (rootID: number, commitIndex: number) => CommitDetails,
|
||||
getNativeFromReactElement?: ?(component: Fiber) => ?NativeType,
|
||||
getFiberIDFromNative: (
|
||||
@@ -108,7 +109,6 @@ export type RendererInterface = {
|
||||
setInState: (id: number, path: Array<string | number>, value: any) => void,
|
||||
startProfiling: () => void,
|
||||
stopProfiling: () => void,
|
||||
walkTree: () => void,
|
||||
};
|
||||
|
||||
export type Handler = (data: any) => void;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -103,10 +103,14 @@ export default class Store extends EventEmitter {
|
||||
|
||||
if (config != null) {
|
||||
const {
|
||||
isProfiling,
|
||||
supportsFileDownloads,
|
||||
supportsProfiling,
|
||||
supportsReloadAndProfile,
|
||||
} = config;
|
||||
if (isProfiling) {
|
||||
this._isProfiling = true;
|
||||
}
|
||||
if (supportsFileDownloads) {
|
||||
this._supportsFileDownloads = true;
|
||||
}
|
||||
|
||||
@@ -112,14 +112,9 @@ export function getCommitTree({
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
throw Error(
|
||||
`getCommitTree(): Unable to reconstruct tree for root "${rootID}" and commit ${commitIndex}`
|
||||
);
|
||||
|
||||
return {
|
||||
nodes: new Map(),
|
||||
rootID,
|
||||
};
|
||||
}
|
||||
|
||||
function recursivelyIniitliazeTree(
|
||||
|
||||
@@ -56,6 +56,11 @@ export function getChartData({
|
||||
idToDepthMap.set(id, currentDepth);
|
||||
|
||||
const node = ((nodes.get(id): any): Node);
|
||||
|
||||
if (node == null) {
|
||||
throw Error(`Could not find node with id "${id}" in commit tree`);
|
||||
}
|
||||
|
||||
const name = node.displayName || 'Unknown';
|
||||
|
||||
const selfDuration = calculateSelfDuration(id, commitTree, commitDetails);
|
||||
|
||||
@@ -130,7 +130,7 @@ function ProfilerContextController({ children }: Props) {
|
||||
const [
|
||||
isCommitFilterEnabled,
|
||||
setIsCommitFilterEnabled,
|
||||
] = useLocalStorage<boolean>('isCommitFilterEnabled', false);
|
||||
] = useLocalStorage<boolean>('React::DevTools::isCommitFilterEnabled', false);
|
||||
const [minCommitDuration, setMinCommitDuration] = useLocalStorage<number>(
|
||||
'minCommitDuration',
|
||||
0
|
||||
|
||||
@@ -41,6 +41,10 @@ export function getChartData({
|
||||
actualDurations.forEach((actualDuration, id) => {
|
||||
const node = ((nodes.get(id): any): Node);
|
||||
|
||||
if (node == null) {
|
||||
throw Error(`Could not find node with id "${id}" in commit tree`);
|
||||
}
|
||||
|
||||
// Don't show the root node in this chart.
|
||||
if (node.parentID === 0) {
|
||||
return;
|
||||
|
||||
@@ -41,10 +41,13 @@ function SettingsContextController({
|
||||
settingsPortalContainer,
|
||||
}: Props) {
|
||||
const [displayDensity, setDisplayDensity] = useLocalStorage<DisplayDensity>(
|
||||
'displayDensity',
|
||||
'React::DevTools::displayDensity',
|
||||
'compact'
|
||||
);
|
||||
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'auto');
|
||||
const [theme, setTheme] = useLocalStorage<Theme>(
|
||||
'React::DevTools::theme',
|
||||
'auto'
|
||||
);
|
||||
|
||||
const documentElements = useMemo<DocumentElements>(() => {
|
||||
const array: Array<HTMLElement> = [
|
||||
|
||||
@@ -77,6 +77,14 @@ export function installHook(target: any): DevToolsHook | null {
|
||||
? 'deadcode'
|
||||
: detectReactBuildType(renderer);
|
||||
|
||||
// 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', { id, renderer, reactBuildType });
|
||||
|
||||
return id;
|
||||
|
||||
Reference in New Issue
Block a user