mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
## Summary This PR improves the Trace Updates feature by letting developers see component names directly on the update overlay. Before this change, the overlay only highlighted updated regions, leaving it unclear which components were involved. With this update, you can now match visual updates to their corresponding components, making it much easier to debug rendering performance. ### New Feature: Show component names while highlighting When the new **"Show component names while highlighting"** setting is enabled, the update overlay display the names of affected components above the rectangles, along with the update count. This gives immediate context about what’s rendering and why. The preference is stored in local storage and synced with the backend, so it’s remembered across sessions. ### Improvements to Drawing Logic The drawing logic has been updated to make the overlay sharper and easier to read. Overlay now respect device pixel ratios, so they look great on high-DPI screens. Outlines have also been made crisper, which makes it easier to spot exactly where updates are happening. > [!NOTE] > **Grouping Logic and Limitations** > Updates are grouped by their screen position `(left, top coordinates)` to combine overlapping or nearby regions into a single group. Groups are sorted by the highest update count within each group, making the most frequently updated components stand out. > Overlapping labels may still occur when multiple updates involve components that overlap but are not in the exact same position. This is intentional, as the logic aims to maintain a straightforward mapping between update regions and component names without introducing unnecessary complexity. ### Testing This PR also adds tests for the new `groupAndSortNodes` utility, which handles the logic for grouping and sorting updates. The tests ensure the behavior is reliable across different scenarios. ## Before & After https://github.com/user-attachments/assets/6ea0fe3e-9354-44fa-95f3-9a867554f74c https://github.com/user-attachments/assets/32af4d98-92a5-47dd-a732-f05c2293e41b --------- Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>
222 lines
5.4 KiB
JavaScript
222 lines
5.4 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {HostInstance} from '../../types';
|
|
import type Agent from '../../agent';
|
|
|
|
import {isReactNativeEnvironment} from 'react-devtools-shared/src/backend/utils';
|
|
|
|
// 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;
|
|
|
|
function drawNative(nodeToData: Map<HostInstance, Data>, agent: Agent) {
|
|
const nodesToDraw = [];
|
|
iterateNodes(nodeToData, ({color, node}) => {
|
|
nodesToDraw.push({node, color});
|
|
});
|
|
|
|
agent.emit('drawTraceUpdates', nodesToDraw);
|
|
|
|
const mergedNodes = groupAndSortNodes(nodeToData);
|
|
agent.emit('drawGroupedTraceUpdatesWithNames', mergedNodes);
|
|
}
|
|
|
|
function drawWeb(nodeToData: Map<HostInstance, Data>) {
|
|
if (canvas === null) {
|
|
initialize();
|
|
}
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const canvasFlow: HTMLCanvasElement = ((canvas: any): HTMLCanvasElement);
|
|
canvasFlow.width = window.innerWidth * dpr;
|
|
canvasFlow.height = window.innerHeight * dpr;
|
|
canvasFlow.style.width = `${window.innerWidth}px`;
|
|
canvasFlow.style.height = `${window.innerHeight}px`;
|
|
|
|
const context = canvasFlow.getContext('2d');
|
|
context.scale(dpr, dpr);
|
|
|
|
context.clearRect(0, 0, canvasFlow.width / dpr, canvasFlow.height / dpr);
|
|
|
|
const mergedNodes = groupAndSortNodes(nodeToData);
|
|
|
|
mergedNodes.forEach(group => {
|
|
drawGroupBorders(context, group);
|
|
drawGroupLabel(context, group);
|
|
});
|
|
}
|
|
|
|
type GroupItem = {
|
|
rect: Rect,
|
|
color: string,
|
|
displayName: string | null,
|
|
count: number,
|
|
};
|
|
|
|
export type {GroupItem};
|
|
|
|
export function groupAndSortNodes(
|
|
nodeToData: Map<HostInstance, Data>,
|
|
): Array<Array<GroupItem>> {
|
|
const positionGroups: Map<string, Array<GroupItem>> = new Map();
|
|
|
|
iterateNodes(nodeToData, ({rect, color, displayName, count}) => {
|
|
if (!rect) return;
|
|
const key = `${rect.left},${rect.top}`;
|
|
if (!positionGroups.has(key)) positionGroups.set(key, []);
|
|
positionGroups.get(key)?.push({rect, color, displayName, count});
|
|
});
|
|
|
|
return Array.from(positionGroups.values()).sort((groupA, groupB) => {
|
|
const maxCountA = Math.max(...groupA.map(item => item.count));
|
|
const maxCountB = Math.max(...groupB.map(item => item.count));
|
|
return maxCountA - maxCountB;
|
|
});
|
|
}
|
|
|
|
function drawGroupBorders(
|
|
context: CanvasRenderingContext2D,
|
|
group: Array<GroupItem>,
|
|
) {
|
|
group.forEach(({color, rect}) => {
|
|
context.beginPath();
|
|
context.strokeStyle = color;
|
|
context.rect(rect.left, rect.top, rect.width - 1, rect.height - 1);
|
|
context.stroke();
|
|
});
|
|
}
|
|
|
|
function drawGroupLabel(
|
|
context: CanvasRenderingContext2D,
|
|
group: Array<GroupItem>,
|
|
) {
|
|
const mergedName = group
|
|
.map(({displayName, count}) =>
|
|
displayName ? `${displayName}${count > 1 ? ` x${count}` : ''}` : '',
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ');
|
|
|
|
if (mergedName) {
|
|
drawLabel(context, group[0].rect, mergedName, group[0].color);
|
|
}
|
|
}
|
|
|
|
export function draw(nodeToData: Map<HostInstance, Data>, agent: Agent): void {
|
|
return isReactNativeEnvironment()
|
|
? drawNative(nodeToData, agent)
|
|
: drawWeb(nodeToData);
|
|
}
|
|
|
|
type DataWithColorAndNode = {
|
|
...Data,
|
|
color: string,
|
|
node: HostInstance,
|
|
};
|
|
|
|
function iterateNodes(
|
|
nodeToData: Map<HostInstance, Data>,
|
|
execute: (data: DataWithColorAndNode) => void,
|
|
) {
|
|
nodeToData.forEach((data, node) => {
|
|
const colorIndex = Math.min(COLORS.length - 1, data.count - 1);
|
|
const color = COLORS[colorIndex];
|
|
execute({
|
|
color,
|
|
node,
|
|
count: data.count,
|
|
displayName: data.displayName,
|
|
expirationTime: data.expirationTime,
|
|
lastMeasuredAt: data.lastMeasuredAt,
|
|
rect: data.rect,
|
|
});
|
|
});
|
|
}
|
|
|
|
function drawLabel(
|
|
context: CanvasRenderingContext2D,
|
|
rect: Rect,
|
|
text: string,
|
|
color: string,
|
|
): void {
|
|
const {left, top} = rect;
|
|
context.font = '10px monospace';
|
|
context.textBaseline = 'middle';
|
|
context.textAlign = 'center';
|
|
|
|
const padding = 2;
|
|
const textHeight = 14;
|
|
|
|
const metrics = context.measureText(text);
|
|
const backgroundWidth = metrics.width + padding * 2;
|
|
const backgroundHeight = textHeight;
|
|
const labelX = left;
|
|
const labelY = top - backgroundHeight;
|
|
|
|
context.fillStyle = color;
|
|
context.fillRect(labelX, labelY, backgroundWidth, backgroundHeight);
|
|
|
|
context.fillStyle = '#000000';
|
|
context.fillText(
|
|
text,
|
|
labelX + backgroundWidth / 2,
|
|
labelY + backgroundHeight / 2,
|
|
);
|
|
}
|
|
|
|
function destroyNative(agent: Agent) {
|
|
agent.emit('disableTraceUpdates');
|
|
}
|
|
|
|
function destroyWeb() {
|
|
if (canvas !== null) {
|
|
if (canvas.parentNode != null) {
|
|
canvas.parentNode.removeChild(canvas);
|
|
}
|
|
canvas = null;
|
|
}
|
|
}
|
|
|
|
export function destroy(agent: Agent): void {
|
|
return isReactNativeEnvironment() ? destroyNative(agent) : destroyWeb();
|
|
}
|
|
|
|
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);
|
|
}
|