Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export async function runPrompt(
version,
uiMode: PROMPT_UI_MODE,
model: telemetryModel,
sessionId: session.id,
});
setCrashPhase('runtime');

Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/src/cli/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface InitializeCliTelemetryOptions {
readonly version: string;
readonly uiMode: string;
readonly model?: string;
readonly sessionId?: string;
}

export function createCliTelemetryBootstrap(): CliTelemetryBootstrap {
Expand All @@ -54,6 +55,7 @@ export function initializeCliTelemetry(options: InitializeCliTelemetryOptions):
version: options.version,
uiMode: options.uiMode,
model: options.model ?? options.config.defaultModel,
sessionId: options.sessionId,
getAccessToken: async () =>
(await options.harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ describe('kimi export', () => {
version: expect.any(String),
uiMode: 'shell',
model: 'k2',
sessionId: undefined,
getAccessToken: expect.any(Function),
});
expect(mocks.initializeTelemetry.mock.invocationCallOrder[0]).toBeLessThan(
Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-code/test/cli/run-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ describe('runPrompt', () => {
expect(mocks.session.prompt).toHaveBeenCalledWith('say hello');
expect(stdout.text()).toBe('• hello world\n\n');
expect(stderr.text()).toBe('To resume this session: kimi -r ses_prompt\n');
expect(mocks.initializeTelemetry).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: 'ses_prompt' }),
);
expect(mocks.shutdownTelemetry).toHaveBeenCalled();
expect(mocks.harnessClose).toHaveBeenCalled();
});
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/run-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ describe('runShell', () => {
version: '1.2.3-test',
uiMode: 'shell',
model: 'k2',
sessionId: undefined,
getAccessToken: expect.any(Function),
});
expect(mocks.setCrashPhase).toHaveBeenCalledWith('runtime');
Expand Down
6 changes: 6 additions & 0 deletions packages/telemetry/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getDefaultTelemetryClient } from './client';
import { EventSink } from './sink';
import { SystemMetricsCollector } from './systemMetrics';
import { AsyncTransport } from './transport';

export const TELEMETRY_DISABLE_ENV = 'KIMI_DISABLE_TELEMETRY';
Expand Down Expand Up @@ -65,5 +66,10 @@ export function initializeTelemetry(options: TelemetryBootstrapOptions): void {

client.attachSink(sink);
sink.startPeriodicFlush();

const systemMetricsCollector = new SystemMetricsCollector({ client });
client.setSystemMetricsCollector(systemMetricsCollector);
systemMetricsCollector.start();

void sink.retryDiskEvents().catch(() => {});
}
22 changes: 22 additions & 0 deletions packages/telemetry/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export interface TelemetryShutdownOptions {
readonly timeoutMs?: number;
}

export interface SystemMetricsCollectorHandle {
stop(): void;
}

const MAX_QUEUE_SIZE = 1000;

