Skip to content

Commit a7b8295

Browse files
piotrskihoxyq
andauthored
[DevTools] Show component names while highlighting renders (facebook#31577)
## 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 <[email protected]>
1 parent 56ae4b8 commit a7b8295

File tree

10 files changed

+505
-40
lines changed

10 files changed

+505
-40
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import {groupAndSortNodes} from 'react-devtools-shared/src/backend/views/TraceUpdates/canvas';
2+
3+
describe('Trace updates group and sort nodes', () => {
4+
test('should group nodes by position without changing order within group', () => {
5+
const nodeToData = new Map([
6+
[
7+
{id: 1},
8+
{
9+
rect: {left: 0, top: 0, width: 100, height: 100},
10+
color: '#80b393',
11+
displayName: 'Node1',
12+
count: 3,
13+
},
14+
],
15+
[
16+
{id: 2},
17+
{
18+
rect: {left: 0, top: 0, width: 100, height: 100},
19+
color: '#63b19e',
20+
displayName: 'Node2',
21+
count: 2,
22+
},
23+
],
24+
]);
25+
26+
const result = groupAndSortNodes(nodeToData);
27+
28+
expect(result).toEqual([
29+
[
30+
{
31+
rect: {left: 0, top: 0, width: 100, height: 100},
32+
color: '#80b393',
33+
displayName: 'Node1',
34+
count: 3,
35+
},
36+
{
37+
rect: {left: 0, top: 0, width: 100, height: 100},
38+
color: '#63b19e',
39+
displayName: 'Node2',
40+
count: 2,
41+
},
42+
],
43+
]);
44+
});
45+
46+
test('should sort groups by lowest count in each group', () => {
47+
const nodeToData = new Map([
48+
[
49+
{id: 1},
50+
{
51+
rect: {left: 0, top: 0, width: 100, height: 100},
52+
color: '#97b488',
53+
displayName: 'Group1',
54+
count: 4,
55+
},
56+
],
57+
[
58+
{id: 2},
59+
{
60+
rect: {left: 100, top: 0, width: 100, height: 100},
61+
color: '#37afa9',
62+
displayName: 'Group2',
63+
count: 1,
64+
},
65+
],
66+
[
67+
{id: 3},
68+
{
69+
rect: {left: 200, top: 0, width: 100, height: 100},
70+
color: '#63b19e',
71+
displayName: 'Group3',
72+
count: 2,
73+
},
74+
],
75+
]);
76+
77+
const result = groupAndSortNodes(nodeToData);
78+
79+
expect(result).toEqual([
80+
[
81+
{
82+
rect: {left: 100, top: 0, width: 100, height: 100},
83+
color: '#37afa9',
84+
displayName: 'Group2',
85+
count: 1,
86+
},
87+
],
88+
[
89+
{
90+
rect: {left: 200, top: 0, width: 100, height: 100},
91+
color: '#63b19e',
92+
displayName: 'Group3',
93+
count: 2,
94+
},
95+
],
96+
[
97+
{
98+
rect: {left: 0, top: 0, width: 100, height: 100},
99+
color: '#97b488',
100+
displayName: 'Group1',
101+
count: 4,
102+
},
103+
],
104+
]);
105+
});
106+
107+
test('should maintain order within groups while sorting groups by lowest count', () => {
108+
const nodeToData = new Map([
109+
[
110+
{id: 1},
111+
{
112+
rect: {left: 0, top: 0, width: 50, height: 50},
113+
color: '#97b488',
114+
displayName: 'Pos1Node1',
115+
count: 4,
116+
},
117+
],
118+
[
119+
{id: 2},
120+
{
121+
rect: {left: 0, top: 0, width: 60, height: 60},
122+
color: '#63b19e',
123+
displayName: 'Pos1Node2',
124+
count: 2,
125+
},
126+
],
127+
[
128+
{id: 3},
129+
{
130+
rect: {left: 100, top: 0, width: 70, height: 70},
131+
color: '#80b393',
132+
displayName: 'Pos2Node1',
133+
count: 3,
134+
},
135+
],
136+
[
137+
{id: 4},
138+
{
139+
rect: {left: 100, top: 0, width: 80, height: 80},
140+
color: '#37afa9',
141+
displayName: 'Pos2Node2',
142+
count: 1,
143+
},
144+
],
145+
]);
146+
147+
const result = groupAndSortNodes(nodeToData);
148+
149+
expect(result).toEqual([
150+
[
151+
{
152+
rect: {left: 100, top: 0, width: 70, height: 70},
153+
color: '#80b393',
154+
displayName: 'Pos2Node1',
155+
count: 3,
156+
},
157+
{
158+
rect: {left: 100, top: 0, width: 80, height: 80},
159+
color: '#37afa9',
160+
displayName: 'Pos2Node2',
161+
count: 1,
162+
},
163+
],
164+
[
165+
{
166+
rect: {left: 0, top: 0, width: 50, height: 50},
167+
color: '#97b488',
168+
displayName: 'Pos1Node1',
169+
count: 4,
170+
},
171+
{
172+
rect: {left: 0, top: 0, width: 60, height: 60},
173+
color: '#63b19e',
174+
displayName: 'Pos1Node2',
175+
count: 2,
176+
},
177+
],
178+
]);
179+
});
180+
181+
test('should handle multiple groups with same minimum count', () => {
182+
const nodeToData = new Map([
183+
[
184+
{id: 1},
185+
{
186+
rect: {left: 0, top: 0, width: 100, height: 100},
187+
color: '#37afa9',
188+
displayName: 'Group1Node1',
189+
count: 1,
190+
},
191+
],
192+
[
193+
{id: 2},
194+
{
195+
rect: {left: 100, top: 0, width: 100, height: 100},
196+
color: '#37afa9',
197+
displayName: 'Group2Node1',
198+
count: 1,
199+
},
200+
],
201+
]);
202+
203+
const result = groupAndSortNodes(nodeToData);
204+
205+
expect(result).toEqual([
206+
[
207+
{
208+
rect: {left: 0, top: 0, width: 100, height: 100},
209+
color: '#37afa9',
210+
displayName: 'Group1Node1',
211+
count: 1,
212+
},
213+
],
214+
[
215+
{
216+
rect: {left: 100, top: 0, width: 100, height: 100},
217+
color: '#37afa9',
218+
displayName: 'Group2Node1',
219+
count: 1,
220+
},
221+
],
222+
]);
223+
});
224+
225+
test('should filter out nodes without rect property', () => {
226+
const nodeToData = new Map([
227+
[
228+
{id: 1},
229+
{
230+
rect: null,
231+
color: '#37afa9',
232+
displayName: 'NoRectNode',
233+
count: 1,
234+
},
235+
],
236+
[
237+
{id: 2},
238+
{
239+
rect: undefined,
240+
color: '#63b19e',
241+
displayName: 'UndefinedRectNode',
242+
count: 2,
243+
},
244+
],
245+
[
246+
{id: 3},
247+
{
248+
rect: {left: 0, top: 0, width: 100, height: 100},
249+
color: '#80b393',
250+
displayName: 'ValidNode',
251+
count: 3,
252+
},
253+
],
254+
]);
255+
256+
const result = groupAndSortNodes(nodeToData);
257+
258+
expect(result).toEqual([
259+
[
260+
{
261+
rect: {left: 0, top: 0, width: 100, height: 100},
262+
color: '#80b393',
263+
displayName: 'ValidNode',
264+
count: 3,
265+
},
266+
],
267+
]);
268+
});
269+
});

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
DevToolsHookSettings,
2929
} from './types';
3030
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
31+
import type {GroupItem} from './views/TraceUpdates/canvas';
3132
import {isReactNativeEnvironment} from './utils';
3233
import {
3334
sessionStorageGetItem,
@@ -142,10 +143,12 @@ export default class Agent extends EventEmitter<{
142143
shutdown: [],
143144
traceUpdates: [Set<HostInstance>],
144145
drawTraceUpdates: [Array<HostInstance>],
146+
drawGroupedTraceUpdatesWithNames: [Array<Array<GroupItem>>],
145147
disableTraceUpdates: [],
146148
getIfHasUnsupportedRendererVersion: [],
147149
updateHookSettings: [$ReadOnly<DevToolsHookSettings>],
148150
getHookSettings: [],
151+
showNamesWhenTracing: [boolean],
149152
}> {
150153
_bridge: BackendBridge;
151154
_isProfiling: boolean = false;
@@ -156,6 +159,7 @@ export default class Agent extends EventEmitter<{
156159
_onReloadAndProfile:
157160
| ((recordChangeDescriptions: boolean, recordTimeline: boolean) => void)
158161
| void;
162+
_showNamesWhenTracing: boolean = true;
159163

160164
constructor(
161165
bridge: BackendBridge,
@@ -200,6 +204,7 @@ export default class Agent extends EventEmitter<{
200204
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
201205
bridge.addListener('renamePath', this.renamePath);
202206
bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
207+
bridge.addListener('setShowNamesWhenTracing', this.setShowNamesWhenTracing);
203208
bridge.addListener('startProfiling', this.startProfiling);
204209
bridge.addListener('stopProfiling', this.stopProfiling);
205210
bridge.addListener('storeAsGlobal', this.storeAsGlobal);
@@ -722,6 +727,7 @@ export default class Agent extends EventEmitter<{
722727
this._traceUpdatesEnabled = traceUpdatesEnabled;
723728

724729
setTraceUpdatesEnabled(traceUpdatesEnabled);
730+
this.emit('showNamesWhenTracing', this._showNamesWhenTracing);
725731

726732
for (const rendererID in this._rendererInterfaces) {
727733
const renderer = ((this._rendererInterfaces[
@@ -731,6 +737,14 @@ export default class Agent extends EventEmitter<{
731737
}
732738
};
733739

740+
setShowNamesWhenTracing: (show: boolean) => void = show => {
741+
if (this._showNamesWhenTracing === show) {
742+
return;
743+
}
744+
this._showNamesWhenTracing = show;
745+
this.emit('showNamesWhenTracing', show);
746+
};
747+
734748
syncSelectionFromBuiltinElementsPanel: () => void = () => {
735749
const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
736750
if (target == null) {

0 commit comments

Comments
 (0)