Skip to content

Commit fb0a48d

Browse files
committed
feat(cli): add /perf performance monitoring dashboard (rebased)
Implemented a standalone /perf command with dedicated subcommands: - /perf overview: session stats and memory summary - /perf memory: detailed heap/RSS utilization - /perf tools: execution timing and frequency - /perf api: model latency and token breakdown - /perf export: session metrics export to JSON Includes new Sink-based UI components and type definitions. Relates to #21142 Relates to #22403
1 parent 0bf7ea6 commit fb0a48d

File tree

5 files changed

+744
-1
lines changed

5 files changed

+744
-1
lines changed

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { quitCommand } from '../ui/commands/quitCommand.js';
5252
import { restoreCommand } from '../ui/commands/restoreCommand.js';
5353
import { resumeCommand } from '../ui/commands/resumeCommand.js';
5454
import { statsCommand } from '../ui/commands/statsCommand.js';
55+
import { perfCommand } from '../ui/commands/perfCommand.js';
5556
import { themeCommand } from '../ui/commands/themeCommand.js';
5657
import { toolsCommand } from '../ui/commands/toolsCommand.js';
5758
import { skillsCommand } from '../ui/commands/skillsCommand.js';
@@ -195,6 +196,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
195196
subCommands: addDebugToChatResumeSubCommands(resumeCommand.subCommands),
196197
},
197198
statsCommand,
199+
perfCommand,
198200
themeCommand,
199201
toolsCommand,
200202
...(this.config?.isSkillsSupportEnabled()
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { MessageType } from '../types.js';
8+
import type { HistoryItemPerfDashboard } from '../types.js';
9+
import { formatDuration } from '../utils/formatters.js';
10+
import {
11+
type CommandContext,
12+
type SlashCommand,
13+
CommandKind,
14+
} from './types.js';
15+
16+
/**
17+
* Collects a performance snapshot from the current session context.
18+
* This aggregates metrics from the session's models and tools into
19+
* a single PerfSnapshot object for the dashboard to display.
20+
*/
21+
function collectPerfSnapshot(context: CommandContext) {
22+
const { stats } = context.session;
23+
const { metrics } = stats;
24+
const now = new Date();
25+
const uptimeMs = now.getTime() - stats.sessionStartTime.getTime();
26+
27+
// Aggregate tool performance by name
28+
const toolPerf: Array<{
29+
name: string;
30+
calls: number;
31+
avgMs: number;
32+
totalMs: number;
33+
successRate: number;
34+
}> = [];
35+
36+
for (const [name, toolStats] of Object.entries(metrics.tools.byName)) {
37+
const avgMs = toolStats.count > 0 ? toolStats.durationMs / toolStats.count : 0;
38+
const successRate = toolStats.count > 0 ? (toolStats.success / toolStats.count) * 100 : 0;
39+
toolPerf.push({
40+
name,
41+
calls: toolStats.count,
42+
avgMs: Math.round(avgMs),
43+
totalMs: Math.round(toolStats.durationMs),
44+
successRate,
45+
});
46+
}
47+
48+
// Sort tools by total time descending
49+
toolPerf.sort((a, b) => b.totalMs - a.totalMs);
50+
51+
// Aggregate API performance by model
52+
const apiPerf: Array<{
53+
model: string;
54+
requests: number;
55+
avgLatencyMs: number;
56+
totalLatencyMs: number;
57+
errorRate: number;
58+
inputTokens: number;
59+
outputTokens: number;
60+
cachedTokens: number;
61+
}> = [];
62+
63+
for (const [model, modelMetrics] of Object.entries(metrics.models)) {
64+
const avgLatency = modelMetrics.api.totalRequests > 0
65+
? modelMetrics.api.totalLatencyMs / modelMetrics.api.totalRequests
66+
: 0;
67+
const errorRate = modelMetrics.api.totalRequests > 0
68+
? (modelMetrics.api.totalErrors / modelMetrics.api.totalRequests) * 100
69+
: 0;
70+
apiPerf.push({
71+
model: model.replace('-001', ''),
72+
requests: modelMetrics.api.totalRequests,
73+
avgLatencyMs: Math.round(avgLatency),
74+
totalLatencyMs: Math.round(modelMetrics.api.totalLatencyMs),
75+
errorRate,
76+
inputTokens: modelMetrics.tokens.input,
77+
outputTokens: modelMetrics.tokens.candidates,
78+
cachedTokens: modelMetrics.tokens.cached,
79+
});
80+
}
81+
82+
// Memory snapshot
83+
const memUsage = process.memoryUsage();
84+
const memory = {
85+
heapUsedMB: Math.round((memUsage.heapUsed / 1024 / 1024) * 10) / 10,
86+
heapTotalMB: Math.round((memUsage.heapTotal / 1024 / 1024) * 10) / 10,
87+
rssMB: Math.round((memUsage.rss / 1024 / 1024) * 10) / 10,
88+
externalMB: Math.round((memUsage.external / 1024 / 1024) * 10) / 10,
89+
};
90+
91+
// Memory warnings
92+
const memoryWarnings: string[] = [];
93+
const heapPercent = memUsage.heapUsed / memUsage.heapTotal;
94+
if (heapPercent > 0.9) {
95+
memoryWarnings.push('Critical: Heap usage above 90%!');
96+
} else if (heapPercent > 0.75) {
97+
memoryWarnings.push('Warning: Heap usage above 75%');
98+
}
99+
if (memUsage.rss > 512 * 1024 * 1024) {
100+
memoryWarnings.push('Warning: RSS memory exceeds 512MB');
101+
}
102+
103+
// Total token counts
104+
const totalInputTokens = Object.values(metrics.models).reduce(
105+
(acc, m) => acc + m.tokens.input, 0,
106+
);
107+
const totalOutputTokens = Object.values(metrics.models).reduce(
108+
(acc, m) => acc + m.tokens.candidates, 0,
109+
);
110+
const totalCachedTokens = Object.values(metrics.models).reduce(
111+
(acc, m) => acc + m.tokens.cached, 0,
112+
);
113+
114+
// Total API time and tool time
115+
const totalApiTime = Object.values(metrics.models).reduce(
116+
(acc, m) => acc + m.api.totalLatencyMs, 0,
117+
);
118+
const totalToolTime = metrics.tools.totalDurationMs;
119+
120+
return {
121+
uptimeMs,
122+
duration: formatDuration(uptimeMs),
123+
memory,
124+
memoryWarnings,
125+
toolPerf,
126+
apiPerf,
127+
totalToolCalls: metrics.tools.totalCalls,
128+
totalToolTime,
129+
totalApiRequests: Object.values(metrics.models).reduce(
130+
(acc, m) => acc + m.api.totalRequests, 0,
131+
),
132+
totalApiTime,
133+
totalInputTokens,
134+
totalOutputTokens,
135+
totalCachedTokens,
136+
totalLinesAdded: metrics.files.totalLinesAdded,
137+
totalLinesRemoved: metrics.files.totalLinesRemoved,
138+
};
139+
}
140+
141+
export const perfCommand: SlashCommand = {
142+
name: 'perf',
143+
altNames: ['performance'],
144+
description: 'Performance monitoring dashboard. Usage: /perf [overview|memory|tools|api]',
145+
kind: CommandKind.BUILT_IN,
146+
autoExecute: false,
147+
isSafeConcurrent: true,
148+
action: async (context: CommandContext) => {
149+
const snapshot = collectPerfSnapshot(context);
150+
context.ui.addItem({
151+
type: MessageType.PERF_DASHBOARD,
152+
view: 'overview',
153+
snapshot,
154+
} as HistoryItemPerfDashboard);
155+
},
156+
subCommands: [
157+
{
158+
name: 'overview',
159+
description: 'Show performance overview dashboard',
160+
kind: CommandKind.BUILT_IN,
161+
autoExecute: true,
162+
isSafeConcurrent: true,
163+
action: (context: CommandContext) => {
164+
const snapshot = collectPerfSnapshot(context);
165+
context.ui.addItem({
166+
type: MessageType.PERF_DASHBOARD,
167+
view: 'overview',
168+
snapshot,
169+
} as HistoryItemPerfDashboard);
170+
},
171+
},
172+
{
173+
name: 'memory',
174+
description: 'Show detailed memory usage and warnings',
175+
kind: CommandKind.BUILT_IN,
176+
autoExecute: true,
177+
isSafeConcurrent: true,
178+
action: (context: CommandContext) => {
179+
const snapshot = collectPerfSnapshot(context);
180+
context.ui.addItem({
181+
type: MessageType.PERF_DASHBOARD,
182+
view: 'memory',
183+
snapshot,
184+
} as HistoryItemPerfDashboard);
185+
},
186+
},
187+
{
188+
name: 'tools',
189+
description: 'Show tool execution timing and frequency',
190+
kind: CommandKind.BUILT_IN,
191+
autoExecute: true,
192+
isSafeConcurrent: true,
193+
action: (context: CommandContext) => {
194+
const snapshot = collectPerfSnapshot(context);
195+
context.ui.addItem({
196+
type: MessageType.PERF_DASHBOARD,
197+
view: 'tools',
198+
snapshot,
199+
} as HistoryItemPerfDashboard);
200+
},
201+
},
202+
{
203+
name: 'api',
204+
description: 'Show model API latency breakdown',
205+
kind: CommandKind.BUILT_IN,
206+
autoExecute: true,
207+
isSafeConcurrent: true,
208+
action: (context: CommandContext) => {
209+
const snapshot = collectPerfSnapshot(context);
210+
context.ui.addItem({
211+
type: MessageType.PERF_DASHBOARD,
212+
view: 'api',
213+
snapshot,
214+
} as HistoryItemPerfDashboard);
215+
},
216+
},
217+
{
218+
name: 'export',
219+
description: 'Export performance report as JSON',
220+
kind: CommandKind.BUILT_IN,
221+
autoExecute: true,
222+
isSafeConcurrent: true,
223+
action: (context: CommandContext) => {
224+
const snapshot = collectPerfSnapshot(context);
225+
const report = JSON.stringify({
226+
timestamp: new Date().toISOString(),
227+
session_id: context.session.stats.sessionId,
228+
uptime_ms: snapshot.uptimeMs,
229+
memory: snapshot.memory,
230+
tools: {
231+
total_calls: snapshot.totalToolCalls,
232+
total_time_ms: snapshot.totalToolTime,
233+
by_tool: snapshot.toolPerf,
234+
},
235+
api: {
236+
total_requests: snapshot.totalApiRequests,
237+
total_time_ms: snapshot.totalApiTime,
238+
by_model: snapshot.apiPerf,
239+
},
240+
tokens: {
241+
input: snapshot.totalInputTokens,
242+
output: snapshot.totalOutputTokens,
243+
cached: snapshot.totalCachedTokens,
244+
},
245+
files: {
246+
lines_added: snapshot.totalLinesAdded,
247+
lines_removed: snapshot.totalLinesRemoved,
248+
},
249+
}, null, 2);
250+
251+
context.ui.addItem({
252+
type: MessageType.INFO,
253+
text: `📊 Performance Report (JSON):\n\n${report}`,
254+
});
255+
},
256+
},
257+
],
258+
};

packages/cli/src/ui/components/HistoryItemDisplay.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { AboutBox } from './AboutBox.js';
2222
import { StatsDisplay } from './StatsDisplay.js';
2323
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
2424
import { ToolStatsDisplay } from './ToolStatsDisplay.js';
25+
import { PerfDisplay } from './PerfDisplay.js';
2526
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
2627
import { Help } from './Help.js';
2728
import type { SlashCommand } from '../commands/types.js';
@@ -185,6 +186,12 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
185186
/>
186187
)}
187188
{itemForDisplay.type === 'tool_stats' && <ToolStatsDisplay />}
189+
{itemForDisplay.type === 'perf_dashboard' && (
190+
<PerfDisplay
191+
view={itemForDisplay.view}
192+
snapshot={itemForDisplay.snapshot}
193+
/>
194+
)}
188195
{itemForDisplay.type === 'model' && (
189196
<ModelMessage model={itemForDisplay.model} />
190197
)}

0 commit comments

Comments
 (0)