agent-react-devtools 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/cli.js +584 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +1091 -0
- package/dist/daemon.js.map +1 -0
- package/package.json +35 -1
- package/src/__tests__/cli-parser.test.ts +76 -0
- package/src/__tests__/component-tree.test.ts +229 -0
- package/src/__tests__/formatters.test.ts +189 -0
- package/src/__tests__/profiler.test.ts +264 -0
- package/src/cli.ts +315 -0
- package/src/component-tree.ts +495 -0
- package/src/daemon-client.ts +144 -0
- package/src/daemon.ts +275 -0
- package/src/devtools-bridge.ts +391 -0
- package/src/formatters.ts +270 -0
- package/src/profiler.ts +356 -0
- package/src/types.ts +126 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StatusInfo,
|
|
3
|
+
InspectedElement,
|
|
4
|
+
ComponentRenderReport,
|
|
5
|
+
} from './types.js';
|
|
6
|
+
import type { TreeNode } from './component-tree.js';
|
|
7
|
+
import type { ProfileSummary, TimelineEntry, CommitDetail } from './profiler.js';
|
|
8
|
+
|
|
9
|
+
// ── Abbreviations for component types ──
|
|
10
|
+
const TYPE_ABBREV: Record<string, string> = {
|
|
11
|
+
function: 'fn',
|
|
12
|
+
class: 'cls',
|
|
13
|
+
host: 'host',
|
|
14
|
+
memo: 'memo',
|
|
15
|
+
forwardRef: 'fRef',
|
|
16
|
+
profiler: 'prof',
|
|
17
|
+
suspense: 'susp',
|
|
18
|
+
context: 'ctx',
|
|
19
|
+
other: '?',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function typeTag(type: string): string {
|
|
23
|
+
return TYPE_ABBREV[type] || type;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Tree connector characters ──
|
|
27
|
+
const PIPE = '│ ';
|
|
28
|
+
const TEE = '├─ ';
|
|
29
|
+
const ELBOW = '└─ ';
|
|
30
|
+
const SPACE = ' ';
|
|
31
|
+
|
|
32
|
+
export function formatTree(nodes: TreeNode[]): string {
|
|
33
|
+
if (nodes.length === 0) return 'No components (is a React app connected?)';
|
|
34
|
+
|
|
35
|
+
// Build tree structure from the flat list
|
|
36
|
+
const childrenMap = new Map<number | null, TreeNode[]>();
|
|
37
|
+
for (const node of nodes) {
|
|
38
|
+
const parentId = node.parentId;
|
|
39
|
+
let siblings = childrenMap.get(parentId);
|
|
40
|
+
if (!siblings) {
|
|
41
|
+
siblings = [];
|
|
42
|
+
childrenMap.set(parentId, siblings);
|
|
43
|
+
}
|
|
44
|
+
siblings.push(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines: string[] = [];
|
|
48
|
+
|
|
49
|
+
function walk(nodeId: number, prefix: string, isLast: boolean, isRoot: boolean): void {
|
|
50
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
51
|
+
if (!node) return;
|
|
52
|
+
|
|
53
|
+
const connector = isRoot ? '' : isLast ? ELBOW : TEE;
|
|
54
|
+
let line = `${node.label} [${typeTag(node.type)}] "${node.displayName}"`;
|
|
55
|
+
if (node.key) line += ` key="${node.key}"`;
|
|
56
|
+
|
|
57
|
+
lines.push(`${prefix}${connector}${line}`);
|
|
58
|
+
|
|
59
|
+
const children = childrenMap.get(node.id) || [];
|
|
60
|
+
const childPrefix = isRoot ? '' : prefix + (isLast ? SPACE : PIPE);
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < children.length; i++) {
|
|
63
|
+
walk(children[i].id, childPrefix, i === children.length - 1, false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Find root nodes
|
|
68
|
+
const roots = childrenMap.get(null) || [];
|
|
69
|
+
for (let i = 0; i < roots.length; i++) {
|
|
70
|
+
walk(roots[i].id, '', i === roots.length - 1, true);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatComponent(element: InspectedElement, label?: string): string {
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
|
|
79
|
+
const ref = label || `#${element.id}`;
|
|
80
|
+
let header = `${ref} [${typeTag(element.type)}] "${element.displayName}"`;
|
|
81
|
+
if (element.key) header += ` key="${element.key}"`;
|
|
82
|
+
lines.push(header);
|
|
83
|
+
|
|
84
|
+
// Props
|
|
85
|
+
if (element.props && Object.keys(element.props).length > 0) {
|
|
86
|
+
lines.push('props:');
|
|
87
|
+
for (const [key, value] of Object.entries(element.props)) {
|
|
88
|
+
lines.push(` ${key}: ${formatCompactValue(value) ?? 'undefined'}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// State
|
|
93
|
+
if (element.state && Object.keys(element.state).length > 0) {
|
|
94
|
+
lines.push('state:');
|
|
95
|
+
for (const [key, value] of Object.entries(element.state)) {
|
|
96
|
+
lines.push(` ${key}: ${formatCompactValue(value) ?? 'undefined'}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Hooks
|
|
101
|
+
if (element.hooks && element.hooks.length > 0) {
|
|
102
|
+
lines.push('hooks:');
|
|
103
|
+
for (const h of element.hooks) {
|
|
104
|
+
const val = formatCompactValue(h.value);
|
|
105
|
+
lines.push(val !== undefined ? ` ${h.name}: ${val}` : ` ${h.name}`);
|
|
106
|
+
if (h.subHooks && h.subHooks.length > 0) {
|
|
107
|
+
for (const sh of h.subHooks) {
|
|
108
|
+
const sval = formatCompactValue(sh.value);
|
|
109
|
+
lines.push(sval !== undefined ? ` ${sh.name}: ${sval}` : ` ${sh.name}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatSearchResults(results: TreeNode[]): string {
|
|
119
|
+
if (results.length === 0) return 'No components found';
|
|
120
|
+
|
|
121
|
+
return results
|
|
122
|
+
.map((n) => {
|
|
123
|
+
let line = `${n.label} [${typeTag(n.type)}] "${n.displayName}"`;
|
|
124
|
+
if (n.key) line += ` key="${n.key}"`;
|
|
125
|
+
return line;
|
|
126
|
+
})
|
|
127
|
+
.join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function formatCount(counts: Record<string, number>): string {
|
|
131
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
132
|
+
const parts = Object.entries(counts)
|
|
133
|
+
.sort((a, b) => b[1] - a[1])
|
|
134
|
+
.map(([type, count]) => `${typeTag(type)}:${count}`)
|
|
135
|
+
.join(' ');
|
|
136
|
+
return `${total} components (${parts})`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatStatus(status: StatusInfo): string {
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
lines.push(`Daemon: running (port ${status.port})`);
|
|
142
|
+
lines.push(
|
|
143
|
+
`Apps: ${status.connectedApps} connected, ${status.componentCount} components`,
|
|
144
|
+
);
|
|
145
|
+
if (status.profilingActive) {
|
|
146
|
+
lines.push('Profiling: active');
|
|
147
|
+
}
|
|
148
|
+
const upSec = Math.round(status.uptime / 1000);
|
|
149
|
+
lines.push(`Uptime: ${upSec}s`);
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function formatProfileSummary(summary: ProfileSummary): string {
|
|
154
|
+
const lines: string[] = [];
|
|
155
|
+
const durSec = (summary.duration / 1000).toFixed(1);
|
|
156
|
+
lines.push(
|
|
157
|
+
`Profile "${summary.name}" (${durSec}s, ${summary.commitCount} commits)`,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (summary.componentRenderCounts.length > 0) {
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push('Top renders:');
|
|
163
|
+
for (const c of summary.componentRenderCounts.slice(0, 10)) {
|
|
164
|
+
const name = c.displayName || `#${c.id}`;
|
|
165
|
+
lines.push(` ${name} ${c.count} renders`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function formatProfileReport(report: ComponentRenderReport, label?: string): string {
|
|
173
|
+
const lines: string[] = [];
|
|
174
|
+
const ref = label || `#${report.id}`;
|
|
175
|
+
lines.push(`${ref} "${report.displayName}"`);
|
|
176
|
+
lines.push(
|
|
177
|
+
`renders:${report.renderCount} avg:${report.avgDuration.toFixed(1)}ms max:${report.maxDuration.toFixed(1)}ms total:${report.totalDuration.toFixed(1)}ms`,
|
|
178
|
+
);
|
|
179
|
+
if (report.causes.length > 0) {
|
|
180
|
+
lines.push(`causes: ${report.causes.join(', ')}`);
|
|
181
|
+
}
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatSlowest(reports: ComponentRenderReport[]): string {
|
|
186
|
+
if (reports.length === 0) return 'No profiling data';
|
|
187
|
+
|
|
188
|
+
const lines: string[] = ['Slowest (by avg render time):'];
|
|
189
|
+
for (const r of reports) {
|
|
190
|
+
const cause = r.causes[0] || '?';
|
|
191
|
+
lines.push(
|
|
192
|
+
` ${pad(r.displayName, 20)} avg:${r.avgDuration.toFixed(1)}ms max:${r.maxDuration.toFixed(1)}ms renders:${r.renderCount} cause:${cause}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function formatRerenders(reports: ComponentRenderReport[]): string {
|
|
199
|
+
if (reports.length === 0) return 'No profiling data';
|
|
200
|
+
|
|
201
|
+
const lines: string[] = ['Most re-renders:'];
|
|
202
|
+
for (const r of reports) {
|
|
203
|
+
const cause = r.causes[0] || '?';
|
|
204
|
+
lines.push(
|
|
205
|
+
` ${pad(r.displayName, 20)} ${r.renderCount} renders — ${cause}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return lines.join('\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function formatTimeline(entries: TimelineEntry[]): string {
|
|
212
|
+
if (entries.length === 0) return 'No profiling data';
|
|
213
|
+
|
|
214
|
+
const lines: string[] = ['Commit timeline:'];
|
|
215
|
+
for (const e of entries) {
|
|
216
|
+
lines.push(
|
|
217
|
+
` #${e.index} ${e.duration.toFixed(1)}ms ${e.componentCount} components`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function formatCommitDetail(detail: CommitDetail): string {
|
|
224
|
+
const lines: string[] = [];
|
|
225
|
+
lines.push(`Commit #${detail.index} ${detail.duration.toFixed(1)}ms ${detail.totalComponents} components`);
|
|
226
|
+
lines.push('');
|
|
227
|
+
for (const c of detail.components) {
|
|
228
|
+
const causes = c.causes.length > 0 ? c.causes.join(', ') : '?';
|
|
229
|
+
lines.push(` ${pad(c.displayName, 24)} self:${c.selfDuration.toFixed(1)}ms total:${c.actualDuration.toFixed(1)}ms ${causes}`);
|
|
230
|
+
}
|
|
231
|
+
const hidden = detail.totalComponents - detail.components.length;
|
|
232
|
+
if (hidden > 0) {
|
|
233
|
+
lines.push(` ... ${hidden} more (use --limit to show more)`);
|
|
234
|
+
}
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Helpers ──
|
|
239
|
+
|
|
240
|
+
function formatValue(obj: unknown): string {
|
|
241
|
+
try {
|
|
242
|
+
return JSON.stringify(obj, replacer, 0) || 'undefined';
|
|
243
|
+
} catch {
|
|
244
|
+
return String(obj);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatCompactValue(val: unknown): string | undefined {
|
|
249
|
+
if (val === undefined) return undefined;
|
|
250
|
+
if (val === null) return 'null';
|
|
251
|
+
if (typeof val === 'function') return 'ƒ';
|
|
252
|
+
if (typeof val === 'string') return `"${val}"`;
|
|
253
|
+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
254
|
+
try {
|
|
255
|
+
const s = JSON.stringify(val, replacer, 0);
|
|
256
|
+
if (s && s.length > 60) return s.slice(0, 57) + '...';
|
|
257
|
+
return s || String(val);
|
|
258
|
+
} catch {
|
|
259
|
+
return String(val);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function replacer(_key: string, value: unknown): unknown {
|
|
264
|
+
if (typeof value === 'function') return 'ƒ';
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function pad(s: string, len: number): string {
|
|
269
|
+
return s.length >= len ? s : s + ' '.repeat(len - s.length);
|
|
270
|
+
}
|
package/src/profiler.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProfilingSession,
|
|
3
|
+
ProfilingCommit,
|
|
4
|
+
ChangeDescription,
|
|
5
|
+
ComponentRenderReport,
|
|
6
|
+
RenderCause,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
import type { ComponentTree } from './component-tree.js';
|
|
9
|
+
|
|
10
|
+
export interface ProfileSummary {
|
|
11
|
+
name: string;
|
|
12
|
+
duration: number;
|
|
13
|
+
commitCount: number;
|
|
14
|
+
componentRenderCounts: { id: number; displayName: string; count: number }[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TimelineEntry {
|
|
18
|
+
index: number;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
duration: number;
|
|
21
|
+
componentCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CommitDetail {
|
|
25
|
+
index: number;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
duration: number;
|
|
28
|
+
components: Array<{
|
|
29
|
+
id: number;
|
|
30
|
+
displayName: string;
|
|
31
|
+
actualDuration: number;
|
|
32
|
+
selfDuration: number;
|
|
33
|
+
causes: RenderCause[];
|
|
34
|
+
}>;
|
|
35
|
+
totalComponents: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Profiler {
|
|
39
|
+
private session: ProfilingSession | null = null;
|
|
40
|
+
/** Display names captured during profiling (survives unmounts) */
|
|
41
|
+
private displayNames = new Map<number, string>();
|
|
42
|
+
|
|
43
|
+
isActive(): boolean {
|
|
44
|
+
return this.session !== null && this.session.stoppedAt === null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
start(name?: string): void {
|
|
48
|
+
this.displayNames.clear();
|
|
49
|
+
this.session = {
|
|
50
|
+
name: name || `session-${Date.now()}`,
|
|
51
|
+
startedAt: Date.now(),
|
|
52
|
+
stoppedAt: null,
|
|
53
|
+
commits: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Cache a component's display name (call during profiling to survive unmounts) */
|
|
58
|
+
trackComponent(id: number, displayName: string): void {
|
|
59
|
+
this.displayNames.set(id, displayName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
stop(tree?: ComponentTree): ProfileSummary | null {
|
|
63
|
+
if (!this.session) return null;
|
|
64
|
+
this.session.stoppedAt = Date.now();
|
|
65
|
+
|
|
66
|
+
const duration = this.session.stoppedAt - this.session.startedAt;
|
|
67
|
+
|
|
68
|
+
// Count renders per component
|
|
69
|
+
const renderCounts = new Map<number, number>();
|
|
70
|
+
for (const commit of this.session.commits) {
|
|
71
|
+
for (const [id] of commit.fiberActualDurations) {
|
|
72
|
+
renderCounts.set(id, (renderCounts.get(id) || 0) + 1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const componentRenderCounts = Array.from(renderCounts.entries())
|
|
77
|
+
.map(([id, count]) => ({
|
|
78
|
+
id,
|
|
79
|
+
displayName: tree?.getNode(id)?.displayName || this.displayNames.get(id) || '',
|
|
80
|
+
count,
|
|
81
|
+
}))
|
|
82
|
+
.sort((a, b) => b.count - a.count);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
name: this.session.name,
|
|
86
|
+
duration,
|
|
87
|
+
commitCount: this.session.commits.length,
|
|
88
|
+
componentRenderCounts,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Process profiling data sent from React DevTools.
|
|
94
|
+
*
|
|
95
|
+
* The data format varies between React versions. We handle the common
|
|
96
|
+
* format where each commit contains:
|
|
97
|
+
* - commitTime
|
|
98
|
+
* - duration
|
|
99
|
+
* - fiberActualDurations: [id, duration, ...]
|
|
100
|
+
* - fiberSelfDurations: [id, duration, ...]
|
|
101
|
+
* - changeDescriptions: Map<id, description>
|
|
102
|
+
*/
|
|
103
|
+
processProfilingData(payload: unknown): void {
|
|
104
|
+
if (!this.session || this.session.stoppedAt !== null) return;
|
|
105
|
+
|
|
106
|
+
const data = payload as {
|
|
107
|
+
dataForRoots?: Array<{
|
|
108
|
+
commitData?: Array<{
|
|
109
|
+
changeDescriptions?: Array<[number, unknown]> | Map<number, unknown>;
|
|
110
|
+
duration?: number;
|
|
111
|
+
fiberActualDurations?: Array<[number, number]> | number[];
|
|
112
|
+
fiberSelfDurations?: Array<[number, number]> | number[];
|
|
113
|
+
timestamp?: number;
|
|
114
|
+
}>;
|
|
115
|
+
operations?: unknown[];
|
|
116
|
+
}>;
|
|
117
|
+
// Alternative flat format
|
|
118
|
+
commitData?: Array<{
|
|
119
|
+
changeDescriptions?: Array<[number, unknown]> | Map<number, unknown>;
|
|
120
|
+
duration?: number;
|
|
121
|
+
fiberActualDurations?: Array<[number, number]> | number[];
|
|
122
|
+
fiberSelfDurations?: Array<[number, number]> | number[];
|
|
123
|
+
timestamp?: number;
|
|
124
|
+
}>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Handle nested format (dataForRoots)
|
|
128
|
+
const roots = data?.dataForRoots;
|
|
129
|
+
if (roots) {
|
|
130
|
+
for (const root of roots) {
|
|
131
|
+
if (root.commitData) {
|
|
132
|
+
for (const commitData of root.commitData) {
|
|
133
|
+
this.processCommitData(commitData);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle flat format
|
|
141
|
+
if (data?.commitData) {
|
|
142
|
+
for (const commitData of data.commitData) {
|
|
143
|
+
this.processCommitData(commitData);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private processCommitData(commitData: {
|
|
149
|
+
changeDescriptions?: Array<[number, unknown]> | Map<number, unknown>;
|
|
150
|
+
duration?: number;
|
|
151
|
+
fiberActualDurations?: Array<[number, number]> | number[];
|
|
152
|
+
fiberSelfDurations?: Array<[number, number]> | number[];
|
|
153
|
+
timestamp?: number;
|
|
154
|
+
}): void {
|
|
155
|
+
const commit: ProfilingCommit = {
|
|
156
|
+
timestamp: commitData.timestamp || Date.now(),
|
|
157
|
+
duration: commitData.duration || 0,
|
|
158
|
+
fiberActualDurations: new Map(),
|
|
159
|
+
fiberSelfDurations: new Map(),
|
|
160
|
+
changeDescriptions: new Map(),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Parse fiber durations (can be [id, duration, id, duration, ...] or [[id, duration], ...])
|
|
164
|
+
if (commitData.fiberActualDurations) {
|
|
165
|
+
parseDurations(commitData.fiberActualDurations, commit.fiberActualDurations);
|
|
166
|
+
}
|
|
167
|
+
if (commitData.fiberSelfDurations) {
|
|
168
|
+
parseDurations(commitData.fiberSelfDurations, commit.fiberSelfDurations);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse change descriptions
|
|
172
|
+
if (commitData.changeDescriptions) {
|
|
173
|
+
const entries =
|
|
174
|
+
commitData.changeDescriptions instanceof Map
|
|
175
|
+
? commitData.changeDescriptions.entries()
|
|
176
|
+
: commitData.changeDescriptions[Symbol.iterator]();
|
|
177
|
+
for (const [id, desc] of entries) {
|
|
178
|
+
const d = desc as {
|
|
179
|
+
didHooksChange?: boolean;
|
|
180
|
+
isFirstMount?: boolean;
|
|
181
|
+
props?: string[] | null;
|
|
182
|
+
state?: string[] | null;
|
|
183
|
+
hooks?: number[] | null;
|
|
184
|
+
};
|
|
185
|
+
commit.changeDescriptions.set(id as number, {
|
|
186
|
+
didHooksChange: d.didHooksChange || false,
|
|
187
|
+
isFirstMount: d.isFirstMount || false,
|
|
188
|
+
props: d.props || null,
|
|
189
|
+
state: d.state || null,
|
|
190
|
+
hooks: d.hooks || null,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.session!.commits.push(commit);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getReport(
|
|
199
|
+
componentId: number,
|
|
200
|
+
tree: ComponentTree,
|
|
201
|
+
): ComponentRenderReport | null {
|
|
202
|
+
if (!this.session) return null;
|
|
203
|
+
|
|
204
|
+
const node = tree.getNode(componentId);
|
|
205
|
+
let renderCount = 0;
|
|
206
|
+
let totalDuration = 0;
|
|
207
|
+
let maxDuration = 0;
|
|
208
|
+
const causeSet = new Set<RenderCause>();
|
|
209
|
+
|
|
210
|
+
for (const commit of this.session.commits) {
|
|
211
|
+
const duration = commit.fiberActualDurations.get(componentId);
|
|
212
|
+
if (duration !== undefined) {
|
|
213
|
+
renderCount++;
|
|
214
|
+
totalDuration += duration;
|
|
215
|
+
if (duration > maxDuration) maxDuration = duration;
|
|
216
|
+
|
|
217
|
+
const desc = commit.changeDescriptions.get(componentId);
|
|
218
|
+
if (desc) {
|
|
219
|
+
for (const cause of describeCauses(desc)) {
|
|
220
|
+
causeSet.add(cause);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (renderCount === 0) return null;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
id: componentId,
|
|
230
|
+
displayName: node?.displayName || this.displayNames.get(componentId) || `Component#${componentId}`,
|
|
231
|
+
renderCount,
|
|
232
|
+
totalDuration,
|
|
233
|
+
avgDuration: totalDuration / renderCount,
|
|
234
|
+
maxDuration,
|
|
235
|
+
causes: Array.from(causeSet),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getSlowest(
|
|
240
|
+
tree: ComponentTree,
|
|
241
|
+
limit = 10,
|
|
242
|
+
): ComponentRenderReport[] {
|
|
243
|
+
return this.getAllReports(tree)
|
|
244
|
+
.sort((a, b) => b.avgDuration - a.avgDuration)
|
|
245
|
+
.slice(0, limit);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
getMostRerenders(
|
|
249
|
+
tree: ComponentTree,
|
|
250
|
+
limit = 10,
|
|
251
|
+
): ComponentRenderReport[] {
|
|
252
|
+
return this.getAllReports(tree)
|
|
253
|
+
.sort((a, b) => b.renderCount - a.renderCount)
|
|
254
|
+
.slice(0, limit);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getCommitDetails(index: number, tree: ComponentTree, limit = 10): CommitDetail | null {
|
|
258
|
+
if (!this.session) return null;
|
|
259
|
+
if (index < 0 || index >= this.session.commits.length) return null;
|
|
260
|
+
|
|
261
|
+
const commit = this.session.commits[index];
|
|
262
|
+
const components: CommitDetail['components'] = [];
|
|
263
|
+
|
|
264
|
+
for (const [id, actualDuration] of commit.fiberActualDurations) {
|
|
265
|
+
const selfDuration = commit.fiberSelfDurations.get(id) || 0;
|
|
266
|
+
const desc = commit.changeDescriptions.get(id);
|
|
267
|
+
components.push({
|
|
268
|
+
id,
|
|
269
|
+
displayName: tree.getNode(id)?.displayName || this.displayNames.get(id) || `Component#${id}`,
|
|
270
|
+
actualDuration,
|
|
271
|
+
selfDuration,
|
|
272
|
+
causes: desc ? describeCauses(desc) : [],
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
components.sort((a, b) => b.selfDuration - a.selfDuration);
|
|
277
|
+
|
|
278
|
+
const totalCount = components.length;
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
index,
|
|
282
|
+
timestamp: commit.timestamp,
|
|
283
|
+
duration: commit.duration,
|
|
284
|
+
components: limit > 0 ? components.slice(0, limit) : components,
|
|
285
|
+
totalComponents: totalCount,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getTimeline(limit?: number): TimelineEntry[] {
|
|
290
|
+
if (!this.session) return [];
|
|
291
|
+
|
|
292
|
+
const entries = this.session.commits.map((commit, index) => ({
|
|
293
|
+
index,
|
|
294
|
+
timestamp: commit.timestamp,
|
|
295
|
+
duration: commit.duration,
|
|
296
|
+
componentCount: commit.fiberActualDurations.size,
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
if (limit) return entries.slice(0, limit);
|
|
300
|
+
return entries;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getAllReports(tree: ComponentTree): ComponentRenderReport[] {
|
|
304
|
+
if (!this.session) return [];
|
|
305
|
+
|
|
306
|
+
// Collect all component IDs that appear in profiling data
|
|
307
|
+
const componentIds = new Set<number>();
|
|
308
|
+
for (const commit of this.session.commits) {
|
|
309
|
+
for (const id of commit.fiberActualDurations.keys()) {
|
|
310
|
+
componentIds.add(id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const reports: ComponentRenderReport[] = [];
|
|
315
|
+
for (const id of componentIds) {
|
|
316
|
+
const report = this.getReport(id, tree);
|
|
317
|
+
if (report) reports.push(report);
|
|
318
|
+
}
|
|
319
|
+
return reports;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseDurations(
|
|
324
|
+
raw: Array<[number, number]> | number[],
|
|
325
|
+
target: Map<number, number>,
|
|
326
|
+
): void {
|
|
327
|
+
if (raw.length === 0) return;
|
|
328
|
+
|
|
329
|
+
// Check if it's array of tuples or flat array
|
|
330
|
+
if (Array.isArray(raw[0])) {
|
|
331
|
+
// [[id, duration], ...]
|
|
332
|
+
for (const [id, duration] of raw as Array<[number, number]>) {
|
|
333
|
+
target.set(id, duration);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// [id, duration, id, duration, ...]
|
|
337
|
+
const flat = raw as number[];
|
|
338
|
+
for (let i = 0; i < flat.length; i += 2) {
|
|
339
|
+
target.set(flat[i], flat[i + 1]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function describeCauses(desc: ChangeDescription): RenderCause[] {
|
|
345
|
+
const causes: RenderCause[] = [];
|
|
346
|
+
if (desc.isFirstMount) {
|
|
347
|
+
causes.push('first-mount');
|
|
348
|
+
return causes;
|
|
349
|
+
}
|
|
350
|
+
if (desc.props && desc.props.length > 0) causes.push('props-changed');
|
|
351
|
+
if (desc.state && desc.state.length > 0) causes.push('state-changed');
|
|
352
|
+
if (desc.didHooksChange) causes.push('hooks-changed');
|
|
353
|
+
// If no specific cause found, it was likely parent-triggered
|
|
354
|
+
if (causes.length === 0) causes.push('parent-rendered');
|
|
355
|
+
return causes;
|
|
356
|
+
}
|