mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-12 04:17:15 -07:00
258 lines
7.9 KiB
TypeScript
258 lines
7.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
|
import { TestRig, PerfTestHarness } from '@google/gemini-cli-test-utils';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const BASELINES_PATH = join(__dirname, 'baselines.json');
|
|
const TOLERANCE_PERCENT = 15;
|
|
|
|
const SAMPLE_COUNT = 1;
|
|
const WARMUP_COUNT = 0;
|
|
|
|
describe.each([
|
|
{ alt: true, term: true },
|
|
{ alt: false, term: false },
|
|
{ alt: true, term: false },
|
|
{ alt: false, term: true },
|
|
])('Scrolling Performance Tests (alt=$alt, term=$term)', ({ alt, term }) => {
|
|
let harness: PerfTestHarness;
|
|
|
|
beforeAll(() => {
|
|
harness = new PerfTestHarness({
|
|
baselinesPath: BASELINES_PATH,
|
|
defaultTolerancePercent: TOLERANCE_PERCENT,
|
|
sampleCount: SAMPLE_COUNT,
|
|
warmupCount: WARMUP_COUNT,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await harness.generateReport();
|
|
}, 30000);
|
|
|
|
async function createRealHistory(rig: TestRig, count: number) {
|
|
const run = await rig.runInteractive({
|
|
args: [],
|
|
env: {
|
|
HOME: rig.homeDir!,
|
|
GEMINI_API_KEY: 'test-api-key',
|
|
},
|
|
});
|
|
|
|
await run.expectText('type your message');
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
// Send the /compress command directly
|
|
await run.sendText('/compress');
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
// First enter to select from autocomplete
|
|
await run.sendText('\r');
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
// Second enter to execute
|
|
await run.sendText('\r');
|
|
// Wait for the output to confirm execution
|
|
await run.expectText('nothing to compress.');
|
|
}
|
|
|
|
// Exit gracefully by sending SIGTERM to ensure telemetry is flushed
|
|
await run.kill();
|
|
await run.expectExit();
|
|
}
|
|
|
|
function readMetrics(
|
|
rig: TestRig,
|
|
): { p50: number; p95: number; max: number }[] {
|
|
const logs = rig.readTelemetryLogs();
|
|
const metrics: { p50: number; p95: number; max: number }[] = [];
|
|
|
|
for (const log of logs) {
|
|
if (log.scopeMetrics) {
|
|
for (const sm of log.scopeMetrics) {
|
|
for (const m of sm.metrics) {
|
|
if (m.descriptor.name === 'gemini_cli.event_loop.delay') {
|
|
const p50 = m.dataPoints.find(
|
|
(dp: { attributes: { percentile: string }; value: number }) =>
|
|
dp.attributes.percentile === 'p50',
|
|
)?.value;
|
|
const p95 = m.dataPoints.find(
|
|
(dp: { attributes: { percentile: string }; value: number }) =>
|
|
dp.attributes.percentile === 'p95',
|
|
)?.value;
|
|
const max = m.dataPoints.find(
|
|
(dp: { attributes: { percentile: string }; value: number }) =>
|
|
dp.attributes.percentile === 'max',
|
|
)?.value;
|
|
|
|
if (p50 !== undefined || p95 !== undefined || max !== undefined) {
|
|
metrics.push({ p50: p50 ?? 0, p95: p95 ?? 0, max: max ?? 0 });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return metrics;
|
|
}
|
|
|
|
it('straight-scroll-up: scrolling all the way up', async () => {
|
|
await harness.runScenario(
|
|
`straight-scroll-up-alt-${alt}-term-${term}`,
|
|
async () => {
|
|
const rig = new TestRig();
|
|
try {
|
|
rig.setup(`perf-scroll-up-${alt}-${term}`, {
|
|
settings: {
|
|
ui: {
|
|
useAlternateBuffer: alt,
|
|
terminalBuffer: term,
|
|
},
|
|
},
|
|
});
|
|
|
|
await createRealHistory(rig, 10);
|
|
|
|
const run = await rig.runInteractive({
|
|
args: ['--resume', '1'],
|
|
env: {
|
|
GEMINI_EVENT_LOOP_MONITOR_ENABLED: 'true',
|
|
GEMINI_MEMORY_MONITOR_INTERVAL: '500',
|
|
HOME: rig.homeDir!,
|
|
GEMINI_API_KEY: 'test-api-key',
|
|
},
|
|
});
|
|
|
|
await run.expectText('type your message');
|
|
// We expect to see the history text when resuming
|
|
await run.expectText('nothing to compress.', 30000);
|
|
|
|
const snapshot = await harness.measure(
|
|
'straight-scroll',
|
|
async () => {
|
|
// Send PageUp 10 times to go to top
|
|
for (let i = 0; i < 10; i++) {
|
|
run.ptyProcess.write('\x1b[5~');
|
|
await new Promise((r) => setImmediate(r));
|
|
}
|
|
await run.expectText('type your message');
|
|
},
|
|
);
|
|
|
|
await run.type('/exit');
|
|
await run.sendText('\r');
|
|
await run.sendText('\r');
|
|
await run.expectExit();
|
|
run.ptyProcess.kill();
|
|
|
|
const metrics = readMetrics(rig);
|
|
|
|
expect(metrics.length).toBeGreaterThan(0);
|
|
|
|
if (metrics.length > 0) {
|
|
snapshot.eventLoopDelayP50Ms = Math.max(
|
|
...metrics.map((m) => m.p50),
|
|
);
|
|
snapshot.eventLoopDelayP95Ms = Math.max(
|
|
...metrics.map((m) => m.p95),
|
|
);
|
|
snapshot.eventLoopDelayMaxMs = Math.max(
|
|
...metrics.map((m) => m.max),
|
|
);
|
|
}
|
|
|
|
expect(snapshot.eventLoopDelayMaxMs).toBeLessThan(250);
|
|
|
|
return snapshot;
|
|
} finally {
|
|
rig.cleanup();
|
|
delete process.env['GEMINI_EVENT_LOOP_MONITOR_ENABLED'];
|
|
delete process.env['GEMINI_MEMORY_MONITOR_INTERVAL'];
|
|
}
|
|
},
|
|
);
|
|
}, 180000);
|
|
|
|
it('jitter-scroll: random scroll ups and downs', async () => {
|
|
await harness.runScenario(
|
|
`jitter-scroll-alt-${alt}-term-${term}`,
|
|
async () => {
|
|
const rig = new TestRig();
|
|
try {
|
|
rig.setup(`perf-jitter-scroll-${alt}-${term}`, {
|
|
settings: {
|
|
ui: {
|
|
useAlternateBuffer: alt,
|
|
terminalBuffer: term,
|
|
},
|
|
},
|
|
});
|
|
|
|
await createRealHistory(rig, 10);
|
|
|
|
const run = await rig.runInteractive({
|
|
args: ['--resume', '1'],
|
|
env: {
|
|
GEMINI_EVENT_LOOP_MONITOR_ENABLED: 'true',
|
|
GEMINI_MEMORY_MONITOR_INTERVAL: '500',
|
|
HOME: rig.homeDir!,
|
|
GEMINI_API_KEY: 'test-api-key',
|
|
},
|
|
});
|
|
|
|
await run.expectText('type your message');
|
|
await run.expectText('nothing to compress.', 30000);
|
|
|
|
const snapshot = await harness.measure('jitter-scroll', async () => {
|
|
// Simulate jitter: 3 up, 1 down. Do it 10 times to ensure we go all the way up.
|
|
for (let i = 0; i < 10; i++) {
|
|
for (let j = 0; j < 3; j++) {
|
|
run.ptyProcess.write('\x1b[5~'); // PageUp
|
|
await new Promise((r) => setImmediate(r));
|
|
}
|
|
run.ptyProcess.write('\x1b[6~'); // PageDown
|
|
await new Promise((r) => setImmediate(r));
|
|
}
|
|
await run.expectText('type your message');
|
|
});
|
|
|
|
await run.type('/exit');
|
|
await run.sendText('\r');
|
|
await run.sendText('\r');
|
|
await run.expectExit();
|
|
run.ptyProcess.kill();
|
|
|
|
const metrics = readMetrics(rig);
|
|
|
|
expect(metrics.length).toBeGreaterThan(0);
|
|
|
|
if (metrics.length > 0) {
|
|
snapshot.eventLoopDelayP50Ms = Math.max(
|
|
...metrics.map((m) => m.p50),
|
|
);
|
|
snapshot.eventLoopDelayP95Ms = Math.max(
|
|
...metrics.map((m) => m.p95),
|
|
);
|
|
snapshot.eventLoopDelayMaxMs = Math.max(
|
|
...metrics.map((m) => m.max),
|
|
);
|
|
}
|
|
|
|
expect(snapshot.eventLoopDelayMaxMs).toBeLessThan(250);
|
|
|
|
return snapshot;
|
|
} finally {
|
|
rig.cleanup();
|
|
delete process.env['GEMINI_EVENT_LOOP_MONITOR_ENABLED'];
|
|
delete process.env['GEMINI_MEMORY_MONITOR_INTERVAL'];
|
|
}
|
|
},
|
|
);
|
|
}, 180000);
|
|
});
|