Skip to content

Commit 4cae176

Browse files
committed
feat: improve profile grouping output
1 parent f22798b commit 4cae176

13 files changed

Lines changed: 924 additions & 51 deletions

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,15 @@ agent-react-devtools profile slow
114114

115115
```
116116
Slowest (by avg render time):
117-
@c5 [fn] TodoList avg:4.2ms max:8.1ms renders:6 causes:props-changed changed: props: items, onDelete
118-
@c4 [fn] SearchBar avg:2.1ms max:3.4ms renders:12 causes:hooks-changed changed: hooks: #0
119-
@c2 [fn] Header avg:0.8ms max:1.2ms renders:3 causes:parent-rendered
117+
TodoItem 3 instances src: TodoItem.tsx:14:1 top avg:4.2ms
118+
@c5 [fn] TodoItem avg:4.2ms max:8.1ms renders:6 causes:props-changed in:TodoList > VisibleItems changed: props: item, onDelete
119+
@c8 [fn] TodoItem avg:3.9ms max:7.5ms renders:6 causes:props-changed in:TodoList > ArchivedItems changed: props: item, onDelete
120+
@c9 [fn] TodoItem avg:3.5ms max:6.8ms renders:6 causes:props-changed in:TodoList > SearchResults changed: props: item, onDelete
121+
@c4 [fn] SearchBar avg:2.1ms max:3.4ms renders:12 causes:hooks-changed in:App > Header changed: hooks: #0
120122
```
121123

124+
Repeated rows are grouped only when they share the same implementation source. Components with the same display name from different files, or components without source metadata, remain separate rows.
125+
122126
## Commands
123127

124128
### Daemon

packages/agent-react-devtools/src/__tests__/component-tree.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,21 @@ describe('ComponentTree', () => {
194194
expect(shallow.map((n) => n.displayName)).toEqual(['App', 'Level1']);
195195
});
196196

