Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/cli/src/ui/commands/perfCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
52 changes: 52 additions & 0 deletions packages/cli/src/ui/commands/perfCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { MessageType } from '../types.js';
import type {
HistoryItemPerfLatency,
HistoryItemPerfMemory,
HistoryItemPerfStartup,
} from '../types.js';
import { type CommandContext } from './types.js';
import {
getLocalMetricsSnapshot,
localMetricReader,
} from '@google/gemini-cli-core';

// Export the bare function, parsing the remaining args to determine the view
export async function handlePerfCommand(
context: CommandContext,
args: string[] = [],
) {
// 1. Force OTel to process the pending queue
await localMetricReader.forceFlush();

// 2. Await the Promise to grab the actual data payload
const data = await getLocalMetricsSnapshot();

// 3. Figure out which view the user asked for
const targetView =
args.find((arg) => ['latency', 'memory', 'startup'].includes(arg)) ||
'latency';

// 4. Dispatch to the correct Ink UI component with explicit type casting
if (targetView === 'memory') {
context.ui.addItem({
type: MessageType.PERF_MEMORY,
data,
} as HistoryItemPerfMemory);
} else if (targetView === 'startup') {
context.ui.addItem({
type: MessageType.PERF_STARTUP,
data,
} as HistoryItemPerfStartup);
} else {
context.ui.addItem({
type: MessageType.PERF_LATENCY,
data,
} as HistoryItemPerfLatency);
}
}
43 changes: 43 additions & 0 deletions packages/cli/src/ui/commands/statsCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
type SlashCommand,
CommandKind,
} from './types.js';
import { handlePerfCommand } from './perfCommand.js';

function getUserIdentity(context: CommandContext) {
const selectedAuthType =
Expand Down Expand Up @@ -131,5 +132,47 @@ export const statsCommand: SlashCommand = {
} as HistoryItemToolStats);
},
},
{
name: 'perf',
description: 'Show advanced performance metrics and telemetry',
kind: CommandKind.BUILT_IN,
autoExecute: true,
// 1. Change args to be a single string to satisfy the SlashCommand interface
action: async (context: CommandContext, args: string = '') => {
// 2. Safely split the string into an array for your decoupled handler
const argsArray = args ? args.trim().split(/\s+/) : [];
await handlePerfCommand(context, argsArray);
},
subCommands: [
{
name: 'latency',
description: 'Show latency P50/P90/P99 percentiles',
kind: CommandKind.BUILT_IN,
autoExecute: true,
// We can just omit 'args' from the parameters here since we hardcode the array
action: async (context: CommandContext) => {
await handlePerfCommand(context, ['latency']);
},
},
{
name: 'memory',
description: 'Show V8 heap utilization',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
await handlePerfCommand(context, ['memory']);
},
},
{
name: 'startup',
description: 'Show startup phase analysis',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
await handlePerfCommand(context, ['startup']);
},
},
],
},
],
};
22 changes: 22 additions & 0 deletions packages/cli/src/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type SkillDefinition,
type AgentDefinition,
type ApprovalMode,
type PerfSnapshot,
CoreToolCallStatus,
checkExhaustive,
} from '@google/gemini-cli-core';
Expand Down Expand Up @@ -221,6 +222,21 @@ export type HistoryItemModel = HistoryItemBase & {
model: string;
};

export type HistoryItemPerfLatency = HistoryItemBase & {
type: MessageType.PERF_LATENCY;
data: PerfSnapshot;
};

export type HistoryItemPerfMemory = HistoryItemBase & {
type: MessageType.PERF_MEMORY;
data: PerfSnapshot;
};

export type HistoryItemPerfStartup = HistoryItemBase & {
type: MessageType.PERF_STARTUP;
data: PerfSnapshot;
};

