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)