197+
it('should build parent path strings', () => {
198+
const ops = buildOps(1, 100, ['App', 'SearchPage', 'FiltersPanel', 'Context.Provider'], (s) => [
199+
...addOp(1, 5, 0, s('App')),
200+
...addOp(2, 5, 1, s('SearchPage')),
201+
...addOp(3, 5, 2, s('FiltersPanel')),
202+
...addOp(4, 5, 3, s('Context.Provider')),
203+
]);
204+
tree.applyOperations(ops);
205+
206+
expect(tree.getPathString(4)).toBe('App > SearchPage > FiltersPanel');
207+
expect(tree.getPathString(4, true)).toBe('App > SearchPage > FiltersPanel > Context.Provider');
208+
expect(tree.getPathString(4, false, 2)).toBe('SearchPage > FiltersPanel');
209+
expect(tree.getPathString(1)).toBeUndefined();
210+
});
211+
197212
describe('subtree extraction (rootId)', () => {
198213
it('should get subtree from a specific root', () => {
199214
const ops = buildOps(1, 100, ['App', 'Header', 'Nav', 'Logo', 'Footer'], (s) => [
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { DevToolsBridge } from '../devtools-bridge.js';
3+
import { ComponentTree } from '../component-tree.js';
4+
import { Profiler } from '../profiler.js';
5+
6+
function buildStringTable(strings: string[]): [number[], Map<string, number>] {
7+
const idMap = new Map<string, number>();
8+
const data: number[] = [];
9+
for (const s of strings) {
10+
const id = idMap.size + 1;
11+
if (!idMap.has(s)) {
12+
idMap.set(s, id);
13+
data.push(s.length, ...Array.from(s).map((c) => c.charCodeAt(0)));
14+
}
15+
}
16+
return [data, idMap];
17+
}
18+
19+
function buildOps(
20+
rendererID: number,
21+
rootID: number,
22+
strings: string[],
23+
opsFn: (strId: (s: string) => number) => number[],
24+
): number[] {
25+
const [tableData, idMap] = buildStringTable(strings);
26+
const strId = (s: string) => idMap.get(s) || 0;
27+
const ops = opsFn(strId);
28+
return [rendererID, rootID, tableData.length, ...tableData, ...ops];
29+
}
30+
31+
function addOp(
32+
id: number,
33+
elementType: number,
34+
parentId: number,
35+
displayNameStrId: number,
36+
): number[] {
37+
return [1, id, elementType, parentId, 0, displayNameStrId, 0];
38+
}
39+
40+
describe('DevToolsBridge', () => {
41+
let tree: ComponentTree;
42+
let bridge: DevToolsBridge;
43+
44+
beforeEach(() => {
45+
tree = new ComponentTree();
46+
bridge = new DevToolsBridge(8097, tree, new Profiler());
47+
48+
const ops = buildOps(1, 100, ['App'], (s) => [
49+
...addOp(1, 5, 0, s('App')),
50+
]);
51+
tree.applyOperations(ops);
52+
});
53+
54+
afterEach(() => {
55+
vi.restoreAllMocks();
56+
});
57+
58+
it('preserves source metadata from inspected element payloads', async () => {
59+
const pending = new Promise((resolve) => {
60+
(bridge as any).pendingInspections.set(123, {
61+
id: 1,
62+
resolve,
63+
timer: setTimeout(() => {}, 1000),
64+
});
65+
});
66+
67+
(bridge as any).handleInspectedElement({
68+
type: 'full-data',
69+
id: 1,
70+
responseID: 123,
71+
value: {
72+
id: 1,
73+
displayName: 'App',
74+
type: 5,
75+
key: null,
76+
props: {},
77+
state: null,
78+
hooks: null,
79+
source: {
80+
fileName: '/src/App.tsx',
81+
lineNumber: 10,
82+
columnNumber: 4,
83+
},
84+
},
85+
});
86+
87+
await expect(pending).resolves.toMatchObject({
88+
source: {
89+
fileName: '/src/App.tsx',
90+
lineNumber: 10,
91+
columnNumber: 4,
92+
},
93+
});
94+
});
95+
96+
it('keeps inspection working when source metadata is missing', async () => {
97+
const pending = new Promise((resolve) => {
98+
(bridge as any).pendingInspections.set(123, {
99+
id: 1,
100+
resolve,
101+
timer: setTimeout(() => {}, 1000),
102+
});
103+
});
104+
105+
(bridge as any).handleInspectedElement({
106+
type: 'full-data',
107+
id: 1,
108+
responseID: 123,
109+
value: {
110+
id: 1,
111+
displayName: 'App',
112+
type: 5,
113+
key: null,
114+
props: { count: 1 },
115+
state: null,
116+
hooks: null,
117+
},
118+
});
119+
120+
await expect(pending).resolves.toMatchObject({
121+
displayName: 'App',
122+
props: { count: 1 },
123+
source: undefined,
124+
});
125+
});
126+
127+
it('returns cached inspection data without a live connection', async () => {
128+
(bridge as any).inspectionCache.set(1, {
129+
id: 1,
130+
displayName: 'App',
131+
type: 'function',
132+
key: null,
133+
props: { cached: true },
134+
state: null,
135+
hooks: null,
136+
renderedAt: null,
137+
source: undefined,
138+
});
139+
140+
await expect(bridge.inspectElement(1)).resolves.toMatchObject({
141+
props: { cached: true },
142+
});
143+
});
144+
145+
it('supports concurrent inspections for the same component id', async () => {
146+
const sent: Array<{ event: string; payload: { requestID: number } }> = [];
147+
(bridge as any).connections.add({});
148+
(bridge as any).sendToAll = (msg: { event: string; payload: { requestID: number } }) => {
149+
sent.push(msg);
150+
};
151+
152+
const first = bridge.inspectElement(1);
153+
const second = bridge.inspectElement(1);
154+
155+
expect(sent).toHaveLength(2);
156+
expect(sent[0].payload.requestID).not.toBe(sent[1].payload.requestID);
157+
expect((bridge as any).pendingInspections.size).toBe(2);
158+
159+
(bridge as any).handleInspectedElement({
160+
type: 'full-data',
161+
id: 1,
162+
responseID: sent[0].payload.requestID,
163+
value: {
164+
id: 1,
165+
displayName: 'App',
166+
type: 5,
167+
key: null,
168+
props: { source: 'first' },
169+
state: null,
170+
hooks: null,
171+
},
172+
});
173+
(bridge as any).handleInspectedElement({
174+
type: 'full-data',
175+
id: 1,
176+
responseID: sent[1].payload.requestID,
177+
value: {
178+
id: 1,
179+
displayName: 'App',
180+
type: 5,
181+
key: null,
182+
props: { source: 'second' },
183+
state: null,
184+
hooks: null,
185+
},
186+
});
187+
188+
await expect(first).resolves.toMatchObject({ props: { source: 'first' } });
189+
await expect(second).resolves.toMatchObject({ props: { source: 'second' } });
190+
});
191+
});

0 commit comments

Comments
 (0)