export type HistoryItemQuit = HistoryItemBase & {
type: 'quit';
duration: string;
Expand Down Expand Up @@ -368,6 +384,9 @@ export type HistoryItemWithoutId =
| HistoryItemStats
| HistoryItemModelStats
| HistoryItemToolStats
| HistoryItemPerfLatency
| HistoryItemPerfMemory
| HistoryItemPerfStartup
| HistoryItemModel
| HistoryItemQuit
| HistoryItemCompression
Expand All @@ -393,6 +412,9 @@ export enum MessageType {
STATS = 'stats',
MODEL_STATS = 'model_stats',
TOOL_STATS = 'tool_stats',
PERF_LATENCY = 'perf_latency',
PERF_MEMORY = 'perf_memory',
PERF_STARTUP = 'perf_startup',
QUIT = 'quit',
GEMINI = 'gemini',
COMPRESSION = 'compression',
Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,10 +1025,8 @@ export class Config implements McpContext {
setGeminiMdFilename(params.contextFileName);
}

if (this.telemetrySettings.enabled) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
initializeTelemetry(this);
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
initializeTelemetry(this);

const proxy = this.getProxy();
if (proxy) {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ export { sessionId, createSessionId } from './utils/session.js';
export * from './utils/compatibility.js';
export * from './utils/browser.js';
export { Storage } from './config/storage.js';
export {
getLocalMetricsSnapshot,
type PerfSnapshot,
} from './telemetry/localBuffer.js';

// Export hooks system
export * from './hooks/index.js';
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,8 @@ export {
export { runInDevTraceSpan, type SpanMetadata } from './trace.js';
export { startupProfiler, StartupProfiler } from './startupProfiler.js';
export * from './constants.js';
export {
getLocalMetricsSnapshot,
localMetricReader,
type PerfSnapshot,
} from './localBuffer.js';
67 changes: 67 additions & 0 deletions packages/core/src/telemetry/localBuffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MeterProvider } from '@opentelemetry/sdk-metrics';
import {
setupLocalMetricsReader,
teardownLocalMetrics,
getLocalMetricsSnapshot,
} from './localBuffer.js';

describe('Telemetry Local Buffer', () => {
let meterProvider: MeterProvider;

beforeEach(() => {
// 1. Initialize our fresh local reader using the new factory function
const reader = setupLocalMetricsReader();

// 2. Create a fresh OTel engine just for this test,
// passing our custom local bridge directly into the constructor.
meterProvider = new MeterProvider({
readers: [reader],
});
});

afterEach(async () => {
// Clean up the engine and the singleton after the test finishes
await meterProvider.shutdown();
teardownLocalMetrics();
});

it('should correctly capture and simplify OTel counters and histograms with attributes', async () => {
const meter = meterProvider.getMeter('test-meter');

// 1. Simulate an API Request Counter WITH an attribute
const counter = meter.createCounter('gemini_cli.api.request.count');
counter.add(10, { model: 'gemini-3.5' });
counter.add(5, { model: 'gemini-3.5' }); // Total should be 15 for this model

// 2. Simulate a Tool Execution Latency Histogram WITH an attribute
const histogram = meter.createHistogram('gemini_cli.tool.call.latency');
histogram.record(100, { function_name: 'search' });
histogram.record(200, { function_name: 'search' });
histogram.record(300, { function_name: 'search' }); // Count: 3, Sum: 600

// 3. Pull the snapshot through our bridge utility
const snapshot = await getLocalMetricsSnapshot();

// 4. Prove the counter was flattened correctly into an array
const requestStats = snapshot.counters['gemini_cli.api.request.count'];
expect(requestStats).toBeDefined();
expect(requestStats[0].value).toBe(15);
expect(requestStats[0].attributes).toEqual({ model: 'gemini-3.5' }); // Proves we kept granularity!

// 5. Prove the histogram was flattened correctly into an array
const latencyStats = snapshot.histograms['gemini_cli.tool.call.latency'];
expect(latencyStats).toBeDefined();
expect(latencyStats[0].count).toBe(3);
expect(latencyStats[0].sum).toBe(600);
expect(latencyStats[0].min).toBe(100);
expect(latencyStats[0].max).toBe(300);
expect(latencyStats[0].attributes).toEqual({ function_name: 'search' }); // Proves we kept granularity!
});
});
Loading