From 363bf0cceb08552ac76cfa67a0ad55b81886aaf0 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 2 Oct 2019 11:55:38 -0700 Subject: [PATCH 01/12] Added trace updates feature (DOM only) --- .../react-devtools-extensions/src/main.js | 1 + .../react-devtools-inline/src/frontend.js | 2 +- .../src/backend/agent.js | 40 +++++ .../src/backend/index.js | 1 + .../src/backend/legacy/renderer.js | 5 + .../src/backend/renderer.js | 47 +++++- .../src/backend/types.js | 3 +- .../src/backend/views/Highlighter/Overlay.js | 120 +-------------- .../src/backend/views/TraceUpdates/canvas.js | 107 +++++++++++++ .../src/backend/views/TraceUpdates/index.js | 143 ++++++++++++++++++ .../src/backend/views/utils.js | 127 ++++++++++++++++ packages/react-devtools-shared/src/bridge.js | 1 + .../src/devtools/store.js | 11 +- .../views/Settings/GeneralSettings.js | 43 ++++-- .../views/Settings/SettingsContext.js | 18 +++ 15 files changed, 536 insertions(+), 133 deletions(-) create mode 100644 packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js create mode 100644 packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js create mode 100644 packages/react-devtools-shared/src/backend/views/utils.js diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index d535ba7303b47..2d84567299c04 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -129,6 +129,7 @@ function createPanelIfReactLoaded() { isProfiling, supportsReloadAndProfile: isChrome, supportsProfiling, + supportsTraceUpdates: true, }); store.profilerStore.profilingData = profilingData; diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index 286d1c28a65f0..86f3300d4b1c2 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -64,7 +64,7 @@ export function initialize( }, }); - const store: Store = new Store(bridge); + const store: Store = new Store(bridge, {supportsTraceUpdates: true}); const ForwardRef = forwardRef((props, ref) => ( diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 3e73c0f750e63..b8ee6510507b9 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -21,10 +21,15 @@ import { sessionStorageSetItem, } from 'react-devtools-shared/src/storage'; import setupHighlighter from './views/Highlighter'; +import { + initialize as setupTraceUpdates, + toggleEnabled as toggleTraceUpdatesEnabled, +} from './views/TraceUpdates'; import {patch as patchConsole, unpatch as unpatchConsole} from './console'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type { + FindNativeNodesForFiberID, InstanceAndStyle, NativeType, OwnersList, @@ -87,6 +92,7 @@ export default class Agent extends EventEmitter<{| hideNativeHighlight: [], showNativeHighlight: [NativeType], shutdown: [], + traceUpdates: [Map], |}> { _bridge: BackendBridge; _isProfiling: boolean = false; @@ -143,6 +149,7 @@ export default class Agent extends EventEmitter<{| this.updateAppendComponentStack, ); bridge.addListener('updateComponentFilters', this.updateComponentFilters); + bridge.addListener('updateTraceUpdates', this.updateTraceUpdates); bridge.addListener('viewElementSource', this.viewElementSource); if (this._isProfiling) { @@ -159,6 +166,7 @@ export default class Agent extends EventEmitter<{| bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported); setupHighlighter(bridge, this); + setupTraceUpdates(this); } get rendererInterfaces(): {[key: RendererID]: RendererInterface} { @@ -196,6 +204,22 @@ export default class Agent extends EventEmitter<{| return null; } + getNodesForID(id: number): Array | null { + for (let rendererID in this._rendererInterfaces) { + const renderer = ((this._rendererInterfaces[ + (rendererID: any) + ]: any): RendererInterface); + + try { + const nodes = renderer.findNativeNodesForFiberID(id); + if (nodes != null) { + return nodes; + } + } catch (error) {} + } + return null; + } + getProfilingData = ({rendererID}: {|rendererID: RendererID|}) => { const renderer = this._rendererInterfaces[rendererID]; if (renderer == null) { @@ -407,6 +431,16 @@ export default class Agent extends EventEmitter<{| } }; + updateTraceUpdates = (isEnabled: boolean) => { + toggleTraceUpdatesEnabled(isEnabled); + for (let rendererID in this._rendererInterfaces) { + const renderer = ((this._rendererInterfaces[ + (rendererID: any) + ]: any): RendererInterface); + renderer.toggleTraceUpdatesEnabled(isEnabled); + } + }; + viewElementSource = ({id, rendererID}: ElementAndRendererID) => { const renderer = this._rendererInterfaces[rendererID]; if (renderer == null) { @@ -416,6 +450,12 @@ export default class Agent extends EventEmitter<{| } }; + onTraceUpdates = ( + highlightedNodesMap: Map, + ) => { + this.emit('traceUpdates', highlightedNodesMap); + }; + onHookOperations = (operations: Array) => { if (__DEBUG__) { debug('onHookOperations', operations); diff --git a/packages/react-devtools-shared/src/backend/index.js b/packages/react-devtools-shared/src/backend/index.js index 73e6c9f8f2518..4af370046f073 100644 --- a/packages/react-devtools-shared/src/backend/index.js +++ b/packages/react-devtools-shared/src/backend/index.js @@ -44,6 +44,7 @@ export function initBackend( }), hook.sub('operations', agent.onHookOperations), + hook.sub('traceUpdates', agent.onTraceUpdates), // TODO Add additional subscriptions required for profiling mode ]; diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index bc2fb30ccf8d2..1fb76e217cfdb 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -912,6 +912,10 @@ export function attach( // Not implemented. } + function toggleTraceUpdatesEnabled(enabled: boolean) { + // Not implemented. + } + function setTrackedPath(path: Array | null) { // Not implemented. } @@ -949,5 +953,6 @@ export function attach( startProfiling, stopProfiling, updateComponentFilters, + toggleTraceUpdatesEnabled, }; } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 7a5268f632bd9..17fffd6d80aed 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -54,6 +54,7 @@ import type { ChangeDescription, CommitDataBackend, DevToolsHook, + FindNativeNodesForFiberID, InspectedElement, InspectedElementPayload, InstanceAndStyle, @@ -532,6 +533,10 @@ export function attach( const hideElementsWithPaths: Set = new Set(); const hideElementsWithTypes: Set = new Set(); + // Highlight updates + let traceUpdatesEnabled: boolean = false; + let highlightedNodesMap: Map = new Map(); + function applyComponentFilters(componentFilters: Array) { hideElementsWithTypes.clear(); hideElementsWithDisplayNames.clear(); @@ -1124,6 +1129,23 @@ export function attach( return stringID; } + function recordTraceUpdate(fiber: Fiber) { + // Highlighting every host node would be too noisy. + // We highlight user components and context consumers. + // Without consumers, a context update that renders only host nodes directly wouldn't highlight at all. + const elementType = getElementTypeForFiber(fiber); + if ( + elementType === ElementTypeFunction || + elementType === ElementTypeClass || + elementType === ElementTypeContext + ) { + highlightedNodesMap.set( + getFiberID(getPrimaryFiber(fiber)), + findNativeNodesForFiberID, + ); + } + } + function recordMount(fiber: Fiber, parentFiber: Fiber | null) { const isRoot = fiber.tag === HostRoot; const id = getFiberID(getPrimaryFiber(fiber)); @@ -1244,6 +1266,10 @@ export function attach( recordMount(fiber, parentFiber); } + if (traceUpdatesEnabled) { + recordTraceUpdate(fiber); + } + const isTimedOutSuspense = fiber.tag === ReactTypeOfWork.SuspenseComponent && fiber.memoizedState !== null; @@ -1435,11 +1461,17 @@ export function attach( debug('updateFiberRecursively()', nextFiber, parentFiber); } + const didRender = didFiberRender(prevFiber, nextFiber); + + if (traceUpdatesEnabled && didRender) { + recordTraceUpdate(nextFiber); + } + if ( mostRecentlyInspectedElement !== null && mostRecentlyInspectedElement.id === getFiberID(getPrimaryFiber(nextFiber)) && - didFiberRender(prevFiber, nextFiber) + didRender ) { // If this Fiber has updated, clear cached inspected data. // If it is inspected again, it may need to be re-run to obtain updated hooks values. @@ -1671,6 +1703,10 @@ export function attach( mightBeOnTrackedPath = true; } + if (traceUpdatesEnabled) { + highlightedNodesMap.clear(); + } + // Checking root.memoizedInteractions handles multi-renderer edge-case- // where some v16 renderers support profiling and others don't. const isProfilingSupported = root.memoizedInteractions != null; @@ -1738,6 +1774,10 @@ export function attach( // We're done here. flushPendingEvents(root); + if (traceUpdatesEnabled) { + hook.emit('traceUpdates', highlightedNodesMap); + } + currentRootID = -1; } @@ -3015,6 +3055,10 @@ export function attach( } }; + function toggleTraceUpdatesEnabled(isEnabled: boolean): void { + traceUpdatesEnabled = isEnabled; + } + return { cleanup, findNativeNodesForFiberID, @@ -3040,5 +3084,6 @@ export function attach( startProfiling, stopProfiling, updateComponentFilters, + toggleTraceUpdatesEnabled, }; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index bf6a4c7286711..fe27b094024c3 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -255,7 +255,8 @@ export type RendererInterface = { setTrackedPath: (path: Array | null) => void, startProfiling: (recordChangeDescriptions: boolean) => void, stopProfiling: () => void, - updateComponentFilters: (somponentFilters: Array) => void, + updateComponentFilters: (componentFilters: Array) => void, + toggleTraceUpdatesEnabled: (enabled: boolean) => void, }; export type Handler = (data: any) => void; diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js b/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js index 4cf78b264cd87..cc2ac643d5956 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js @@ -8,15 +8,9 @@ */ import assign from 'object-assign'; +import {getElementDimensions, getNestedBoundingClientRect} from '../utils'; -type Rect = { - bottom: number, - height: number, - left: number, - right: number, - top: number, - width: number, -}; +import type {Rect} from '../utils'; type Box = {|top: number, left: number, width: number, height: number|}; @@ -333,116 +327,6 @@ function findTipPos(dims, bounds, tipSize) { }; } -export function getElementDimensions(domElement: Element) { - const calculatedStyle = window.getComputedStyle(domElement); - return { - borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10), - borderRight: parseInt(calculatedStyle.borderRightWidth, 10), - borderTop: parseInt(calculatedStyle.borderTopWidth, 10), - borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10), - marginLeft: parseInt(calculatedStyle.marginLeft, 10), - marginRight: parseInt(calculatedStyle.marginRight, 10), - marginTop: parseInt(calculatedStyle.marginTop, 10), - marginBottom: parseInt(calculatedStyle.marginBottom, 10), - paddingLeft: parseInt(calculatedStyle.paddingLeft, 10), - paddingRight: parseInt(calculatedStyle.paddingRight, 10), - paddingTop: parseInt(calculatedStyle.paddingTop, 10), - paddingBottom: parseInt(calculatedStyle.paddingBottom, 10), - }; -} - -// Get the window object for the document that a node belongs to, -// or return null if it cannot be found (node not attached to DOM, -// etc). -function getOwnerWindow(node: HTMLElement): typeof window | null { - if (!node.ownerDocument) { - return null; - } - return node.ownerDocument.defaultView; -} - -// Get the iframe containing a node, or return null if it cannot -// be found (node not within iframe, etc). -function getOwnerIframe(node: HTMLElement): HTMLElement | null { - const nodeWindow = getOwnerWindow(node); - if (nodeWindow) { - return nodeWindow.frameElement; - } - return null; -} - -// Get a bounding client rect for a node, with an -// offset added to compensate for its border. -function getBoundingClientRectWithBorderOffset(node: HTMLElement) { - const dimensions = getElementDimensions(node); - return mergeRectOffsets([ - node.getBoundingClientRect(), - { - top: dimensions.borderTop, - left: dimensions.borderLeft, - bottom: dimensions.borderBottom, - right: dimensions.borderRight, - // This width and height won't get used by mergeRectOffsets (since this - // is not the first rect in the array), but we set them so that this - // object typechecks as a ClientRect. - width: 0, - height: 0, - }, - ]); -} - -// Add together the top, left, bottom, and right properties of -// each ClientRect, but keep the width and height of the first one. -function mergeRectOffsets(rects: Array): Rect { - return rects.reduce((previousRect, rect) => { - if (previousRect == null) { - return rect; - } - - return { - top: previousRect.top + rect.top, - left: previousRect.left + rect.left, - width: previousRect.width, - height: previousRect.height, - bottom: previousRect.bottom + rect.bottom, - right: previousRect.right + rect.right, - }; - }); -} - -// Calculate a boundingClientRect for a node relative to boundaryWindow, -// taking into account any offsets caused by intermediate iframes. -function getNestedBoundingClientRect( - node: HTMLElement, - boundaryWindow: typeof window, -): Rect { - const ownerIframe = getOwnerIframe(node); - if (ownerIframe && ownerIframe !== boundaryWindow) { - const rects = [node.getBoundingClientRect()]; - let currentIframe = ownerIframe; - let onlyOneMore = false; - while (currentIframe) { - const rect = getBoundingClientRectWithBorderOffset(currentIframe); - rects.push(rect); - currentIframe = getOwnerIframe(currentIframe); - - if (onlyOneMore) { - break; - } - // We don't want to calculate iframe offsets upwards beyond - // the iframe containing the boundaryWindow, but we - // need to calculate the offset relative to the boundaryWindow. - if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) { - onlyOneMore = true; - } - } - - return mergeRectOffsets(rects); - } else { - return node.getBoundingClientRect(); - } -} - function boxWrap(dims, what, node) { assign(node.style, { borderTopWidth: dims[what + 'Top'] + 'px', diff --git a/packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js b/packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js new file mode 100644 index 0000000000000..9e9df914fcda3 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js @@ -0,0 +1,107 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Data} from './index'; +import type {Rect} from '../utils'; + +const OUTLINE_COLOR = '#f0f0f0'; + +// Note these colors are in sync with DevTools Profiler chart colors. +const COLORS = [ + '#37afa9', + '#63b19e', + '#80b393', + '#97b488', + '#abb67d', + '#beb771', + '#cfb965', + '#dfba57', + '#efbb49', + '#febc38', +]; + +let canvas: HTMLCanvasElement | null = null; + +export function draw(idToData: Map): void { + if (canvas === null) { + initialize(); + } + + const canvasFlow: HTMLCanvasElement = ((canvas: any): HTMLCanvasElement); + canvasFlow.width = window.screen.availWidth; + canvasFlow.height = window.screen.availHeight; + + const context = canvasFlow.getContext('2d'); + context.clearRect(0, 0, canvasFlow.width, canvasFlow.height); + + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (let data of idToData.values()) { + const colorIndex = Math.min(COLORS.length - 1, data.count - 1); + const color = COLORS[colorIndex]; + + data.rects.forEach(rect => { + drawBorder(context, rect, color); + }); + } +} + +function drawBorder( + context: CanvasRenderingContext2D, + rect: Rect, + color: string, +): void { + const {height, left, top, width} = rect; + + // outline + context.lineWidth = 1; + context.strokeStyle = OUTLINE_COLOR; + + context.strokeRect(left - 1, top - 1, width + 2, height + 2); + + // inset + context.lineWidth = 1; + context.strokeStyle = OUTLINE_COLOR; + context.strokeRect(left + 1, top + 1, width - 1, height - 1); + context.strokeStyle = color; + + context.setLineDash([0]); + + // border + context.lineWidth = 1; + context.strokeRect(left, top, width - 1, height - 1); + + context.setLineDash([0]); +} + +export function destroy(): void { + if (canvas !== null) { + if (canvas.parentNode != null) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + } +} + +function initialize(): void { + canvas = window.document.createElement('canvas'); + canvas.style.cssText = ` + xx-background-color: red; + xx-opacity: 0.5; + bottom: 0; + left: 0; + pointer-events: none; + position: fixed; + right: 0; + top: 0; + z-index: 1000000000; + `; + + const root = window.document.documentElement; + root.insertBefore(canvas, root.firstChild); +} diff --git a/packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js b/packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js new file mode 100644 index 0000000000000..1c713213ab15c --- /dev/null +++ b/packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js @@ -0,0 +1,143 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import Agent from 'react-devtools-shared/src/backend/agent'; +import {destroy as destroyCanvas, draw} from './canvas'; +import {getNestedBoundingClientRect} from '../utils'; + +import type {FindNativeNodesForFiberID} from '../../types'; +import type {Rect} from '../utils'; + +// How long the rect should be shown for? +const DISPLAY_DURATION = 250; + +// How long should a rect be considered valid for? +const REMEASUREMENT_AFTER_DURATION = 250; + +// Some environments (e.g. React Native / Hermes) don't support the performace API yet. +const getCurrentTime = + typeof performance === 'object' && typeof performance.now === 'function' + ? () => performance.now() + : () => Date.now(); + +export type Data = {| + count: number, + expirationTime: number, + lastMeasuredAt: number, + rects: Array, +|}; + +const idToData: Map = new Map(); + +let agent: Agent = ((null: any): Agent); +let drawAnimationFrameID: AnimationFrameID | null = null; +let isEnabled: boolean = false; +let redrawTimeoutID: TimeoutID | null = null; + +export function initialize(injectedAgent: Agent): void { + agent = injectedAgent; + agent.addListener('traceUpdates', traceUpdates); +} + +export function toggleEnabled(value: boolean): void { + console.log('[TraceUpdates] toggleEnabled()', value); + isEnabled = value; + + if (!isEnabled) { + idToData.clear(); + + if (drawAnimationFrameID !== null) { + cancelAnimationFrame(drawAnimationFrameID); + drawAnimationFrameID = null; + } + + if (redrawTimeoutID !== null) { + clearTimeout(redrawTimeoutID); + redrawTimeoutID = null; + } + + destroyCanvas(); + } +} + +function traceUpdates( + highlightedNodesMap: Map, +): void { + if (!isEnabled) { + return; + } + + highlightedNodesMap.forEach((findNativeNodes, id) => { + const data = idToData.get(id); + const now = getCurrentTime(); + + let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0; + let rects = data != null ? data.rects : []; + if (lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) { + lastMeasuredAt = now; + + const nodes = findNativeNodes(id); + if (nodes != null) { + rects = ((nodes + .map(measureNode) + .filter(rect => rect !== null): any): Array); + } + } + + idToData.set(id, { + count: data != null ? data.count + 1 : 1, + expirationTime: + data != null + ? Math.min(Number.MAX_VALUE, data.expirationTime + DISPLAY_DURATION) + : now + DISPLAY_DURATION, + lastMeasuredAt, + rects, + }); + }); + + if (redrawTimeoutID !== null) { + clearTimeout(redrawTimeoutID); + redrawTimeoutID = null; + } + + if (drawAnimationFrameID === null) { + drawAnimationFrameID = requestAnimationFrame(prepareToDraw); + } +} + +function prepareToDraw(): void { + drawAnimationFrameID = null; + redrawTimeoutID = null; + + const now = getCurrentTime(); + let earliestExpiration = Number.MAX_VALUE; + + // Remove any items that have already expired. + idToData.forEach((data, id) => { + if (data.expirationTime < now) { + idToData.delete(id); + } else { + earliestExpiration = Math.min(earliestExpiration, data.expirationTime); + } + }); + + draw(idToData); + + redrawTimeoutID = setTimeout(prepareToDraw, earliestExpiration - now); +} + +function measureNode(node: Object): Rect | null { + if (!node || typeof node.getBoundingClientRect !== 'function') { + return null; + } + + let currentWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window; + + return getNestedBoundingClientRect(node, currentWindow); +} diff --git a/packages/react-devtools-shared/src/backend/views/utils.js b/packages/react-devtools-shared/src/backend/views/utils.js new file mode 100644 index 0000000000000..fa779a574d77c --- /dev/null +++ b/packages/react-devtools-shared/src/backend/views/utils.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type Rect = { + bottom: number, + height: number, + left: number, + right: number, + top: number, + width: number, +}; + +// Get the window object for the document that a node belongs to, +// or return null if it cannot be found (node not attached to DOM, +// etc). +export function getOwnerWindow(node: HTMLElement): typeof window | null { + if (!node.ownerDocument) { + return null; + } + return node.ownerDocument.defaultView; +} + +// Get the iframe containing a node, or return null if it cannot +// be found (node not within iframe, etc). +export function getOwnerIframe(node: HTMLElement): HTMLElement | null { + const nodeWindow = getOwnerWindow(node); + if (nodeWindow) { + return nodeWindow.frameElement; + } + return null; +} + +// Get a bounding client rect for a node, with an +// offset added to compensate for its border. +export function getBoundingClientRectWithBorderOffset(node: HTMLElement) { + const dimensions = getElementDimensions(node); + return mergeRectOffsets([ + node.getBoundingClientRect(), + { + top: dimensions.borderTop, + left: dimensions.borderLeft, + bottom: dimensions.borderBottom, + right: dimensions.borderRight, + // This width and height won't get used by mergeRectOffsets (since this + // is not the first rect in the array), but we set them so that this + // object typechecks as a ClientRect. + width: 0, + height: 0, + }, + ]); +} + +// Add together the top, left, bottom, and right properties of +// each ClientRect, but keep the width and height of the first one. +export function mergeRectOffsets(rects: Array): Rect { + return rects.reduce((previousRect, rect) => { + if (previousRect == null) { + return rect; + } + + return { + top: previousRect.top + rect.top, + left: previousRect.left + rect.left, + width: previousRect.width, + height: previousRect.height, + bottom: previousRect.bottom + rect.bottom, + right: previousRect.right + rect.right, + }; + }); +} + +// Calculate a boundingClientRect for a node relative to boundaryWindow, +// taking into account any offsets caused by intermediate iframes. +export function getNestedBoundingClientRect( + node: HTMLElement, + boundaryWindow: typeof window, +): Rect { + const ownerIframe = getOwnerIframe(node); + if (ownerIframe && ownerIframe !== boundaryWindow) { + const rects = [node.getBoundingClientRect()]; + let currentIframe = ownerIframe; + let onlyOneMore = false; + while (currentIframe) { + const rect = getBoundingClientRectWithBorderOffset(currentIframe); + rects.push(rect); + currentIframe = getOwnerIframe(currentIframe); + + if (onlyOneMore) { + break; + } + // We don't want to calculate iframe offsets upwards beyond + // the iframe containing the boundaryWindow, but we + // need to calculate the offset relative to the boundaryWindow. + if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) { + onlyOneMore = true; + } + } + + return mergeRectOffsets(rects); + } else { + return node.getBoundingClientRect(); + } +} + +export function getElementDimensions(domElement: Element) { + const calculatedStyle = window.getComputedStyle(domElement); + return { + borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10), + borderRight: parseInt(calculatedStyle.borderRightWidth, 10), + borderTop: parseInt(calculatedStyle.borderTopWidth, 10), + borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10), + marginLeft: parseInt(calculatedStyle.marginLeft, 10), + marginRight: parseInt(calculatedStyle.marginRight, 10), + marginTop: parseInt(calculatedStyle.marginTop, 10), + marginBottom: parseInt(calculatedStyle.marginBottom, 10), + paddingLeft: parseInt(calculatedStyle.paddingLeft, 10), + paddingRight: parseInt(calculatedStyle.paddingRight, 10), + paddingTop: parseInt(calculatedStyle.paddingTop, 10), + paddingBottom: parseInt(calculatedStyle.paddingBottom, 10), + }; +} diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 695121d08c250..937374d71025d 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -115,6 +115,7 @@ type FrontendEvents = {| stopProfiling: [], updateAppendComponentStack: [boolean], updateComponentFilters: [Array], + updateTraceUpdates: [boolean], viewElementSource: [ElementAndRendererID], // React Native style editor plug-in. diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index efbcf0219688c..5cb23a4d2e877 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -53,6 +53,7 @@ type Config = {| supportsNativeInspection?: boolean, supportsReloadAndProfile?: boolean, supportsProfiling?: boolean, + supportsTraceUpdates?: boolean, |}; export type Capabilities = {| @@ -125,6 +126,7 @@ export default class Store extends EventEmitter<{| _supportsNativeInspection: boolean = true; _supportsProfiling: boolean = false; _supportsReloadAndProfile: boolean = false; + _supportsTraceUpdates: boolean = false; _unsupportedRendererVersionDetected: boolean = false; @@ -157,6 +159,7 @@ export default class Store extends EventEmitter<{| supportsNativeInspection, supportsProfiling, supportsReloadAndProfile, + supportsTraceUpdates, } = config; this._supportsNativeInspection = supportsNativeInspection !== false; if (supportsProfiling) { @@ -165,6 +168,9 @@ export default class Store extends EventEmitter<{| if (supportsReloadAndProfile) { this._supportsReloadAndProfile = true; } + if (supportsTraceUpdates) { + this._supportsTraceUpdates = true; + } } this._bridge = bridge; @@ -336,7 +342,6 @@ export default class Store extends EventEmitter<{| get supportsProfiling(): boolean { return this._supportsProfiling; } - get supportsReloadAndProfile(): boolean { // Does the DevTools shell support reloading and eagerly injecting the renderer interface? // And if so, can the backend use the localStorage API? @@ -344,6 +349,10 @@ export default class Store extends EventEmitter<{| return this._supportsReloadAndProfile && this._isBackendStorageAPISupported; } + get supportsTraceUpdates(): boolean { + return this._supportsTraceUpdates; + } + get unsupportedRendererVersionDetected(): boolean { return this._unsupportedRendererVersionDetected; } diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js index c896a14152f10..2b039e7ac9437 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js @@ -9,31 +9,33 @@ import React, {useContext} from 'react'; import {SettingsContext} from './SettingsContext'; +import {StoreContext} from '../context'; import {CHANGE_LOG_URL} from 'react-devtools-shared/src/constants'; import styles from './SettingsShared.css'; export default function GeneralSettings(_: {||}) { const { + appendComponentStack, displayDensity, + traceUpdates, + setAppendComponentStack, + setTraceUpdates, setDisplayDensity, - theme, setTheme, - appendComponentStack, - setAppendComponentStack, + theme, } = useContext(SettingsContext); - const updateDisplayDensity = ({currentTarget}) => - setDisplayDensity(currentTarget.value); - const updateTheme = ({currentTarget}) => setTheme(currentTarget.value); - const updateappendComponentStack = ({currentTarget}) => - setAppendComponentStack(currentTarget.checked); + const {supportsTraceUpdates} = useContext(StoreContext); return (
Theme
- setTheme(currentTarget.value)}> @@ -45,18 +47,37 @@ export default function GeneralSettings(_: {||}) {
+ {supportsTraceUpdates && ( +
+ +
+ )} +
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index ed536874ce08b..eeccfa1014055 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -40,6 +40,9 @@ type Context = {| theme: Theme, setTheme(value: Theme): void, + + traceUpdates: boolean, + setTraceUpdates: (value: boolean) => void, |}; const SettingsContext = createContext(((null: any): Context)); @@ -73,6 +76,10 @@ function SettingsContextController({ const [appendComponentStack, setAppendComponentStack] = useLocalStorage< boolean, >(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, true); + const [traceUpdates, setTraceUpdates] = useLocalStorage( + 'React::DevTools::traceUpdates', + false, + ); const documentElements = useMemo( () => { @@ -138,6 +145,13 @@ function SettingsContextController({ [bridge, appendComponentStack], ); + useEffect( + () => { + bridge.send('updateTraceUpdates', traceUpdates); + }, + [bridge, traceUpdates], + ); + const value = useMemo( () => ({ displayDensity, @@ -146,6 +160,8 @@ function SettingsContextController({ setTheme, appendComponentStack, setAppendComponentStack, + traceUpdates, + setTraceUpdates, lineHeight: displayDensity === 'compact' ? COMPACT_LINE_HEIGHT @@ -157,6 +173,8 @@ function SettingsContextController({ setTheme, appendComponentStack, setAppendComponentStack, + traceUpdates, + setTraceUpdates, theme, ], ); From 7e6882efa1d16b88d7937f974da22ae408991840 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 2 Oct 2019 13:25:46 -0700 Subject: [PATCH 02/12] Updated DevTools CHANGELOG --- packages/react-devtools/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index 51d05827d034f..20fc2f7a3a98b 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -8,6 +8,8 @@ +#### Features +* "Highlight updates" feature added for browser extensions and `react-devtools-inline` NPM package. ([bvaughn](https://github.com/bvaughn) in [#16989](https://github.com/facebook/react/pull/16989)) ## 4.1.3 (September 30, 2019) From 4720865415c2c4efc562fbb6a7d0f2382cb2c6d4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 2 Oct 2019 13:31:28 -0700 Subject: [PATCH 03/12] Deleted unused getNodesForID() function --- .../react-devtools-shared/src/backend/agent.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index b8ee6510507b9..a352be2a8d965 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -204,22 +204,6 @@ export default class Agent extends EventEmitter<{| return null; } - getNodesForID(id: number): Array | null { - for (let rendererID in this._rendererInterfaces) { - const renderer = ((this._rendererInterfaces[ - (rendererID: any) - ]: any): RendererInterface); - - try { - const nodes = renderer.findNativeNodesForFiberID(id); - if (nodes != null) { - return nodes; - } - } catch (error) {} - } - return null; - } - getProfilingData = ({rendererID}: {|rendererID: RendererID|}) => { const renderer = this._rendererInterfaces[rendererID]; if (renderer == null) { From 19b54edd1c107ada27dccf67a60a7ef3144de783 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 2 Oct 2019 15:53:14 -0700 Subject: [PATCH 04/12] Optimized host node lookup for traced updates feature --- .../src/backend/agent.js | 9 +- .../src/backend/renderer.js | 121 ++++++++++++------ .../src/backend/views/TraceUpdates/canvas.js | 16 +-- .../src/backend/views/TraceUpdates/index.js | 36 ++---- 4 files changed, 110 insertions(+), 72 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index a352be2a8d965..933d1b9e6aad7 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -29,7 +29,6 @@ import {patch as patchConsole, unpatch as unpatchConsole} from './console'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type { - FindNativeNodesForFiberID, InstanceAndStyle, NativeType, OwnersList, @@ -92,7 +91,7 @@ export default class Agent extends EventEmitter<{| hideNativeHighlight: [], showNativeHighlight: [NativeType], shutdown: [], - traceUpdates: [Map], + traceUpdates: [Set], |}> { _bridge: BackendBridge; _isProfiling: boolean = false; @@ -434,10 +433,8 @@ export default class Agent extends EventEmitter<{| } }; - onTraceUpdates = ( - highlightedNodesMap: Map, - ) => { - this.emit('traceUpdates', highlightedNodesMap); + onTraceUpdates = (nodes: Set) => { + this.emit('traceUpdates', nodes); }; onHookOperations = (operations: Array) => { diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 17fffd6d80aed..15789e5aa6ca0 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -54,10 +54,10 @@ import type { ChangeDescription, CommitDataBackend, DevToolsHook, - FindNativeNodesForFiberID, InspectedElement, InspectedElementPayload, InstanceAndStyle, + NativeType, Owner, PathFrame, PathMatch, @@ -535,7 +535,7 @@ export function attach( // Highlight updates let traceUpdatesEnabled: boolean = false; - let highlightedNodesMap: Map = new Map(); + let traceUpdatesForNodes: Set = new Set(); function applyComponentFilters(componentFilters: Array) { hideElementsWithTypes.clear(); @@ -618,7 +618,7 @@ export function attach( hook.getFiberRoots(rendererID).forEach(root => { currentRootID = getFiberID(getPrimaryFiber(root.current)); setRootPseudoKey(currentRootID, root.current); - mountFiberRecursively(root.current, null); + mountFiberRecursively(root.current, null, false, false); flushPendingEvents(root); currentRootID = -1; }); @@ -1129,23 +1129,6 @@ export function attach( return stringID; } - function recordTraceUpdate(fiber: Fiber) { - // Highlighting every host node would be too noisy. - // We highlight user components and context consumers. - // Without consumers, a context update that renders only host nodes directly wouldn't highlight at all. - const elementType = getElementTypeForFiber(fiber); - if ( - elementType === ElementTypeFunction || - elementType === ElementTypeClass || - elementType === ElementTypeContext - ) { - highlightedNodesMap.set( - getFiberID(getPrimaryFiber(fiber)), - findNativeNodesForFiberID, - ); - } - } - function recordMount(fiber: Fiber, parentFiber: Fiber | null) { const isRoot = fiber.tag === HostRoot; const id = getFiberID(getPrimaryFiber(fiber)); @@ -1249,7 +1232,8 @@ export function attach( function mountFiberRecursively( fiber: Fiber, parentFiber: Fiber | null, - traverseSiblings = false, + traverseSiblings: boolean, + belongsToUntracedAncestor: boolean, ) { if (__DEBUG__) { debug('mountFiberRecursively()', fiber, parentFiber); @@ -1267,7 +1251,23 @@ export function attach( } if (traceUpdatesEnabled) { - recordTraceUpdate(fiber); + const elementType = getElementTypeForFiber(fiber); + if (belongsToUntracedAncestor) { + // If a traced ancestor rendered, we should mark the nearest host nodes for highlighting. + if (elementType === ElementTypeHostComponent) { + traceUpdatesForNodes.add(fiber.stateNode); + belongsToUntracedAncestor = false; + } + } else { + if ( + elementType === ElementTypeFunction || + elementType === ElementTypeClass || + elementType === ElementTypeContext + ) { + // Otherwise if this is a traced ancestor, flag it for descendants. + belongsToUntracedAncestor = true; + } + } } const isTimedOutSuspense = @@ -1290,6 +1290,7 @@ export function attach( fallbackChild, shouldIncludeInTree ? fiber : parentFiber, true, + belongsToUntracedAncestor, ); } } else { @@ -1298,6 +1299,7 @@ export function attach( fiber.child, shouldIncludeInTree ? fiber : parentFiber, true, + belongsToUntracedAncestor, ); } } @@ -1307,7 +1309,12 @@ export function attach( updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); if (traverseSiblings && fiber.sibling !== null) { - mountFiberRecursively(fiber.sibling, parentFiber, true); + mountFiberRecursively( + fiber.sibling, + parentFiber, + true, + belongsToUntracedAncestor, + ); } } @@ -1456,22 +1463,37 @@ export function attach( nextFiber: Fiber, prevFiber: Fiber, parentFiber: Fiber | null, + nearestTracedAncestorDidRender: boolean, ): boolean { if (__DEBUG__) { debug('updateFiberRecursively()', nextFiber, parentFiber); } - const didRender = didFiberRender(prevFiber, nextFiber); - - if (traceUpdatesEnabled && didRender) { - recordTraceUpdate(nextFiber); + if (traceUpdatesEnabled) { + const elementType = getElementTypeForFiber(nextFiber); + if (nearestTracedAncestorDidRender) { + // If a traced ancestor rendered, we should mark the nearest host nodes for highlighting. + if (elementType === ElementTypeHostComponent) { + traceUpdatesForNodes.add(nextFiber.stateNode); + nearestTracedAncestorDidRender = false; + } + } else { + if ( + elementType === ElementTypeFunction || + elementType === ElementTypeClass || + elementType === ElementTypeContext + ) { + // Otherwise if this is a traced ancestor, flag it for descendants. + nearestTracedAncestorDidRender = didFiberRender(prevFiber, nextFiber); + } + } } if ( mostRecentlyInspectedElement !== null && mostRecentlyInspectedElement.id === getFiberID(getPrimaryFiber(nextFiber)) && - didRender + didFiberRender(prevFiber, nextFiber) ) { // If this Fiber has updated, clear cached inspected data. // If it is inspected again, it may need to be re-run to obtain updated hooks values. @@ -1513,6 +1535,7 @@ export function attach( nextFallbackChildSet, prevFallbackChildSet, nextFiber, + nearestTracedAncestorDidRender, ) ) { shouldResetChildren = true; @@ -1524,7 +1547,12 @@ export function attach( // 2. Mount primary set const nextPrimaryChildSet = nextFiber.child; if (nextPrimaryChildSet !== null) { - mountFiberRecursively(nextPrimaryChildSet, nextFiber, true); + mountFiberRecursively( + nextPrimaryChildSet, + nextFiber, + true, + nearestTracedAncestorDidRender, + ); } shouldResetChildren = true; } else if (!prevDidTimeout && nextDidTimeOut) { @@ -1539,7 +1567,12 @@ export function attach( ? nextFiberChild.sibling : null; if (nextFallbackChildSet != null) { - mountFiberRecursively(nextFallbackChildSet, nextFiber, true); + mountFiberRecursively( + nextFallbackChildSet, + nextFiber, + true, + nearestTracedAncestorDidRender, + ); shouldResetChildren = true; } } else { @@ -1562,6 +1595,7 @@ export function attach( nextChild, prevChild, shouldIncludeInTree ? nextFiber : parentFiber, + nearestTracedAncestorDidRender, ) ) { // If a nested tree child order changed but it can't handle its own @@ -1579,6 +1613,8 @@ export function attach( mountFiberRecursively( nextChild, shouldIncludeInTree ? nextFiber : parentFiber, + false, + nearestTracedAncestorDidRender, ); shouldResetChildren = true; } @@ -1594,6 +1630,19 @@ export function attach( if (prevChildAtSameIndex !== null) { shouldResetChildren = true; } + } else { + if (traceUpdatesEnabled) { + // If we're tracing updates and we've bailed out before reaching a host node, + // we should fall back to recursively marking the nearest host descendates for highlight. + if (nearestTracedAncestorDidRender) { + const hostFibers = findAllCurrentHostFibers( + getFiberID(getPrimaryFiber(nextFiber)), + ); + hostFibers.forEach(hostFiber => { + traceUpdatesForNodes.add(hostFiber.stateNode); + }); + } + } } } if (shouldIncludeInTree) { @@ -1677,7 +1726,7 @@ export function attach( }; } - mountFiberRecursively(root.current, null); + mountFiberRecursively(root.current, null, false, false); flushPendingEvents(root); currentRootID = -1; }); @@ -1704,7 +1753,7 @@ export function attach( } if (traceUpdatesEnabled) { - highlightedNodesMap.clear(); + traceUpdatesForNodes.clear(); } // Checking root.memoizedInteractions handles multi-renderer edge-case- @@ -1740,10 +1789,10 @@ export function attach( if (!wasMounted && isMounted) { // Mount a new root. setRootPseudoKey(currentRootID, current); - mountFiberRecursively(current, null); + mountFiberRecursively(current, null, false, false); } else if (wasMounted && isMounted) { // Update an existing root. - updateFiberRecursively(current, alternate, null); + updateFiberRecursively(current, alternate, null, false); } else if (wasMounted && !isMounted) { // Unmount an existing root. removeRootPseudoKey(currentRootID); @@ -1752,7 +1801,7 @@ export function attach( } else { // Mount a new root. setRootPseudoKey(currentRootID, current); - mountFiberRecursively(current, null); + mountFiberRecursively(current, null, false, false); } if (isProfiling && isProfilingSupported) { @@ -1775,7 +1824,7 @@ export function attach( flushPendingEvents(root); if (traceUpdatesEnabled) { - hook.emit('traceUpdates', highlightedNodesMap); + hook.emit('traceUpdates', traceUpdatesForNodes); } currentRootID = -1; diff --git a/packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js b/packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js index 9e9df914fcda3..4acdbef21a4e4 100644 --- a/packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js +++ b/packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js @@ -9,6 +9,7 @@ import type {Data} from './index'; import type {Rect} from '../utils'; +import type {NativeType} from '../../types'; const OUTLINE_COLOR = '#f0f0f0'; @@ -28,7 +29,7 @@ const COLORS = [ let canvas: HTMLCanvasElement | null = null; -export function draw(idToData: Map): void { +export function draw(nodeToData: Map): void { if (canvas === null) { initialize(); } @@ -40,15 +41,14 @@ export function draw(idToData: Map): void { const context = canvasFlow.getContext('2d'); context.clearRect(0, 0, canvasFlow.width, canvasFlow.height); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (let data of idToData.values()) { - const colorIndex = Math.min(COLORS.length - 1, data.count - 1); - const color = COLORS[colorIndex]; + nodeToData.forEach(({count, rect}) => { + if (rect !== null) { + const colorIndex = Math.min(COLORS.length - 1, count - 1); + const color = COLORS[colorIndex]; - data.rects.forEach(rect => { drawBorder(context, rect, color); - }); - } + } + }); } function drawBorder( diff --git a/packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js b/packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js index 1c713213ab15c..2b1c0c92cbb95 100644 --- a/packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js +++ b/packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js @@ -11,7 +11,7 @@ import Agent from 'react-devtools-shared/src/backend/agent'; import {destroy as destroyCanvas, draw} from './canvas'; import {getNestedBoundingClientRect} from '../utils'; -import type {FindNativeNodesForFiberID} from '../../types'; +import type {NativeType} from '../../types'; import type {Rect} from '../utils'; // How long the rect should be shown for? @@ -30,10 +30,10 @@ export type Data = {| count: number, expirationTime: number, lastMeasuredAt: number, - rects: Array, + rect: Rect | null, |}; -const idToData: Map = new Map(); +const nodeToData: Map = new Map(); let agent: Agent = ((null: any): Agent); let drawAnimationFrameID: AnimationFrameID | null = null; @@ -50,7 +50,7 @@ export function toggleEnabled(value: boolean): void { isEnabled = value; if (!isEnabled) { - idToData.clear(); + nodeToData.clear(); if (drawAnimationFrameID !== null) { cancelAnimationFrame(drawAnimationFrameID); @@ -66,38 +66,30 @@ export function toggleEnabled(value: boolean): void { } } -function traceUpdates( - highlightedNodesMap: Map, -): void { +function traceUpdates(nodes: Set): void { if (!isEnabled) { return; } - highlightedNodesMap.forEach((findNativeNodes, id) => { - const data = idToData.get(id); + nodes.forEach(node => { + const data = nodeToData.get(node); const now = getCurrentTime(); let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0; - let rects = data != null ? data.rects : []; + let rect = data != null ? data.rect : null; if (lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) { lastMeasuredAt = now; - - const nodes = findNativeNodes(id); - if (nodes != null) { - rects = ((nodes - .map(measureNode) - .filter(rect => rect !== null): any): Array); - } + rect = measureNode(node); } - idToData.set(id, { + nodeToData.set(node, { count: data != null ? data.count + 1 : 1, expirationTime: data != null ? Math.min(Number.MAX_VALUE, data.expirationTime + DISPLAY_DURATION) : now + DISPLAY_DURATION, lastMeasuredAt, - rects, + rect, }); }); @@ -119,15 +111,15 @@ function prepareToDraw(): void { let earliestExpiration = Number.MAX_VALUE; // Remove any items that have already expired. - idToData.forEach((data, id) => { + nodeToData.forEach((data, node) => { if (data.expirationTime < now) { - idToData.delete(id); + nodeToData.delete(node); } else { earliestExpiration = Math.min(earliestExpiration, data.expirationTime); } }); - draw(idToData); + draw(nodeToData); redrawTimeoutID = setTimeout(prepareToDraw, earliestExpiration - now); } From 5b69234b88184eb3bd66139dc916227c5dac19e0 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 2 Oct 2019 15:56:51 -0700 Subject: [PATCH 05/12] Enable trace updates for late-injected renderers --- packages/react-devtools-shared/src/backend/agent.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 933d1b9e6aad7..f826a0b6ed21d 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -99,6 +99,7 @@ export default class Agent extends EventEmitter<{| _rendererInterfaces: {[key: RendererID]: RendererInterface} = {}; _persistedSelection: PersistedSelection | null = null; _persistedSelectionMatch: PathMatch | null = null; + _traceUpdatesEnabled: boolean = false; constructor(bridge: BackendBridge) { super(); @@ -347,6 +348,8 @@ export default class Agent extends EventEmitter<{| rendererInterface.startProfiling(this._recordChangeDescriptions); } + rendererInterface.toggleTraceUpdatesEnabled(this._traceUpdatesEnabled); + // When the renderer is attached, we need to tell it whether // we remember the previous selection that we'd like to restore. // It'll start tracking mounts for matches to the last selection path. @@ -415,7 +418,10 @@ export default class Agent extends EventEmitter<{| }; updateTraceUpdates = (isEnabled: boolean) => { + this._traceUpdatesEnabled = isEnabled; + toggleTraceUpdatesEnabled(isEnabled); + for (let rendererID in this._rendererInterfaces) { const renderer = ((this._rendererInterfaces[ (rendererID: any) From 6044a560cc7b9939c0bce4aa899a25e653f76819 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 2 Oct 2019 16:07:53 -0700 Subject: [PATCH 06/12] Cleaned up event and method names --- .../src/backend/agent.js | 32 ++++++++-------- .../src/backend/legacy/renderer.js | 4 +- .../src/backend/renderer.js | 4 +- .../src/backend/types.js | 2 +- packages/react-devtools-shared/src/bridge.js | 2 +- .../views/Settings/GeneralSettings.js | 8 ++-- .../views/Settings/SettingsContext.js | 37 +++++++++---------- 7 files changed, 44 insertions(+), 45 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index f826a0b6ed21d..5161b434c5bad 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -23,7 +23,7 @@ import { import setupHighlighter from './views/Highlighter'; import { initialize as setupTraceUpdates, - toggleEnabled as toggleTraceUpdatesEnabled, + toggleEnabled as setTraceUpdatesEnabled, } from './views/TraceUpdates'; import {patch as patchConsole, unpatch as unpatchConsole} from './console'; @@ -137,6 +137,7 @@ export default class Agent extends EventEmitter<{| bridge.addListener('overrideState', this.overrideState); bridge.addListener('overrideSuspense', this.overrideSuspense); bridge.addListener('reloadAndProfile', this.reloadAndProfile); + bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled); bridge.addListener('startProfiling', this.startProfiling); bridge.addListener('stopProfiling', this.stopProfiling); bridge.addListener( @@ -149,7 +150,6 @@ export default class Agent extends EventEmitter<{| this.updateAppendComponentStack, ); bridge.addListener('updateComponentFilters', this.updateComponentFilters); - bridge.addListener('updateTraceUpdates', this.updateTraceUpdates); bridge.addListener('viewElementSource', this.viewElementSource); if (this._isProfiling) { @@ -348,7 +348,7 @@ export default class Agent extends EventEmitter<{| rendererInterface.startProfiling(this._recordChangeDescriptions); } - rendererInterface.toggleTraceUpdatesEnabled(this._traceUpdatesEnabled); + rendererInterface.setTraceUpdatesEnabled(this._traceUpdatesEnabled); // When the renderer is attached, we need to tell it whether // we remember the previous selection that we'd like to restore. @@ -359,6 +359,19 @@ export default class Agent extends EventEmitter<{| } } + setTraceUpdatesEnabled = (traceUpdatesEnabled: boolean) => { + this._traceUpdatesEnabled = traceUpdatesEnabled; + + setTraceUpdatesEnabled(traceUpdatesEnabled); + + for (let rendererID in this._rendererInterfaces) { + const renderer = ((this._rendererInterfaces[ + (rendererID: any) + ]: any): RendererInterface); + renderer.setTraceUpdatesEnabled(traceUpdatesEnabled); + } + }; + syncSelectionFromNativeElementsPanel = () => { const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0; if (target == null) { @@ -417,19 +430,6 @@ export default class Agent extends EventEmitter<{| } }; - updateTraceUpdates = (isEnabled: boolean) => { - this._traceUpdatesEnabled = isEnabled; - - toggleTraceUpdatesEnabled(isEnabled); - - for (let rendererID in this._rendererInterfaces) { - const renderer = ((this._rendererInterfaces[ - (rendererID: any) - ]: any): RendererInterface); - renderer.toggleTraceUpdatesEnabled(isEnabled); - } - }; - viewElementSource = ({id, rendererID}: ElementAndRendererID) => { const renderer = this._rendererInterfaces[rendererID]; if (renderer == null) { diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 1fb76e217cfdb..3e7232874504b 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -912,7 +912,7 @@ export function attach( // Not implemented. } - function toggleTraceUpdatesEnabled(enabled: boolean) { + function setTraceUpdatesEnabled(enabled: boolean) { // Not implemented. } @@ -949,10 +949,10 @@ export function attach( setInHook, setInProps, setInState, + setTraceUpdatesEnabled, setTrackedPath, startProfiling, stopProfiling, updateComponentFilters, - toggleTraceUpdatesEnabled, }; } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 15789e5aa6ca0..c4896a42a1d44 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -3104,7 +3104,7 @@ export function attach( } }; - function toggleTraceUpdatesEnabled(isEnabled: boolean): void { + function setTraceUpdatesEnabled(isEnabled: boolean): void { traceUpdatesEnabled = isEnabled; } @@ -3129,10 +3129,10 @@ export function attach( setInHook, setInProps, setInState, + setTraceUpdatesEnabled, setTrackedPath, startProfiling, stopProfiling, updateComponentFilters, - toggleTraceUpdatesEnabled, }; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index fe27b094024c3..118049abc2d87 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -252,11 +252,11 @@ export type RendererInterface = { ) => void, setInProps: (id: number, path: Array, value: any) => void, setInState: (id: number, path: Array, value: any) => void, + setTraceUpdatesEnabled: (enabled: boolean) => void, setTrackedPath: (path: Array | null) => void, startProfiling: (recordChangeDescriptions: boolean) => void, stopProfiling: () => void, updateComponentFilters: (componentFilters: Array) => void, - toggleTraceUpdatesEnabled: (enabled: boolean) => void, }; export type Handler = (data: any) => void; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 937374d71025d..179c13a1f68a9 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -108,6 +108,7 @@ type FrontendEvents = {| profilingData: [ProfilingDataBackend], reloadAndProfile: [boolean], selectFiber: [number], + setTraceUpdatesEnabled: [boolean], shutdown: [], startInspectingNative: [], startProfiling: [boolean], @@ -115,7 +116,6 @@ type FrontendEvents = {| stopProfiling: [], updateAppendComponentStack: [boolean], updateComponentFilters: [Array], - updateTraceUpdates: [boolean], viewElementSource: [ElementAndRendererID], // React Native style editor plug-in. diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js index 2b039e7ac9437..4e7ce6357c638 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js @@ -18,12 +18,12 @@ export default function GeneralSettings(_: {||}) { const { appendComponentStack, displayDensity, - traceUpdates, setAppendComponentStack, - setTraceUpdates, setDisplayDensity, setTheme, + setTraceUpdatesEnabled, theme, + traceUpdatesEnabled, } = useContext(SettingsContext); const {supportsTraceUpdates} = useContext(StoreContext); @@ -60,9 +60,9 @@ export default function GeneralSettings(_: {||}) {