interface PendingTelemetryEvent extends TelemetryEvent {
Expand All @@ -25,6 +29,7 @@ interface PendingTelemetryEvent extends TelemetryEvent {
export class TelemetryClient {
private queue: PendingTelemetryEvent[] = [];
private sink: EventSink | null = null;
private systemMetricsCollector: SystemMetricsCollectorHandle | null = null;
private deviceId: string | null = null;
private sessionId: string | null = null;
private disabled = false;
Expand All @@ -38,6 +43,13 @@ export class TelemetryClient {
return new ScopedTelemetryClient(this, input);
}

setSystemMetricsCollector(collector: SystemMetricsCollectorHandle): void {
if (this.systemMetricsCollector !== null && this.systemMetricsCollector !== collector) {
this.systemMetricsCollector.stop();
}
this.systemMetricsCollector = collector;
}

attachSink(sink: EventSink): void {
if (this.sink !== null && this.sink !== sink) {
this.sink.stopPeriodicFlush();
Expand All @@ -60,6 +72,8 @@ export class TelemetryClient {
disable(): void {
this.disabled = true;
this.queue = [];
this.systemMetricsCollector?.stop();
this.systemMetricsCollector = null;
if (this.sink !== null) {
this.sink.stopPeriodicFlush();
this.sink.clearBuffer();
Expand Down Expand Up @@ -116,6 +130,8 @@ export class TelemetryClient {
}

async shutdown(options: TelemetryShutdownOptions = {}): Promise<void> {
this.systemMetricsCollector?.stop();
this.systemMetricsCollector = null;
const sink = this.sink;
if (sink === null) return;
sink.stopPeriodicFlush();
Expand All @@ -139,6 +155,8 @@ export class TelemetryClient {

resetForTests(): void {
this.sink?.stopPeriodicFlush();
this.systemMetricsCollector?.stop();
this.systemMetricsCollector = null;
this.queue = [];
this.sink = null;
this.deviceId = null;
Expand All @@ -163,6 +181,10 @@ class ScopedTelemetryClient extends TelemetryClient {
return new ScopedTelemetryClient(this.parent, mergeContext(this.context, input));
}

override setSystemMetricsCollector(collector: SystemMetricsCollectorHandle): void {
this.parent.setSystemMetricsCollector(collector);
}

override attachSink(sink: EventSink): void {
this.parent.attachSink(sink);
}
Expand Down
107 changes: 107 additions & 0 deletions packages/telemetry/src/systemMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { cpus, freemem, loadavg, totalmem } from 'node:os';

import type { TelemetryProperties } from './types';

const DEFAULT_INTERVAL_MS = 30_000;
const DEFAULT_WARMUP_SAMPLE_MS = 1_500;
const SYSTEM_METRICS_EVENT = 'system_metrics';

export interface SystemMetricsTrackClient {
track(event: string, properties?: TelemetryProperties): void;
}

export interface SystemMetricsCollectorOptions {
readonly client: SystemMetricsTrackClient;
readonly intervalMs?: number;
readonly warmupSampleMs?: number | null;
}

export class SystemMetricsCollector {
private readonly client: SystemMetricsTrackClient;
private readonly intervalMs: number;
private readonly warmupSampleMs: number | null;
private intervalTimer: ReturnType<typeof setInterval> | null = null;
private warmupTimer: ReturnType<typeof setTimeout> | null = null;
private previousCpuUsage = process.cpuUsage();
private previousHrtime = process.hrtime.bigint();
private readonly processStartedAtSeconds = Math.floor(Date.now() / 1000 - process.uptime());

constructor(options: SystemMetricsCollectorOptions) {
this.client = options.client;
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
this.warmupSampleMs =
options.warmupSampleMs === undefined ? DEFAULT_WARMUP_SAMPLE_MS : options.warmupSampleMs;
}

start(): void {
if (this.intervalTimer !== null) return;

if (this.warmupSampleMs !== null && this.warmupSampleMs > 0) {
this.warmupTimer = setTimeout(() => {
this.warmupTimer = null;
this.sampleSafely();
}, this.warmupSampleMs);
this.warmupTimer.unref?.();
}

this.intervalTimer = setInterval(() => {
this.sampleSafely();
}, this.intervalMs);
this.intervalTimer.unref?.();
}

stop(): void {
if (this.warmupTimer !== null) {
clearTimeout(this.warmupTimer);
this.warmupTimer = null;
}
if (this.intervalTimer !== null) {
clearInterval(this.intervalTimer);
this.intervalTimer = null;
}
}

private sampleSafely(): void {
try {
this.sample();
} catch {
this.stop();
}
}

private sample(): void {
const now = process.hrtime.bigint();
const elapsedUs = Number(now - this.previousHrtime) / 1_000;

const cpu = process.cpuUsage(this.previousCpuUsage);
this.previousCpuUsage = process.cpuUsage();
this.previousHrtime = now;

const mem = process.memoryUsage();
const constrainedMemory = getConstrainedMemoryBytes();

this.client.track(SYSTEM_METRICS_EVENT, {
process_started_at: this.processStartedAtSeconds,
process_uptime_ms: Math.round(process.uptime() * 1000),
rss_bytes: mem.rss,
heap_used_bytes: mem.heapUsed,
heap_total_bytes: mem.heapTotal,
external_bytes: mem.external,
array_buffers_bytes: mem.arrayBuffers,
constrained_memory_bytes: constrainedMemory,
cpu_user_us: cpu.user,
cpu_system_us: cpu.system,
cpu_elapsed_us: Math.round(elapsedUs),
load_avg_1m: loadavg()[0],
free_mem_bytes: freemem(),
total_mem_bytes: totalmem(),
cpu_count: cpus().length,
});
}
}

function getConstrainedMemoryBytes(): number | undefined {
if (typeof process.constrainedMemory !== 'function') return undefined;
const value = process.constrainedMemory();
return Number.isFinite(value) ? value : undefined;
}
Loading
Loading