Skip to content

Commit d8613ed

Browse files
committed
feat: add profile commit command for per-commit details
1 parent f27273c commit d8613ed

5 files changed

Lines changed: 88 additions & 3 deletions

File tree

packages/agent-react-devtools/src/cli.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
formatSlowest,
1717
formatRerenders,
1818
formatTimeline,
19+
formatCommitDetail,
1920
} from './formatters.js';
2021
import type { IpcCommand } from './types.js';
2122

@@ -39,7 +40,8 @@ Profiling:
3940
profile report <@c1 | id> Render report for component
4041
profile slow [--limit N] Slowest components (by avg)
4142
profile rerenders [--limit N] Most re-rendered components
42-
profile timeline [--limit N] Commit timeline`;
43+
profile timeline [--limit N] Commit timeline
44+
profile commit <N | #N> Detail for specific commit`;
4345
}
4446

4547
function parseArgs(argv: string[]): {
@@ -265,6 +267,27 @@ async function main(): Promise<void> {
265267
return;
266268
}
267269

270+
if (cmd0 === 'profile' && cmd1 === 'commit') {
271+
const raw = command[2];
272+
if (!raw) {
273+
console.error('Usage: devtools profile commit <N | #N>');
274+
process.exit(1);
275+
}
276+
const index = parseInt(raw.replace(/^#/, ''), 10);
277+
if (isNaN(index)) {
278+
console.error('Usage: devtools profile commit <N | #N>');
279+
process.exit(1);
280+
}
281+
const resp = await sendCommand({ type: 'profile-commit', index });
282+
if (resp.ok) {
283+
console.log(formatCommitDetail(resp.data as any));
284+
} else {
285+
console.error(resp.error);
286+
process.exit(1);
287+
}
288+
return;
289+
}
290+
268291
if (cmd0 === 'profile' && cmd1 === 'timeline') {
269292
const limit = flags['limit'] ? parseInt(flags['limit'] as string, 10) : undefined;
270293
const resp = await sendCommand({ type: 'profile-timeline', limit });

packages/agent-react-devtools/src/daemon.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ class Daemon {
218218
data: this.profiler.getTimeline(cmd.limit),
219219
};
220220

221+
case 'profile-commit': {
222+
const detail = this.profiler.getCommitDetails(cmd.index, this.tree);
223+
if (!detail) {
224+
return { ok: false, error: `Commit #${cmd.index} not found` };
225+
}
226+
return { ok: true, data: detail };
227+
}
228+
221229
default:
222230
return { ok: false, error: `Unknown command: ${(cmd as any).type}` };
223231
}

packages/agent-react-devtools/src/formatters.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
ComponentRenderReport,
55
} from './types.js';
66
import type { TreeNode } from './component-tree.js';
7-
import type { ProfileSummary, TimelineEntry } from './profiler.js';
7+
import type { ProfileSummary, TimelineEntry, CommitDetail } from './profiler.js';
88

99
// ── Abbreviations for component types ──
1010
const TYPE_ABBREV: Record<string, string> = {
@@ -220,6 +220,17 @@ export function formatTimeline(entries: TimelineEntry[]): string {
220220
return lines.join('\n');
221221
}
222222

223+
export function formatCommitDetail(detail: CommitDetail): string {
224+
const lines: string[] = [];
225+
lines.push(`Commit #${detail.index} ${detail.duration.toFixed(1)}ms ${detail.components.length} 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+
return lines.join('\n');
232+
}
233+
223234
// ── Helpers ──
224235

225236
function formatValue(obj: unknown): string {

packages/agent-react-devtools/src/profiler.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ export interface TimelineEntry {
2121
componentCount: number;
2222
}
2323

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+
}
36+
2437
export class Profiler {
2538
private session: ProfilingSession | null = null;
2639
/** Display names captured during profiling (survives unmounts) */
@@ -240,6 +253,35 @@ export class Profiler {
240253
.slice(0, limit);
241254
}
242255

256+
getCommitDetails(index: number, tree: ComponentTree): CommitDetail | null {
257+
if (!this.session) return null;
258+
if (index < 0 || index >= this.session.commits.length) return null;
259+
260+
const commit = this.session.commits[index];
261+
const components: CommitDetail['components'] = [];
262+
263+
for (const [id, actualDuration] of commit.fiberActualDurations) {
264+
const selfDuration = commit.fiberSelfDurations.get(id) || 0;
265+
const desc = commit.changeDescriptions.get(id);
266+
components.push({
267+
id,
268+
displayName: tree.getNode(id)?.displayName || this.displayNames.get(id) || `Component#${id}`,
269+
actualDuration,
270+
selfDuration,
271+
causes: desc ? describeCauses(desc) : [],
272+
});
273+
}
274+
275+
components.sort((a, b) => b.selfDuration - a.selfDuration);
276+
277+
return {
278+
index,
279+
timestamp: commit.timestamp,
280+
duration: commit.duration,
281+
components,
282+
};
283+
}
284+
243285
getTimeline(limit?: number): TimelineEntry[] {
244286
if (!this.session) return [];
245287

packages/agent-react-devtools/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ export type IpcCommand =
9696
| { type: 'profile-report'; componentId: number | string }
9797
| { type: 'profile-slow'; limit?: number }
9898
| { type: 'profile-rerenders'; limit?: number }
99-
| { type: 'profile-timeline'; limit?: number };
99+
| { type: 'profile-timeline'; limit?: number }
100+
| { type: 'profile-commit'; index: number };
100101

101102
export interface IpcResponse {
102103
ok: boolean;

0 commit comments

Comments
 (0)