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

* Added trace updates feature (DOM only)
* Updated DevTools CHANGELOG
This commit is contained in:
Brian Vaughn
2019-10-03 11:07:18 -07:00
committed by GitHub
parent e09097a75d
commit 0545f366d4
19 changed files with 608 additions and 153 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
}
if (bridge !== null) {
bridge.emit('shutdown');
bridge.shutdown();
}
scheduleRetry();
+4
View File
@@ -67,6 +67,10 @@ function setup(hook) {
initBackend(hook, agent, window);
// Let the frontend know that the backend has attached listeners and is ready for messages.
// This covers the case of of syncing saved values after reloading/navigating while DevTools remain open.
bridge.send('extensionBackendInitialized');
// Setup React Native style editor if a renderer like react-native-web has injected it.
if (hook.resolveRNStyle) {
setupNativeStyleEditor(
+12
View File
@@ -9,6 +9,7 @@ import {
getBrowserName,
getBrowserTheme,
} from './utils';
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
import {
getSavedComponentFilters,
getAppendComponentStack,
@@ -125,10 +126,21 @@ function createPanelIfReactLoaded() {
profilingData = store.profilerStore.profilingData;
}
bridge.addListener('extensionBackendInitialized', () => {
// Initialize the renderer's trace-updates setting.
// This handles the case of navigating to a new page after the DevTools have already been shown.
bridge.send(
'setTraceUpdatesEnabled',
localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) ===
'true',
);
});
store = new Store(bridge, {
isProfiling,
supportsReloadAndProfile: isChrome,
supportsProfiling,
supportsTraceUpdates: true,
});
store.profilerStore.profilingData = profilingData;
+1 -1
View File
@@ -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, mixed>((props, ref) => (
<DevTools ref={ref} bridge={bridge} store={store} {...props} />
+27
View File
@@ -21,6 +21,10 @@ import {
sessionStorageSetItem,
} from 'react-devtools-shared/src/storage';
import setupHighlighter from './views/Highlighter';
import {
initialize as setupTraceUpdates,
toggleEnabled as setTraceUpdatesEnabled,
} from './views/TraceUpdates';
import {patch as patchConsole, unpatch as unpatchConsole} from './console';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
@@ -87,6 +91,7 @@ export default class Agent extends EventEmitter<{|
hideNativeHighlight: [],
showNativeHighlight: [NativeType],
shutdown: [],
traceUpdates: [Set<NativeType>],
|}> {
_bridge: BackendBridge;
_isProfiling: boolean = false;
@@ -94,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();
@@ -131,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(
@@ -159,6 +166,7 @@ export default class Agent extends EventEmitter<{|
bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported);
setupHighlighter(bridge, this);
setupTraceUpdates(this);
}
get rendererInterfaces(): {[key: RendererID]: RendererInterface} {
@@ -340,6 +348,8 @@ export default class Agent extends EventEmitter<{|
rendererInterface.startProfiling(this._recordChangeDescriptions);
}
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.
// It'll start tracking mounts for matches to the last selection path.
@@ -349,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) {
@@ -416,6 +439,10 @@ export default class Agent extends EventEmitter<{|
}
};
onTraceUpdates = (nodes: Set<NativeType>) => {
this.emit('traceUpdates', nodes);
};
onHookOperations = (operations: Array<number>) => {
if (__DEBUG__) {
debug('onHookOperations', operations);
+1
View File
@@ -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
];
@@ -912,6 +912,10 @@ export function attach(
// Not implemented.
}
function setTraceUpdatesEnabled(enabled: boolean) {
// Not implemented.
}
function setTrackedPath(path: Array<PathFrame> | null) {
// Not implemented.
}
@@ -945,6 +949,7 @@ export function attach(
setInHook,
setInProps,
setInState,
setTraceUpdatesEnabled,
setTrackedPath,
startProfiling,
stopProfiling,
+100 -9
View File
@@ -57,6 +57,7 @@ import type {
InspectedElement,
InspectedElementPayload,
InstanceAndStyle,
NativeType,
Owner,
PathFrame,
PathMatch,
@@ -532,6 +533,10 @@ export function attach(
const hideElementsWithPaths: Set<RegExp> = new Set();
const hideElementsWithTypes: Set<ElementType> = new Set();
// Highlight updates
let traceUpdatesEnabled: boolean = false;
let traceUpdatesForNodes: Set<NativeType> = new Set();
function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
hideElementsWithTypes.clear();
hideElementsWithDisplayNames.clear();
@@ -613,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;
});
@@ -1227,7 +1232,8 @@ export function attach(
function mountFiberRecursively(
fiber: Fiber,
parentFiber: Fiber | null,
traverseSiblings = false,
traverseSiblings: boolean,
traceNearestHostComponentUpdate: boolean,
) {
if (__DEBUG__) {
debug('mountFiberRecursively()', fiber, parentFiber);
@@ -1244,6 +1250,20 @@ export function attach(
recordMount(fiber, parentFiber);
}
if (traceUpdatesEnabled) {
if (traceNearestHostComponentUpdate) {
const elementType = getElementTypeForFiber(fiber);
// If an ancestor updated, we should mark the nearest host nodes for highlighting.
if (elementType === ElementTypeHostComponent) {
traceUpdatesForNodes.add(fiber.stateNode);
traceNearestHostComponentUpdate = false;
}
}
// We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
// because we don't want to highlight every host node inside of a newly mounted subtree.
}
const isTimedOutSuspense =
fiber.tag === ReactTypeOfWork.SuspenseComponent &&
fiber.memoizedState !== null;
@@ -1264,6 +1284,7 @@ export function attach(
fallbackChild,
shouldIncludeInTree ? fiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
} else {
@@ -1272,6 +1293,7 @@ export function attach(
fiber.child,
shouldIncludeInTree ? fiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
}
@@ -1281,7 +1303,12 @@ export function attach(
updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
if (traverseSiblings && fiber.sibling !== null) {
mountFiberRecursively(fiber.sibling, parentFiber, true);
mountFiberRecursively(
fiber.sibling,
parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
}
@@ -1430,11 +1457,35 @@ export function attach(
nextFiber: Fiber,
prevFiber: Fiber,
parentFiber: Fiber | null,
traceNearestHostComponentUpdate: boolean,
): boolean {
if (__DEBUG__) {
debug('updateFiberRecursively()', nextFiber, parentFiber);
}
if (traceUpdatesEnabled) {
const elementType = getElementTypeForFiber(nextFiber);
if (traceNearestHostComponentUpdate) {
// If an ancestor updated, we should mark the nearest host nodes for highlighting.
if (elementType === ElementTypeHostComponent) {
traceUpdatesForNodes.add(nextFiber.stateNode);
traceNearestHostComponentUpdate = false;
}
} else {
if (
elementType === ElementTypeFunction ||
elementType === ElementTypeClass ||
elementType === ElementTypeContext
) {
// Otherwise if this is a traced ancestor, flag for the nearest host descendant(s).
traceNearestHostComponentUpdate = didFiberRender(
prevFiber,
nextFiber,
);
}
}
}
if (
mostRecentlyInspectedElement !== null &&
mostRecentlyInspectedElement.id ===
@@ -1481,6 +1532,7 @@ export function attach(
nextFallbackChildSet,
prevFallbackChildSet,
nextFiber,
traceNearestHostComponentUpdate,
)
) {
shouldResetChildren = true;
@@ -1492,7 +1544,12 @@ export function attach(
// 2. Mount primary set
const nextPrimaryChildSet = nextFiber.child;
if (nextPrimaryChildSet !== null) {
mountFiberRecursively(nextPrimaryChildSet, nextFiber, true);
mountFiberRecursively(
nextPrimaryChildSet,
nextFiber,
true,
traceNearestHostComponentUpdate,
);
}
shouldResetChildren = true;
} else if (!prevDidTimeout && nextDidTimeOut) {
@@ -1507,7 +1564,12 @@ export function attach(
? nextFiberChild.sibling
: null;
if (nextFallbackChildSet != null) {
mountFiberRecursively(nextFallbackChildSet, nextFiber, true);
mountFiberRecursively(
nextFallbackChildSet,
nextFiber,
true,
traceNearestHostComponentUpdate,
);
shouldResetChildren = true;
}
} else {
@@ -1530,6 +1592,7 @@ export function attach(
nextChild,
prevChild,
shouldIncludeInTree ? nextFiber : parentFiber,
traceNearestHostComponentUpdate,
)
) {
// If a nested tree child order changed but it can't handle its own
@@ -1547,6 +1610,8 @@ export function attach(
mountFiberRecursively(
nextChild,
shouldIncludeInTree ? nextFiber : parentFiber,
false,
traceNearestHostComponentUpdate,
);
shouldResetChildren = true;
}
@@ -1562,6 +1627,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 (traceNearestHostComponentUpdate) {
const hostFibers = findAllCurrentHostFibers(
getFiberID(getPrimaryFiber(nextFiber)),
);
hostFibers.forEach(hostFiber => {
traceUpdatesForNodes.add(hostFiber.stateNode);
});
}
}
}
}
if (shouldIncludeInTree) {
@@ -1645,7 +1723,7 @@ export function attach(
};
}
mountFiberRecursively(root.current, null);
mountFiberRecursively(root.current, null, false, false);
flushPendingEvents(root);
currentRootID = -1;
});
@@ -1671,6 +1749,10 @@ export function attach(
mightBeOnTrackedPath = true;
}
if (traceUpdatesEnabled) {
traceUpdatesForNodes.clear();
}
// Checking root.memoizedInteractions handles multi-renderer edge-case-
// where some v16 renderers support profiling and others don't.
const isProfilingSupported = root.memoizedInteractions != null;
@@ -1704,10 +1786,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);
@@ -1716,7 +1798,7 @@ export function attach(
} else {
// Mount a new root.
setRootPseudoKey(currentRootID, current);
mountFiberRecursively(current, null);
mountFiberRecursively(current, null, false, false);
}
if (isProfiling && isProfilingSupported) {
@@ -1738,6 +1820,10 @@ export function attach(
// We're done here.
flushPendingEvents(root);
if (traceUpdatesEnabled) {
hook.emit('traceUpdates', traceUpdatesForNodes);
}
currentRootID = -1;
}
@@ -3015,6 +3101,10 @@ export function attach(
}
};
function setTraceUpdatesEnabled(isEnabled: boolean): void {
traceUpdatesEnabled = isEnabled;
}
return {
cleanup,
findNativeNodesForFiberID,
@@ -3036,6 +3126,7 @@ export function attach(
setInHook,
setInProps,
setInState,
setTraceUpdatesEnabled,
setTrackedPath,
startProfiling,
stopProfiling,
+2 -1
View File
@@ -252,10 +252,11 @@ export type RendererInterface = {
) => void,
setInProps: (id: number, path: Array<string | number>, value: any) => void,
setInState: (id: number, path: Array<string | number>, value: any) => void,
setTraceUpdatesEnabled: (enabled: boolean) => void,
setTrackedPath: (path: Array<PathFrame> | null) => void,
startProfiling: (recordChangeDescriptions: boolean) => void,
stopProfiling: () => void,
updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
};
export type Handler = (data: any) => void;
@@ -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>): 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',
@@ -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';
import type {NativeType} from '../../types';
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(nodeToData: Map<NativeType, Data>): 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);
nodeToData.forEach(({count, rect}) => {
if (rect !== null) {
const colorIndex = Math.min(COLORS.length - 1, count - 1);
const color = COLORS[colorIndex];
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);
}
@@ -0,0 +1,141 @@
/**
* 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 {NativeType} from '../../types';
import type {Rect} from '../utils';
// How long the rect should be shown for?
const DISPLAY_DURATION = 250;
// What's the longest we are willing to show the overlay for?
// This can be important if we're getting a flurry of events (e.g. scroll update).
const MAX_DISPLAY_DURATION = 3000;
// 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,
rect: Rect | null,
|};
const nodeToData: Map<NativeType, Data> = 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 {
isEnabled = value;
if (!isEnabled) {
nodeToData.clear();
if (drawAnimationFrameID !== null) {
cancelAnimationFrame(drawAnimationFrameID);
drawAnimationFrameID = null;
}
if (redrawTimeoutID !== null) {
clearTimeout(redrawTimeoutID);
redrawTimeoutID = null;
}
destroyCanvas();
}
}
function traceUpdates(nodes: Set<NativeType>): void {
if (!isEnabled) {
return;
}
nodes.forEach(node => {
const data = nodeToData.get(node);
const now = getCurrentTime();
let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0;
let rect = data != null ? data.rect : null;
if (rect === null || lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) {
lastMeasuredAt = now;
rect = measureNode(node);
}
nodeToData.set(node, {
count: data != null ? data.count + 1 : 1,
expirationTime:
data != null
? Math.min(
now + MAX_DISPLAY_DURATION,
data.expirationTime + DISPLAY_DURATION,
)
: now + DISPLAY_DURATION,
lastMeasuredAt,
rect,
});
});
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.
nodeToData.forEach((data, node) => {
if (data.expirationTime < now) {
nodeToData.delete(node);
} else {
earliestExpiration = Math.min(earliestExpiration, data.expirationTime);
}
});
draw(nodeToData);
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);
}
@@ -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>): 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),
};
}
+2
View File
@@ -70,6 +70,7 @@ type NativeStyleEditor_SetValueParams = {|
|};
type BackendEvents = {|
extensionBackendInitialized: [],
inspectedElement: [InspectedElementPayload],
isBackendStorageAPISupported: [boolean],
operations: [Array<number>],
@@ -108,6 +109,7 @@ type FrontendEvents = {|
profilingData: [ProfilingDataBackend],
reloadAndProfile: [boolean],
selectFiber: [number],
setTraceUpdatesEnabled: [boolean],
shutdown: [],
startInspectingNative: [],
startProfiling: [boolean],
+3
View File
@@ -30,6 +30,9 @@ export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
'React::DevTools::appendComponentStack';
export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
'React::DevTools::traceUpdatesEnabled';
export const PROFILER_EXPORT_VERSION = 4;
export const CHANGE_LOG_URL =
+10 -1
View File
@@ -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;
}
@@ -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 {
displayDensity,
setDisplayDensity,
theme,
setTheme,
appendComponentStack,
displayDensity,
setAppendComponentStack,
setDisplayDensity,
setTheme,
setTraceUpdatesEnabled,
theme,
traceUpdatesEnabled,
} = 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 (
<div className={styles.Settings}>
<div className={styles.Setting}>
<div className={styles.RadioLabel}>Theme</div>
<select className={styles.Select} value={theme} onChange={updateTheme}>
<select
className={styles.Select}
value={theme}
onChange={({currentTarget}) => setTheme(currentTarget.value)}>
<option value="auto">Auto</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
@@ -45,18 +47,37 @@ export default function GeneralSettings(_: {||}) {
<select
className={styles.Select}
value={displayDensity}
onChange={updateDisplayDensity}>
onChange={({currentTarget}) =>
setDisplayDensity(currentTarget.value)
}>
<option value="compact">Compact</option>
<option value="comfortable">Comfortable</option>
</select>
</div>
{supportsTraceUpdates && (
<div className={styles.Setting}>
<label>
<input
type="checkbox"
checked={traceUpdatesEnabled}
onChange={({currentTarget}) =>
setTraceUpdatesEnabled(currentTarget.checked)
}
/>{' '}
Highlight updates when components render.
</label>
</div>
)}
<div className={styles.Setting}>
<label>
<input
type="checkbox"
checked={appendComponentStack}
onChange={updateappendComponentStack}
onChange={({currentTarget}) =>
setAppendComponentStack(currentTarget.checked)
}
/>{' '}
Append component stacks to console warnings and errors.
</label>
@@ -18,6 +18,7 @@ import {
COMFORTABLE_LINE_HEIGHT,
COMPACT_LINE_HEIGHT,
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
} from 'react-devtools-shared/src/constants';
import {useLocalStorage} from '../hooks';
import {BridgeContext} from '../context';
@@ -40,6 +41,9 @@ type Context = {|
theme: Theme,
setTheme(value: Theme): void,
traceUpdatesEnabled: boolean,
setTraceUpdatesEnabled: (value: boolean) => void,
|};
const SettingsContext = createContext<Context>(((null: any): Context));
@@ -73,6 +77,9 @@ function SettingsContextController({
const [appendComponentStack, setAppendComponentStack] = useLocalStorage<
boolean,
>(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, true);
const [traceUpdatesEnabled, setTraceUpdatesEnabled] = useLocalStorage<
boolean,
>(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, false);
const documentElements = useMemo<DocumentElements>(
() => {
@@ -138,26 +145,37 @@ function SettingsContextController({
[bridge, appendComponentStack],
);
useEffect(
() => {
bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
},
[bridge, traceUpdatesEnabled],
);
const value = useMemo(
() => ({
displayDensity,
setDisplayDensity,
theme,
setTheme,
appendComponentStack,
setAppendComponentStack,
displayDensity,
lineHeight:
displayDensity === 'compact'
? COMPACT_LINE_HEIGHT
: COMFORTABLE_LINE_HEIGHT,
}),
[
displayDensity,
setAppendComponentStack,
setDisplayDensity,
setTheme,
appendComponentStack,
setAppendComponentStack,
setTraceUpdatesEnabled,
theme,
traceUpdatesEnabled,
}),
[
appendComponentStack,
displayDensity,
setAppendComponentStack,
setDisplayDensity,
setTheme,
setTraceUpdatesEnabled,
theme,
traceUpdatesEnabled,
],
);
+2
View File
@@ -8,6 +8,8 @@
<!-- Upcoming changes go here -->
#### 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))
</details>
## 4.1.3 (September 30, 2019)