@samitouri / QOS-React-2 / commits / 0545f366d4

Added trace updates feature (DOM only) (#16989)

* Added trace updates feature (DOM only) * Updated DevTools CHANGELOG

Brian Vaughn committed Oct 3, 2019 at 11:07 UTC 0545f366d4d6b5959f4bb172e810c745f74b9513
19 files changed +604 -149
packages/react-devtools-core/src/backend.js
+1 -1
@@ -114,7 +114,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
114 }
115
116 if (bridge !== null) {
117 - bridge.emit('shutdown');
117 + bridge.shutdown();
118 }
119
120 scheduleRetry();
packages/react-devtools-extensions/src/backend.js
+4
@@ -67,6 +67,10 @@ function setup(hook) {
67
68 initBackend(hook, agent, window);
69
70 + // Let the frontend know that the backend has attached listeners and is ready for messages.
71 + // This covers the case of of syncing saved values after reloading/navigating while DevTools remain open.
72 + bridge.send('extensionBackendInitialized');
73 +
74 // Setup React Native style editor if a renderer like react-native-web has injected it.
75 if (hook.resolveRNStyle) {
76 setupNativeStyleEditor(
packages/react-devtools-extensions/src/main.js
+12
@@ -9,6 +9,7 @@ import {
9 getBrowserName,
10 getBrowserTheme,
11 } from './utils';
12 +import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
13 import {
14 getSavedComponentFilters,
15 getAppendComponentStack,
@@ -125,10 +126,21 @@ function createPanelIfReactLoaded() {
126 profilingData = store.profilerStore.profilingData;
127 }
128
129 + bridge.addListener('extensionBackendInitialized', () => {
130 + // Initialize the renderer's trace-updates setting.
131 + // This handles the case of navigating to a new page after the DevTools have already been shown.
132 + bridge.send(
133 + 'setTraceUpdatesEnabled',
134 + localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) ===
135 + 'true',
136 + );
137 + });
138 +
139 store = new Store(bridge, {
140 isProfiling,
141 supportsReloadAndProfile: isChrome,
142 supportsProfiling,
143 + supportsTraceUpdates: true,
144 });
145 store.profilerStore.profilingData = profilingData;
146
packages/react-devtools-inline/src/frontend.js
+1 -1
@@ -64,7 +64,7 @@ export function initialize(
64 },
65 });
66
67 - const store: Store = new Store(bridge);
67 + const store: Store = new Store(bridge, {supportsTraceUpdates: true});
68
69 const ForwardRef = forwardRef<Props, mixed>((props, ref) => (
70 <DevTools ref={ref} bridge={bridge} store={store} {...props} />
packages/react-devtools-shared/src/backend/agent.js
+27
@@ -21,6 +21,10 @@ import {
21 sessionStorageSetItem,
22 } from 'react-devtools-shared/src/storage';
23 import setupHighlighter from './views/Highlighter';
24 +import {
25 + initialize as setupTraceUpdates,
26 + toggleEnabled as setTraceUpdatesEnabled,
27 +} from './views/TraceUpdates';
28 import {patch as patchConsole, unpatch as unpatchConsole} from './console';
29
30 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
@@ -87,6 +91,7 @@ export default class Agent extends EventEmitter<{|
91 hideNativeHighlight: [],
92 showNativeHighlight: [NativeType],
93 shutdown: [],
94 + traceUpdates: [Set<NativeType>],
95 |}> {
96 _bridge: BackendBridge;
97 _isProfiling: boolean = false;
@@ -94,6 +99,7 @@ export default class Agent extends EventEmitter<{|
99 _rendererInterfaces: {[key: RendererID]: RendererInterface} = {};
100 _persistedSelection: PersistedSelection | null = null;
101 _persistedSelectionMatch: PathMatch | null = null;
102 + _traceUpdatesEnabled: boolean = false;
103
104 constructor(bridge: BackendBridge) {
105 super();
@@ -131,6 +137,7 @@ export default class Agent extends EventEmitter<{|
137 bridge.addListener('overrideState', this.overrideState);
138 bridge.addListener('overrideSuspense', this.overrideSuspense);
139 bridge.addListener('reloadAndProfile', this.reloadAndProfile);
140 + bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
141 bridge.addListener('startProfiling', this.startProfiling);
142 bridge.addListener('stopProfiling', this.stopProfiling);
143 bridge.addListener(
@@ -159,6 +166,7 @@ export default class Agent extends EventEmitter<{|
166 bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported);
167
168 setupHighlighter(bridge, this);
169 + setupTraceUpdates(this);
170 }
171
172 get rendererInterfaces(): {[key: RendererID]: RendererInterface} {
@@ -340,6 +348,8 @@ export default class Agent extends EventEmitter<{|
348 rendererInterface.startProfiling(this._recordChangeDescriptions);
349 }
350
351 + rendererInterface.setTraceUpdatesEnabled(this._traceUpdatesEnabled);
352 +
353 // When the renderer is attached, we need to tell it whether
354 // we remember the previous selection that we'd like to restore.
355 // It'll start tracking mounts for matches to the last selection path.
@@ -349,6 +359,19 @@ export default class Agent extends EventEmitter<{|
359 }
360 }
361
362 + setTraceUpdatesEnabled = (traceUpdatesEnabled: boolean) => {
363 + this._traceUpdatesEnabled = traceUpdatesEnabled;
364 +
365 + setTraceUpdatesEnabled(traceUpdatesEnabled);
366 +
367 + for (let rendererID in this._rendererInterfaces) {
368 + const renderer = ((this._rendererInterfaces[
369 + (rendererID: any)
370 + ]: any): RendererInterface);
371 + renderer.setTraceUpdatesEnabled(traceUpdatesEnabled);
372 + }
373 + };
374 +
375 syncSelectionFromNativeElementsPanel = () => {
376 const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
377 if (target == null) {
@@ -416,6 +439,10 @@ export default class Agent extends EventEmitter<{|
439 }
440 };
441
442 + onTraceUpdates = (nodes: Set<NativeType>) => {
443 + this.emit('traceUpdates', nodes);
444 + };
445 +
446 onHookOperations = (operations: Array<number>) => {
447 if (__DEBUG__) {
448 debug('onHookOperations', operations);
packages/react-devtools-shared/src/backend/index.js
+1
@@ -44,6 +44,7 @@ export function initBackend(
44 }),
45
46 hook.sub('operations', agent.onHookOperations),
47 + hook.sub('traceUpdates', agent.onTraceUpdates),
48
49 // TODO Add additional subscriptions required for profiling mode
50 ];
packages/react-devtools-shared/src/backend/legacy/renderer.js
+5
@@ -912,6 +912,10 @@ export function attach(
912 // Not implemented.
913 }
914
915 + function setTraceUpdatesEnabled(enabled: boolean) {
916 + // Not implemented.
917 + }
918 +
919 function setTrackedPath(path: Array<PathFrame> | null) {
920 // Not implemented.
921 }
@@ -945,6 +949,7 @@ export function attach(
949 setInHook,
950 setInProps,
951 setInState,
952 + setTraceUpdatesEnabled,
953 setTrackedPath,
954 startProfiling,
955 stopProfiling,
packages/react-devtools-shared/src/backend/renderer.js
+100 -9
@@ -57,6 +57,7 @@ import type {
57 InspectedElement,
58 InspectedElementPayload,
59 InstanceAndStyle,
60 + NativeType,
61 Owner,
62 PathFrame,
63 PathMatch,
@@ -532,6 +533,10 @@ export function attach(
533 const hideElementsWithPaths: Set<RegExp> = new Set();
534 const hideElementsWithTypes: Set<ElementType> = new Set();
535
536 + // Highlight updates
537 + let traceUpdatesEnabled: boolean = false;
538 + let traceUpdatesForNodes: Set<NativeType> = new Set();
539 +
540 function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
541 hideElementsWithTypes.clear();
542 hideElementsWithDisplayNames.clear();
@@ -613,7 +618,7 @@ export function attach(
618 hook.getFiberRoots(rendererID).forEach(root => {
619 currentRootID = getFiberID(getPrimaryFiber(root.current));
620 setRootPseudoKey(currentRootID, root.current);
616 - mountFiberRecursively(root.current, null);
621 + mountFiberRecursively(root.current, null, false, false);
622 flushPendingEvents(root);
623 currentRootID = -1;
624 });
@@ -1227,7 +1232,8 @@ export function attach(
1232 function mountFiberRecursively(
1233 fiber: Fiber,
1234 parentFiber: Fiber | null,
1230 - traverseSiblings = false,
1235 + traverseSiblings: boolean,
1236 + traceNearestHostComponentUpdate: boolean,
1237 ) {
1238 if (__DEBUG__) {
1239 debug('mountFiberRecursively()', fiber, parentFiber);
@@ -1244,6 +1250,20 @@ export function attach(
1250 recordMount(fiber, parentFiber);
1251 }
1252
1253 + if (traceUpdatesEnabled) {
1254 + if (traceNearestHostComponentUpdate) {
1255 + const elementType = getElementTypeForFiber(fiber);
1256 + // If an ancestor updated, we should mark the nearest host nodes for highlighting.
1257 + if (elementType === ElementTypeHostComponent) {
1258 + traceUpdatesForNodes.add(fiber.stateNode);
1259 + traceNearestHostComponentUpdate = false;
1260 + }
1261 + }
1262 +
1263 + // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
1264 + // because we don't want to highlight every host node inside of a newly mounted subtree.
1265 + }
1266 +
1267 const isTimedOutSuspense =
1268 fiber.tag === ReactTypeOfWork.SuspenseComponent &&
1269 fiber.memoizedState !== null;
@@ -1264,6 +1284,7 @@ export function attach(
1284 fallbackChild,
1285 shouldIncludeInTree ? fiber : parentFiber,
1286 true,
1287 + traceNearestHostComponentUpdate,
1288 );
1289 }
1290 } else {
@@ -1272,6 +1293,7 @@ export function attach(
1293 fiber.child,
1294 shouldIncludeInTree ? fiber : parentFiber,
1295 true,
1296 + traceNearestHostComponentUpdate,
1297 );
1298 }
1299 }
@@ -1281,7 +1303,12 @@ export function attach(
1303 updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
1304
1305 if (traverseSiblings && fiber.sibling !== null) {
1284 - mountFiberRecursively(fiber.sibling, parentFiber, true);
1306 + mountFiberRecursively(
1307 + fiber.sibling,
1308 + parentFiber,
1309 + true,
1310 + traceNearestHostComponentUpdate,
1311 + );
1312 }
1313 }
1314
@@ -1430,11 +1457,35 @@ export function attach(
1457 nextFiber: Fiber,
1458 prevFiber: Fiber,
1459 parentFiber: Fiber | null,
1460 + traceNearestHostComponentUpdate: boolean,
1461 ): boolean {
1462 if (__DEBUG__) {
1463 debug('updateFiberRecursively()', nextFiber, parentFiber);
1464 }
1465
1466 + if (traceUpdatesEnabled) {
1467 + const elementType = getElementTypeForFiber(nextFiber);
1468 + if (traceNearestHostComponentUpdate) {
1469 + // If an ancestor updated, we should mark the nearest host nodes for highlighting.
1470 + if (elementType === ElementTypeHostComponent) {
1471 + traceUpdatesForNodes.add(nextFiber.stateNode);
1472 + traceNearestHostComponentUpdate = false;
1473 + }
1474 + } else {
1475 + if (
1476 + elementType === ElementTypeFunction ||
1477 + elementType === ElementTypeClass ||
1478 + elementType === ElementTypeContext
1479 + ) {
1480 + // Otherwise if this is a traced ancestor, flag for the nearest host descendant(s).
1481 + traceNearestHostComponentUpdate = didFiberRender(
1482 + prevFiber,
1483 + nextFiber,
1484 + );
1485 + }
1486 + }
1487 + }
1488 +
1489 if (
1490 mostRecentlyInspectedElement !== null &&
1491 mostRecentlyInspectedElement.id ===
@@ -1481,6 +1532,7 @@ export function attach(
1532 nextFallbackChildSet,
1533 prevFallbackChildSet,
1534 nextFiber,
1535 + traceNearestHostComponentUpdate,
1536 )
1537 ) {
1538 shouldResetChildren = true;
@@ -1492,7 +1544,12 @@ export function attach(
1544 // 2. Mount primary set
1545 const nextPrimaryChildSet = nextFiber.child;
1546 if (nextPrimaryChildSet !== null) {
1495 - mountFiberRecursively(nextPrimaryChildSet, nextFiber, true);
1547 + mountFiberRecursively(
1548 + nextPrimaryChildSet,
1549 + nextFiber,
1550 + true,
1551 + traceNearestHostComponentUpdate,
1552 + );
1553 }
1554 shouldResetChildren = true;
1555 } else if (!prevDidTimeout && nextDidTimeOut) {
@@ -1507,7 +1564,12 @@ export function attach(
1564 ? nextFiberChild.sibling
1565 : null;
1566 if (nextFallbackChildSet != null) {
1510 - mountFiberRecursively(nextFallbackChildSet, nextFiber, true);
1567 + mountFiberRecursively(
1568 + nextFallbackChildSet,
1569 + nextFiber,
1570 + true,
1571 + traceNearestHostComponentUpdate,
1572 + );
1573 shouldResetChildren = true;
1574 }
1575 } else {
@@ -1530,6 +1592,7 @@ export function attach(
1592 nextChild,
1593 prevChild,
1594 shouldIncludeInTree ? nextFiber : parentFiber,
1595 + traceNearestHostComponentUpdate,
1596 )
1597 ) {
1598 // If a nested tree child order changed but it can't handle its own
@@ -1547,6 +1610,8 @@ export function attach(
1610 mountFiberRecursively(
1611 nextChild,
1612 shouldIncludeInTree ? nextFiber : parentFiber,
1613 + false,
1614 + traceNearestHostComponentUpdate,
1615 );
1616 shouldResetChildren = true;
1617 }
@@ -1562,6 +1627,19 @@ export function attach(
1627 if (prevChildAtSameIndex !== null) {
1628 shouldResetChildren = true;
1629 }
1630 + } else {
1631 + if (traceUpdatesEnabled) {
1632 + // If we're tracing updates and we've bailed out before reaching a host node,
1633 + // we should fall back to recursively marking the nearest host descendates for highlight.
1634 + if (traceNearestHostComponentUpdate) {
1635 + const hostFibers = findAllCurrentHostFibers(
1636 + getFiberID(getPrimaryFiber(nextFiber)),
1637 + );
1638 + hostFibers.forEach(hostFiber => {
1639 + traceUpdatesForNodes.add(hostFiber.stateNode);
1640 + });
1641 + }
1642 + }
1643 }
1644 }
1645 if (shouldIncludeInTree) {
@@ -1645,7 +1723,7 @@ export function attach(
1723 };
1724 }
1725
1648 - mountFiberRecursively(root.current, null);
1726 + mountFiberRecursively(root.current, null, false, false);
1727 flushPendingEvents(root);
1728 currentRootID = -1;
1729 });
@@ -1671,6 +1749,10 @@ export function attach(
1749 mightBeOnTrackedPath = true;
1750 }
1751
1752 + if (traceUpdatesEnabled) {
1753 + traceUpdatesForNodes.clear();
1754 + }
1755 +
1756 // Checking root.memoizedInteractions handles multi-renderer edge-case-
1757 // where some v16 renderers support profiling and others don't.
1758 const isProfilingSupported = root.memoizedInteractions != null;
@@ -1704,10 +1786,10 @@ export function attach(
1786 if (!wasMounted && isMounted) {
1787 // Mount a new root.
1788 setRootPseudoKey(currentRootID, current);
1707 - mountFiberRecursively(current, null);
1789 + mountFiberRecursively(current, null, false, false);
1790 } else if (wasMounted && isMounted) {
1791 // Update an existing root.
1710 - updateFiberRecursively(current, alternate, null);
1792 + updateFiberRecursively(current, alternate, null, false);
1793 } else if (wasMounted && !isMounted) {
1794 // Unmount an existing root.
1795 removeRootPseudoKey(currentRootID);
@@ -1716,7 +1798,7 @@ export function attach(
1798 } else {
1799 // Mount a new root.
1800 setRootPseudoKey(currentRootID, current);
1719 - mountFiberRecursively(current, null);
1801 + mountFiberRecursively(current, null, false, false);
1802 }
1803
1804 if (isProfiling && isProfilingSupported) {
@@ -1738,6 +1820,10 @@ export function attach(
1820 // We're done here.
1821 flushPendingEvents(root);
1822
1823 + if (traceUpdatesEnabled) {
1824 + hook.emit('traceUpdates', traceUpdatesForNodes);
1825 + }
1826 +
1827 currentRootID = -1;
1828 }
1829
@@ -3015,6 +3101,10 @@ export function attach(
3101 }
3102 };
3103
3104 + function setTraceUpdatesEnabled(isEnabled: boolean): void {
3105 + traceUpdatesEnabled = isEnabled;
3106 + }
3107 +
3108 return {
3109 cleanup,
3110 findNativeNodesForFiberID,
@@ -3036,6 +3126,7 @@ export function attach(
3126 setInHook,
3127 setInProps,
3128 setInState,
3129 + setTraceUpdatesEnabled,
3130 setTrackedPath,
3131 startProfiling,
3132 stopProfiling,
packages/react-devtools-shared/src/backend/types.js
+2 -1
@@ -252,10 +252,11 @@ export type RendererInterface = {
252 ) => void,
253 setInProps: (id: number, path: Array<string | number>, value: any) => void,
254 setInState: (id: number, path: Array<string | number>, value: any) => void,
255 + setTraceUpdatesEnabled: (enabled: boolean) => void,
256 setTrackedPath: (path: Array<PathFrame> | null) => void,
257 startProfiling: (recordChangeDescriptions: boolean) => void,
258 stopProfiling: () => void,
258 - updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
259 + updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
260 };
261
262 export type Handler = (data: any) => void;
packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js
+2 -118
@@ -8,15 +8,9 @@
8 */
9
10 import assign from 'object-assign';
11 +import {getElementDimensions, getNestedBoundingClientRect} from '../utils';
12
12 -type Rect = {
13 - bottom: number,
14 - height: number,
15 - left: number,
16 - right: number,
17 - top: number,
18 - width: number,
19 -};
13 +import type {Rect} from '../utils';
14
15 type Box = {|top: number, left: number, width: number, height: number|};
16
@@ -333,116 +327,6 @@ function findTipPos(dims, bounds, tipSize) {
327 };
328 }
329
336 -export function getElementDimensions(domElement: Element) {
337 - const calculatedStyle = window.getComputedStyle(domElement);
338 - return {
339 - borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
340 - borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
341 - borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
342 - borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
343 - marginLeft: parseInt(calculatedStyle.marginLeft, 10),
344 - marginRight: parseInt(calculatedStyle.marginRight, 10),
345 - marginTop: parseInt(calculatedStyle.marginTop, 10),
346 - marginBottom: parseInt(calculatedStyle.marginBottom, 10),
347 - paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
348 - paddingRight: parseInt(calculatedStyle.paddingRight, 10),
349 - paddingTop: parseInt(calculatedStyle.paddingTop, 10),
350 - paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
351 - };
352 -}
353 -
354 -// Get the window object for the document that a node belongs to,
355 -// or return null if it cannot be found (node not attached to DOM,
356 -// etc).
357 -function getOwnerWindow(node: HTMLElement): typeof window | null {
358 - if (!node.ownerDocument) {
359 - return null;
360 - }
361 - return node.ownerDocument.defaultView;
362 -}
363 -
364 -// Get the iframe containing a node, or return null if it cannot
365 -// be found (node not within iframe, etc).
366 -function getOwnerIframe(node: HTMLElement): HTMLElement | null {
367 - const nodeWindow = getOwnerWindow(node);
368 - if (nodeWindow) {
369 - return nodeWindow.frameElement;
370 - }
371 - return null;
372 -}
373 -
374 -// Get a bounding client rect for a node, with an
375 -// offset added to compensate for its border.
376 -function getBoundingClientRectWithBorderOffset(node: HTMLElement) {
377 - const dimensions = getElementDimensions(node);
378 - return mergeRectOffsets([
379 - node.getBoundingClientRect(),
380 - {
381 - top: dimensions.borderTop,
382 - left: dimensions.borderLeft,
383 - bottom: dimensions.borderBottom,
384 - right: dimensions.borderRight,
385 - // This width and height won't get used by mergeRectOffsets (since this
386 - // is not the first rect in the array), but we set them so that this
387 - // object typechecks as a ClientRect.
388 - width: 0,
389 - height: 0,
390 - },
391 - ]);
392 -}
393 -
394 -// Add together the top, left, bottom, and right properties of
395 -// each ClientRect, but keep the width and height of the first one.
396 -function mergeRectOffsets(rects: Array<Rect>): Rect {
397 - return rects.reduce((previousRect, rect) => {
398 - if (previousRect == null) {
399 - return rect;
400 - }
401 -
402 - return {
403 - top: previousRect.top + rect.top,
404 - left: previousRect.left + rect.left,
405 - width: previousRect.width,
406 - height: previousRect.height,
407 - bottom: previousRect.bottom + rect.bottom,
408 - right: previousRect.right + rect.right,
409 - };
410 - });
411 -}
412 -
413 -// Calculate a boundingClientRect for a node relative to boundaryWindow,
414 -// taking into account any offsets caused by intermediate iframes.
415 -function getNestedBoundingClientRect(
416 - node: HTMLElement,
417 - boundaryWindow: typeof window,
418 -): Rect {
419 - const ownerIframe = getOwnerIframe(node);
420 - if (ownerIframe && ownerIframe !== boundaryWindow) {
421 - const rects = [node.getBoundingClientRect()];
422 - let currentIframe = ownerIframe;
423 - let onlyOneMore = false;
424 - while (currentIframe) {
425 - const rect = getBoundingClientRectWithBorderOffset(currentIframe);
426 - rects.push(rect);
427 - currentIframe = getOwnerIframe(currentIframe);
428 -
429 - if (onlyOneMore) {
430 - break;
431 - }
432 - // We don't want to calculate iframe offsets upwards beyond
433 - // the iframe containing the boundaryWindow, but we
434 - // need to calculate the offset relative to the boundaryWindow.
435 - if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
436 - onlyOneMore = true;
437 - }
438 - }
439 -
440 - return mergeRectOffsets(rects);
441 - } else {
442 - return node.getBoundingClientRect();
443 - }
444 -}
445 -
330 function boxWrap(dims, what, node) {
331 assign(node.style, {
332 borderTopWidth: dims[what + 'Top'] + 'px',
packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js new
+107
@@ -0,0 +1,107 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Data} from './index';
11 +import type {Rect} from '../utils';
12 +import type {NativeType} from '../../types';
13 +
14 +const OUTLINE_COLOR = '#f0f0f0';
15 +
16 +// Note these colors are in sync with DevTools Profiler chart colors.
17 +const COLORS = [
18 + '#37afa9',
19 + '#63b19e',
20 + '#80b393',
21 + '#97b488',
22 + '#abb67d',
23 + '#beb771',
24 + '#cfb965',
25 + '#dfba57',
26 + '#efbb49',
27 + '#febc38',
28 +];
29 +
30 +let canvas: HTMLCanvasElement | null = null;
31 +
32 +export function draw(nodeToData: Map<NativeType, Data>): void {
33 + if (canvas === null) {
34 + initialize();
35 + }
36 +
37 + const canvasFlow: HTMLCanvasElement = ((canvas: any): HTMLCanvasElement);
38 + canvasFlow.width = window.screen.availWidth;
39 + canvasFlow.height = window.screen.availHeight;
40 +
41 + const context = canvasFlow.getContext('2d');
42 + context.clearRect(0, 0, canvasFlow.width, canvasFlow.height);
43 +
44 + nodeToData.forEach(({count, rect}) => {
45 + if (rect !== null) {
46 + const colorIndex = Math.min(COLORS.length - 1, count - 1);
47 + const color = COLORS[colorIndex];
48 +
49 + drawBorder(context, rect, color);
50 + }
51 + });
52 +}
53 +
54 +function drawBorder(
55 + context: CanvasRenderingContext2D,
56 + rect: Rect,
57 + color: string,
58 +): void {
59 + const {height, left, top, width} = rect;
60 +
61 + // outline
62 + context.lineWidth = 1;
63 + context.strokeStyle = OUTLINE_COLOR;
64 +
65 + context.strokeRect(left - 1, top - 1, width + 2, height + 2);
66 +
67 + // inset
68 + context.lineWidth = 1;
69 + context.strokeStyle = OUTLINE_COLOR;
70 + context.strokeRect(left + 1, top + 1, width - 1, height - 1);
71 + context.strokeStyle = color;
72 +
73 + context.setLineDash([0]);
74 +
75 + // border
76 + context.lineWidth = 1;
77 + context.strokeRect(left, top, width - 1, height - 1);
78 +
79 + context.setLineDash([0]);
80 +}
81 +
82 +export function destroy(): void {
83 + if (canvas !== null) {
84 + if (canvas.parentNode != null) {
85 + canvas.parentNode.removeChild(canvas);
86 + }
87 + canvas = null;
88 + }
89 +}
90 +
91 +function initialize(): void {
92 + canvas = window.document.createElement('canvas');
93 + canvas.style.cssText = `
94 + xx-background-color: red;
95 + xx-opacity: 0.5;
96 + bottom: 0;
97 + left: 0;
98 + pointer-events: none;
99 + position: fixed;
100 + right: 0;
101 + top: 0;
102 + z-index: 1000000000;
103 + `;
104 +
105 + const root = window.document.documentElement;
106 + root.insertBefore(canvas, root.firstChild);
107 +}
packages/react-devtools-shared/src/backend/views/TraceUpdates/index.js new
+141
@@ -0,0 +1,141 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import Agent from 'react-devtools-shared/src/backend/agent';
11 +import {destroy as destroyCanvas, draw} from './canvas';
12 +import {getNestedBoundingClientRect} from '../utils';
13 +
14 +import type {NativeType} from '../../types';
15 +import type {Rect} from '../utils';
16 +
17 +// How long the rect should be shown for?
18 +const DISPLAY_DURATION = 250;
19 +
20 +// What's the longest we are willing to show the overlay for?
21 +// This can be important if we're getting a flurry of events (e.g. scroll update).
22 +const MAX_DISPLAY_DURATION = 3000;
23 +
24 +// How long should a rect be considered valid for?
25 +const REMEASUREMENT_AFTER_DURATION = 250;
26 +
27 +// Some environments (e.g. React Native / Hermes) don't support the performace API yet.
28 +const getCurrentTime =
29 + typeof performance === 'object' && typeof performance.now === 'function'
30 + ? () => performance.now()
31 + : () => Date.now();
32 +
33 +export type Data = {|
34 + count: number,
35 + expirationTime: number,
36 + lastMeasuredAt: number,
37 + rect: Rect | null,
38 +|};
39 +
40 +const nodeToData: Map<NativeType, Data> = new Map();
41 +
42 +let agent: Agent = ((null: any): Agent);
43 +let drawAnimationFrameID: AnimationFrameID | null = null;
44 +let isEnabled: boolean = false;
45 +let redrawTimeoutID: TimeoutID | null = null;
46 +
47 +export function initialize(injectedAgent: Agent): void {
48 + agent = injectedAgent;
49 + agent.addListener('traceUpdates', traceUpdates);
50 +}
51 +
52 +export function toggleEnabled(value: boolean): void {
53 + isEnabled = value;
54 +
55 + if (!isEnabled) {
56 + nodeToData.clear();
57 +
58 + if (drawAnimationFrameID !== null) {
59 + cancelAnimationFrame(drawAnimationFrameID);
60 + drawAnimationFrameID = null;
61 + }
62 +
63 + if (redrawTimeoutID !== null) {
64 + clearTimeout(redrawTimeoutID);
65 + redrawTimeoutID = null;
66 + }
67 +
68 + destroyCanvas();
69 + }
70 +}
71 +
72 +function traceUpdates(nodes: Set<NativeType>): void {
73 + if (!isEnabled) {
74 + return;
75 + }
76 +
77 + nodes.forEach(node => {
78 + const data = nodeToData.get(node);
79 + const now = getCurrentTime();
80 +
81 + let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0;
82 + let rect = data != null ? data.rect : null;
83 + if (rect === null || lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) {
84 + lastMeasuredAt = now;
85 + rect = measureNode(node);
86 + }
87 +
88 + nodeToData.set(node, {
89 + count: data != null ? data.count + 1 : 1,
90 + expirationTime:
91 + data != null
92 + ? Math.min(
93 + now + MAX_DISPLAY_DURATION,
94 + data.expirationTime + DISPLAY_DURATION,
95 + )
96 + : now + DISPLAY_DURATION,
97 + lastMeasuredAt,
98 + rect,
99 + });
100 + });
101 +
102 + if (redrawTimeoutID !== null) {
103 + clearTimeout(redrawTimeoutID);
104 + redrawTimeoutID = null;
105 + }
106 +
107 + if (drawAnimationFrameID === null) {
108 + drawAnimationFrameID = requestAnimationFrame(prepareToDraw);
109 + }
110 +}
111 +
112 +function prepareToDraw(): void {
113 + drawAnimationFrameID = null;
114 + redrawTimeoutID = null;
115 +
116 + const now = getCurrentTime();
117 + let earliestExpiration = Number.MAX_VALUE;
118 +
119 + // Remove any items that have already expired.
120 + nodeToData.forEach((data, node) => {
121 + if (data.expirationTime < now) {
122 + nodeToData.delete(node);
123 + } else {
124 + earliestExpiration = Math.min(earliestExpiration, data.expirationTime);
125 + }
126 + });
127 +
128 + draw(nodeToData);
129 +
130 + redrawTimeoutID = setTimeout(prepareToDraw, earliestExpiration - now);
131 +}
132 +
133 +function measureNode(node: Object): Rect | null {
134 + if (!node || typeof node.getBoundingClientRect !== 'function') {
135 + return null;
136 + }
137 +
138 + let currentWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window;
139 +
140 + return getNestedBoundingClientRect(node, currentWindow);
141 +}
packages/react-devtools-shared/src/backend/views/utils.js new
+127
@@ -0,0 +1,127 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export type Rect = {
11 + bottom: number,
12 + height: number,
13 + left: number,
14 + right: number,
15 + top: number,
16 + width: number,
17 +};
18 +
19 +// Get the window object for the document that a node belongs to,
20 +// or return null if it cannot be found (node not attached to DOM,
21 +// etc).
22 +export function getOwnerWindow(node: HTMLElement): typeof window | null {
23 + if (!node.ownerDocument) {
24 + return null;
25 + }
26 + return node.ownerDocument.defaultView;
27 +}
28 +
29 +// Get the iframe containing a node, or return null if it cannot
30 +// be found (node not within iframe, etc).
31 +export function getOwnerIframe(node: HTMLElement): HTMLElement | null {
32 + const nodeWindow = getOwnerWindow(node);
33 + if (nodeWindow) {
34 + return nodeWindow.frameElement;
35 + }
36 + return null;
37 +}
38 +
39 +// Get a bounding client rect for a node, with an
40 +// offset added to compensate for its border.
41 +export function getBoundingClientRectWithBorderOffset(node: HTMLElement) {
42 + const dimensions = getElementDimensions(node);
43 + return mergeRectOffsets([
44 + node.getBoundingClientRect(),
45 + {
46 + top: dimensions.borderTop,
47 + left: dimensions.borderLeft,
48 + bottom: dimensions.borderBottom,
49 + right: dimensions.borderRight,
50 + // This width and height won't get used by mergeRectOffsets (since this
51 + // is not the first rect in the array), but we set them so that this
52 + // object typechecks as a ClientRect.
53 + width: 0,
54 + height: 0,
55 + },
56 + ]);
57 +}
58 +
59 +// Add together the top, left, bottom, and right properties of
60 +// each ClientRect, but keep the width and height of the first one.
61 +export function mergeRectOffsets(rects: Array<Rect>): Rect {
62 + return rects.reduce((previousRect, rect) => {
63 + if (previousRect == null) {
64 + return rect;
65 + }
66 +
67 + return {
68 + top: previousRect.top + rect.top,
69 + left: previousRect.left + rect.left,
70 + width: previousRect.width,
71 + height: previousRect.height,
72 + bottom: previousRect.bottom + rect.bottom,
73 + right: previousRect.right + rect.right,
74 + };
75 + });
76 +}
77 +
78 +// Calculate a boundingClientRect for a node relative to boundaryWindow,
79 +// taking into account any offsets caused by intermediate iframes.
80 +export function getNestedBoundingClientRect(
81 + node: HTMLElement,
82 + boundaryWindow: typeof window,
83 +): Rect {
84 + const ownerIframe = getOwnerIframe(node);
85 + if (ownerIframe && ownerIframe !== boundaryWindow) {
86 + const rects = [node.getBoundingClientRect()];
87 + let currentIframe = ownerIframe;
88 + let onlyOneMore = false;
89 + while (currentIframe) {
90 + const rect = getBoundingClientRectWithBorderOffset(currentIframe);
91 + rects.push(rect);
92 + currentIframe = getOwnerIframe(currentIframe);
93 +
94 + if (onlyOneMore) {
95 + break;
96 + }
97 + // We don't want to calculate iframe offsets upwards beyond
98 + // the iframe containing the boundaryWindow, but we
99 + // need to calculate the offset relative to the boundaryWindow.
100 + if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
101 + onlyOneMore = true;
102 + }
103 + }
104 +
105 + return mergeRectOffsets(rects);
106 + } else {
107 + return node.getBoundingClientRect();
108 + }
109 +}
110 +
111 +export function getElementDimensions(domElement: Element) {
112 + const calculatedStyle = window.getComputedStyle(domElement);
113 + return {
114 + borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
115 + borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
116 + borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
117 + borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
118 + marginLeft: parseInt(calculatedStyle.marginLeft, 10),
119 + marginRight: parseInt(calculatedStyle.marginRight, 10),
120 + marginTop: parseInt(calculatedStyle.marginTop, 10),
121 + marginBottom: parseInt(calculatedStyle.marginBottom, 10),
122 + paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
123 + paddingRight: parseInt(calculatedStyle.paddingRight, 10),
124 + paddingTop: parseInt(calculatedStyle.paddingTop, 10),
125 + paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
126 + };
127 +}
packages/react-devtools-shared/src/bridge.js
+2
@@ -70,6 +70,7 @@ type NativeStyleEditor_SetValueParams = {|
70 |};
71
72 type BackendEvents = {|
73 + extensionBackendInitialized: [],
74 inspectedElement: [InspectedElementPayload],
75 isBackendStorageAPISupported: [boolean],
76 operations: [Array<number>],
@@ -108,6 +109,7 @@ type FrontendEvents = {|
109 profilingData: [ProfilingDataBackend],
110 reloadAndProfile: [boolean],
111 selectFiber: [number],
112 + setTraceUpdatesEnabled: [boolean],
113 shutdown: [],
114 startInspectingNative: [],
115 startProfiling: [boolean],
packages/react-devtools-shared/src/constants.js
+3
@@ -30,6 +30,9 @@ export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
30 export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
31 'React::DevTools::appendComponentStack';
32
33 +export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
34 + 'React::DevTools::traceUpdatesEnabled';
35 +
36 export const PROFILER_EXPORT_VERSION = 4;
37
38 export const CHANGE_LOG_URL =
packages/react-devtools-shared/src/devtools/store.js
+10 -1
@@ -53,6 +53,7 @@ type Config = {|
53 supportsNativeInspection?: boolean,
54 supportsReloadAndProfile?: boolean,
55 supportsProfiling?: boolean,
56 + supportsTraceUpdates?: boolean,
57 |};
58
59 export type Capabilities = {|
@@ -125,6 +126,7 @@ export default class Store extends EventEmitter<{|
126 _supportsNativeInspection: boolean = true;
127 _supportsProfiling: boolean = false;
128 _supportsReloadAndProfile: boolean = false;
129 + _supportsTraceUpdates: boolean = false;
130
131 _unsupportedRendererVersionDetected: boolean = false;
132
@@ -157,6 +159,7 @@ export default class Store extends EventEmitter<{|
159 supportsNativeInspection,
160 supportsProfiling,
161 supportsReloadAndProfile,
162 + supportsTraceUpdates,
163 } = config;
164 this._supportsNativeInspection = supportsNativeInspection !== false;
165 if (supportsProfiling) {
@@ -165,6 +168,9 @@ export default class Store extends EventEmitter<{|
168 if (supportsReloadAndProfile) {
169 this._supportsReloadAndProfile = true;
170 }
171 + if (supportsTraceUpdates) {
172 + this._supportsTraceUpdates = true;
173 + }
174 }
175
176 this._bridge = bridge;
@@ -336,7 +342,6 @@ export default class Store extends EventEmitter<{|
342 get supportsProfiling(): boolean {
343 return this._supportsProfiling;
344 }
339 -
345 get supportsReloadAndProfile(): boolean {
346 // Does the DevTools shell support reloading and eagerly injecting the renderer interface?
347 // And if so, can the backend use the localStorage API?
@@ -344,6 +349,10 @@ export default class Store extends EventEmitter<{|
349 return this._supportsReloadAndProfile && this._isBackendStorageAPISupported;
350 }
351
352 + get supportsTraceUpdates(): boolean {
353 + return this._supportsTraceUpdates;
354 + }
355 +
356 get unsupportedRendererVersionDetected(): boolean {
357 return this._unsupportedRendererVersionDetected;
358 }
packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js
+32 -11
@@ -9,31 +9,33 @@
9
10 import React, {useContext} from 'react';
11 import {SettingsContext} from './SettingsContext';
12 +import {StoreContext} from '../context';
13 import {CHANGE_LOG_URL} from 'react-devtools-shared/src/constants';
14
15 import styles from './SettingsShared.css';
16
17 export default function GeneralSettings(_: {||}) {
18 const {
19 + appendComponentStack,
20 displayDensity,
21 + setAppendComponentStack,
22 setDisplayDensity,
20 - theme,
23 setTheme,
22 - appendComponentStack,
23 - setAppendComponentStack,
24 + setTraceUpdatesEnabled,
25 + theme,
26 + traceUpdatesEnabled,
27 } = useContext(SettingsContext);
28
26 - const updateDisplayDensity = ({currentTarget}) =>
27 - setDisplayDensity(currentTarget.value);
28 - const updateTheme = ({currentTarget}) => setTheme(currentTarget.value);
29 - const updateappendComponentStack = ({currentTarget}) =>
30 - setAppendComponentStack(currentTarget.checked);
29 + const {supportsTraceUpdates} = useContext(StoreContext);
30
31 return (
32 <div className={styles.Settings}>
33 <div className={styles.Setting}>
34 <div className={styles.RadioLabel}>Theme</div>
36 - <select className={styles.Select} value={theme} onChange={updateTheme}>
35 + <select
36 + className={styles.Select}
37 + value={theme}
38 + onChange={({currentTarget}) => setTheme(currentTarget.value)}>
39 <option value="auto">Auto</option>
40 <option value="light">Light</option>
41 <option value="dark">Dark</option>
@@ -45,18 +47,37 @@ export default function GeneralSettings(_: {||}) {
47 <select
48 className={styles.Select}
49 value={displayDensity}
48 - onChange={updateDisplayDensity}>
50 + onChange={({currentTarget}) =>
51 + setDisplayDensity(currentTarget.value)
52 + }>
53 <option value="compact">Compact</option>
54 <option value="comfortable">Comfortable</option>
55 </select>
56 </div>
57
58 + {supportsTraceUpdates && (
59 + <div className={styles.Setting}>
60 + <label>
61 + <input
62 + type="checkbox"
63 + checked={traceUpdatesEnabled}
64 + onChange={({currentTarget}) =>
65 + setTraceUpdatesEnabled(currentTarget.checked)
66 + }
67 + />{' '}
68 + Highlight updates when components render.
69 + </label>
70 + </div>
71 + )}
72 +
73 <div className={styles.Setting}>
74 <label>
75 <input
76 type="checkbox"
77 checked={appendComponentStack}
59 - onChange={updateappendComponentStack}
78 + onChange={({currentTarget}) =>
79 + setAppendComponentStack(currentTarget.checked)
80 + }
81 />{' '}
82 Append component stacks to console warnings and errors.
83 </label>
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+25 -7
@@ -18,6 +18,7 @@ import {
18 COMFORTABLE_LINE_HEIGHT,
19 COMPACT_LINE_HEIGHT,
20 LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
21 + LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
22 } from 'react-devtools-shared/src/constants';
23 import {useLocalStorage} from '../hooks';
24 import {BridgeContext} from '../context';
@@ -40,6 +41,9 @@ type Context = {|
41
42 theme: Theme,
43 setTheme(value: Theme): void,
44 +
45 + traceUpdatesEnabled: boolean,
46 + setTraceUpdatesEnabled: (value: boolean) => void,
47 |};
48
49 const SettingsContext = createContext<Context>(((null: any): Context));
@@ -73,6 +77,9 @@ function SettingsContextController({
77 const [appendComponentStack, setAppendComponentStack] = useLocalStorage<
78 boolean,
79 >(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, true);
80 + const [traceUpdatesEnabled, setTraceUpdatesEnabled] = useLocalStorage<
81 + boolean,
82 + >(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, false);
83
84 const documentElements = useMemo<DocumentElements>(
85 () => {
@@ -138,26 +145,37 @@ function SettingsContextController({
145 [bridge, appendComponentStack],
146 );
147
148 + useEffect(
149 + () => {
150 + bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
151 + },
152 + [bridge, traceUpdatesEnabled],
153 + );
154 +
155 const value = useMemo(
156 () => ({
143 - displayDensity,
144 - setDisplayDensity,
145 - theme,
146 - setTheme,
157 appendComponentStack,
148 - setAppendComponentStack,
158 + displayDensity,
159 lineHeight:
160 displayDensity === 'compact'
161 ? COMPACT_LINE_HEIGHT
162 : COMFORTABLE_LINE_HEIGHT,
163 + setAppendComponentStack,
164 + setDisplayDensity,
165 + setTheme,
166 + setTraceUpdatesEnabled,
167 + theme,
168 + traceUpdatesEnabled,
169 }),
170 [
171 + appendComponentStack,
172 displayDensity,
173 + setAppendComponentStack,
174 setDisplayDensity,
175 setTheme,
158 - appendComponentStack,
159 - setAppendComponentStack,
176 + setTraceUpdatesEnabled,
177 theme,
178 + traceUpdatesEnabled,
179 ],
180 );
181
packages/react-devtools/CHANGELOG.md
+2
@@ -8,6 +8,8 @@
8
9 <!-- Upcoming changes go here -->
10
11 +#### Features
12 +* "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))
13 </details>
14
15 ## 4.1.3 (September 30, 2019)