Skip to content

Commit 6a07277

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

6 files changed

Lines changed: 100 additions & 5 deletions

File tree

.changeset/profiler.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
Start and stop profiling sessions to capture render performance data from connected React apps.
88

99
- **Render reports** — Per-component render duration and count
10-
- **Slowest components** — Ranked by render time
10+
- **Slowest components** — Ranked by self render time
1111
- **Most re-rendered** — Ranked by render count
1212
- **Commit timeline** — Chronological view of React commits with durations
13+
- **Commit details** — Per-component breakdown for a specific commit, sorted by self time
1314

14-
CLI commands: `profile start`, `profile stop`, `profile report`, `profile slow`, `profile rerenders`, `profile timeline`.
15+
CLI commands: `profile start`, `profile stop`, `profile report`, `profile slow`, `profile rerenders`, `profile timeline`, `profile commit`.

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

Lines changed: 25 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> [--limit N] Detail for specific commit`;
4345
}
4446

4547
function parseArgs(argv: string[]): {
@@ -265,6 +267,28 @@ 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 limit = flags['limit'] ? parseInt(flags['limit'] as string, 10) : undefined;
282+
const resp = await sendCommand({ type: 'profile-commit', index, limit });
283+
if (resp.ok) {
284+
console.log(formatCommitDetail(resp.data as any));
285+
} else {
286+
console.error(resp.error);
287+
process.exit(1);
288+
}
289+
return;
290+
}
291+
268292
if (cmd0 === 'profile' && cmd1 === 'timeline') {
269293
const limit = flags['limit'] ? parseInt(flags['limit'] as string, 10) : undefined;
270294
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, cmd.limit);
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: 16 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,21 @@ 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.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+
223238
// ── Helpers ──
224239

225240
function formatValue(obj: unknown): string {

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ 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+
totalComponents: number;
36+
}
37+
2438
export class Profiler {
2539
private session: ProfilingSession | null = null;
2640
/** Display names captured during profiling (survives unmounts) */
@@ -240,6 +254,38 @@ export class Profiler {
240254
.slice(0, limit);
241255
}
242256

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+
243289
getTimeline(limit?: number): TimelineEntry[] {
244290
if (!this.session) return [];
245291

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; limit?: number };
100101

101102
export interface IpcResponse {
102103
ok: boolean;

0 commit comments

Comments
 (0)