Download memory-tests/memory-usage.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/memory-tests/memory-usage.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/memory-tests/memory-usage.test.ts
-
curl -L -o memory-usage.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/memory-tests/memory-usage.test.ts
15.5 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, beforeAll, afterAll, afterEach } from 'vitest'; | |
| import { TestRig, MemoryTestHarness } from '@google/gemini-cli-test-utils'; | |
| import { join, dirname } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { | |
| createWriteStream, | |
| copyFileSync, | |
| readFileSync, | |
| existsSync, | |
| mkdirSync, | |
| rmSync, | |
| } from 'node:fs'; | |
| import { randomUUID, createHash } from 'node:crypto'; | |
| const __dirname = dirname(fileURLToPath(import.meta.url)); | |
| const BASELINES_PATH = join(__dirname, 'baselines.json'); | |
| const UPDATE_BASELINES = process.env['UPDATE_MEMORY_BASELINES'] === 'true'; | |
| function getProjectHash(projectRoot: string): string { | |
| return createHash('sha256').update(projectRoot).digest('hex'); | |
| } | |
| const TOLERANCE_PERCENT = 10; | |
| // Fake API key for tests using fake responses | |
| const TEST_ENV = { | |
| GEMINI_API_KEY: 'fake-memory-test-key', | |
| GEMINI_MEMORY_MONITOR_INTERVAL: '100', | |
| }; | |
| describe('Memory Usage Tests', () => { | |
| let harness: MemoryTestHarness; | |
| let rig: TestRig; | |
| beforeAll(() => { | |
| harness = new MemoryTestHarness({ | |
| baselinesPath: BASELINES_PATH, | |
| defaultTolerancePercent: TOLERANCE_PERCENT, | |
| gcCycles: 3, | |
| gcDelayMs: 100, | |
| sampleCount: 3, | |
| }); | |
| }); | |
| afterEach(async () => { | |
| await rig.cleanup(); | |
| }); | |
| afterAll(async () => { | |
| // Generate the summary report after all tests | |
| await harness.generateReport(); | |
| }); | |
| it('idle-session-startup: memory usage within baseline', async () => { | |
| rig = new TestRig(); | |
| rig.setup('memory-idle-startup', { | |
| fakeResponsesPath: join(__dirname, 'memory.idle-startup.responses'), | |
| }); | |
| const result = await harness.runScenario( | |
| rig, | |
| 'idle-session-startup', | |
| async (recordSnapshot) => { | |
| await rig.run({ | |
| args: ['hello'], | |
| timeout: 120000, | |
| env: TEST_ENV, | |
| }); | |
| await recordSnapshot('after-startup'); | |
| }, | |
| ); | |
| if (UPDATE_BASELINES) { | |
| harness.updateScenarioBaseline(result); | |
| console.log( | |
| `Updated baseline for idle-session-startup: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`, | |
| ); | |
| } else { | |
| harness.assertWithinBaseline(result); | |
| } | |
| }); | |
| it('simple-prompt-response: memory usage within baseline', async () => { | |
| rig = new TestRig(); | |
| rig.setup('memory-simple-prompt', { | |
| fakeResponsesPath: join(__dirname, 'memory.simple-prompt.responses'), | |
| }); | |
| const result = await harness.runScenario( | |
| rig, | |
| 'simple-prompt-response', | |
| async (recordSnapshot) => { | |
| await rig.run({ | |
| args: ['What is the capital of France?'], | |
| timeout: 120000, | |
| env: TEST_ENV, | |
| }); | |
| await recordSnapshot('after-response'); | |
| }, | |
| ); | |
| if (UPDATE_BASELINES) { | |
| harness.updateScenarioBaseline(result); | |
| console.log( | |
| `Updated baseline for simple-prompt-response: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`, | |
| ); | |
| } else { | |
| harness.assertWithinBaseline(result); | |
| } | |
| }); | |
| it('multi-turn-conversation: memory remains stable over turns', async () => { | |
| rig = new TestRig(); | |
| rig.setup('memory-multi-turn', { | |
| fakeResponsesPath: join(__dirname, 'memory.multi-turn.responses'), | |
| }); | |
| const prompts = [ | |
| 'Hello, what can you help me with?', | |
| 'Tell me about JavaScript', | |
| 'How is TypeScript different?', | |
| 'Can you write a simple TypeScript function?', | |
| 'What are some TypeScript best practices?', | |
| ]; | |
| const result = await harness.runScenario( | |
| rig, | |
| 'multi-turn-conversation', | |
| async (recordSnapshot) => { | |
| // Run through all turns as a piped sequence | |
| const stdinContent = prompts.join('\n'); | |
| await rig.run({ | |
| stdin: stdinContent, | |
| timeout: 120000, | |
| env: TEST_ENV, | |
| }); | |
| // Take snapshots after the conversation completes | |
| await recordSnapshot('after-all-turns'); | |
| }, | |
| ); | |
| if (UPDATE_BASELINES) { | |
| harness.updateScenarioBaseline(result); | |
| console.log( | |
| `Updated baseline for multi-turn-conversation: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`, | |
| ); | |
| } else { | |
| harness.assertWithinBaseline(result); | |
| harness.assertMemoryReturnsToBaseline(result.snapshots, 20); | |
| const { leaked, message } = harness.analyzeSnapshots(result.snapshots); | |
| if (leaked) console.warn(`⚠ ${message}`); | |
| } | |
| }); | |
| it('multi-function-call-repo-search: memory after tool use', async () => { | |
| rig = new TestRig(); | |
| rig.setup('memory-multi-func-call', { | |
| fakeResponsesPath: join( | |
| __dirname, | |
| 'memory.multi-function-call.responses', | |
| ), | |
| }); | |
| // Create directories first, then files in the workspace so the tools have targets | |
| rig.mkdir('packages/core/src/telemetry'); | |
| rig.createFile( | |
| 'packages/core/src/telemetry/memory-monitor.ts', | |
| 'export class MemoryMonitor { constructor() {} }', | |
| ); | |
| rig.createFile( | |
| 'packages/core/src/telemetry/metrics.ts', | |
| 'export function recordMemoryUsage() {}', | |
| ); | |
| const result = await harness.runScenario( | |
| rig, | |
| 'multi-function-call-repo-search', | |
| async (recordSnapshot) => { | |
| await rig.run({ | |
| args: [ | |
| 'Search this repository for MemoryMonitor and tell me what it does', | |
| ], | |
| timeout: 120000, | |
| env: TEST_ENV, | |
| }); | |
| await recordSnapshot('after-tool-calls'); | |
| }, | |
| ); | |
| if (UPDATE_BASELINES) { | |
| harness.updateScenarioBaseline(result); | |
| console.log( | |
| `Updated baseline for multi-function-call-repo-search: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`, | |
| ); | |
| } else { | |
| harness.assertWithinBaseline(result); | |
| harness.assertMemoryReturnsToBaseline(result.snapshots, 20); | |
| } | |
| }); | |
| describe('Large Chat Scenarios', () => { | |
| let sharedResumeResponsesPath: string; | |
| let sharedActiveResponsesPath: string; | |
| let sharedHistoryPath: string; | |
| let sharedPrompts: string; | |
| let tempDir: string; | |
| beforeAll(async () => { | |
| tempDir = join(__dirname, `large-chat-tmp-${randomUUID()}`); | |
| mkdirSync(tempDir, { recursive: true }); | |
| const { resumeResponsesPath, activeResponsesPath, historyPath, prompts } = | |
| await generateSharedLargeChatData(tempDir); | |
| sharedActiveResponsesPath = activeResponsesPath; | |
| sharedResumeResponsesPath = resumeResponsesPath; | |
| sharedHistoryPath = historyPath; | |
| sharedPrompts = prompts; | |
| }, 60000); | |
| afterAll(() => { | |
| if (existsSync(tempDir)) { | |
| rmSync(tempDir, { recursive: true, force: true }); | |
| } | |
| }); | |
| afterEach(async () => { | |
| await rig.cleanup(); | |
| }); | |
| it('large-chat: memory usage within baseline', async () => { | |
| rig = new TestRig(); | |
| rig.setup('memory-large-chat', { | |
| fakeResponsesPath: sharedActiveResponsesPath, | |
| }); | |
| const result = await harness.runScenario( | |
| rig, | |
| 'large-chat', | |
| async (recordSnapshot) => { | |
| await rig.run({ | |
| stdin: sharedPrompts, | |
| timeout: 600000, | |
| env: TEST_ENV, | |
| }); | |
| await recordSnapshot('after-large-chat'); | |
| }, | |
| ); | |
| if (UPDATE_BASELINES) { | |
| harness.updateScenarioBaseline(result); | |
| console.log( | |
| `Updated baseline for large-chat: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`, | |
| ); | |
| } else { | |
| harness.assertWithinBaseline(result); | |
| } | |
| }); | |
| it('resume-large-chat: memory usage within baseline', async () => { | |
| rig = new TestRig(); | |
| rig.setup('memory-resume-large-chat', { | |
| fakeResponsesPath: sharedResumeResponsesPath, | |
| }); | |
| const result = await harness.runScenario( | |
| rig, | |
| 'resume-large-chat', | |
| async (recordSnapshot) => { | |
| // Ensure the history file is linked | |
| const targetChatsDir = join( | |
| rig.homeDir!, | |
| '.gemini', | |
| 'tmp', | |
| getProjectHash(rig.testDir!), | |
| 'chats', | |
| ); | |
| mkdirSync(targetChatsDir, { recursive: true }); | |
| const targetHistoryPath = join( | |
| targetChatsDir, | |
| 'session-large-chat.json', | |
| ); | |
| if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath); | |
| copyFileSync(sharedHistoryPath, targetHistoryPath); | |
| await rig.run({ | |
| // add a prompt to make sure it does not hang there and exits immediately | |
| args: ['--resume', 'latest', '--prompt', 'hello'], | |
| timeout: 600000, | |
| env: TEST_ENV, | |
| }); | |
| await recordSnapshot('after-resume-large-chat'); | |
| }, | |
| ); | |
| if (UPDATE_BASELINES) { | |
| harness.updateScenarioBaseline(result); | |
| console.log( | |
| `Updated baseline for resume-large-chat: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`, | |
| ); | |
| } else { | |
| harness.assertWithinBaseline(result); | |
| } | |
| }); | |
| it('resume-large-chat-with-messages: memory usage within baseline', async () => { | |
| rig = new TestRig(); | |
| rig.setup('memory-resume-large-chat-msgs', { | |
| fakeResponsesPath: sharedResumeResponsesPath, | |
| }); | |
| const result = await harness.runScenario( | |
| rig, | |
| 'resume-large-chat-with-messages', | |
| async (recordSnapshot) => { | |
| // Ensure the history file is linked | |
| const targetChatsDir = join( | |
| rig.homeDir!, | |
| '.gemini', | |
| 'tmp', | |
| getProjectHash(rig.testDir!), | |
| 'chats', | |
| ); | |
| mkdirSync(targetChatsDir, { recursive: true }); | |
| const targetHistoryPath = join( | |
| targetChatsDir, | |
| 'session-large-chat.json', | |
| ); | |
| if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath); | |
| copyFileSync(sharedHistoryPath, targetHistoryPath); | |
| const stdinContent = 'new prompt 1\nnew prompt 2\n'; | |
| await rig.run({ | |
| args: ['--resume', 'latest'], | |
| stdin: stdinContent, | |
| timeout: 600000, | |
| env: TEST_ENV, | |
| }); | |
| await recordSnapshot('after-resume-and-append'); | |
| }, | |
| ); | |
| if (UPDATE_BASELINES) { | |
| harness.updateScenarioBaseline(result); | |
| console.log( | |
| `Updated baseline for resume-large-chat-with-messages: ${(result.finalHeapUsed / (1024 * 1024)).toFixed(1)} MB`, | |
| ); | |
| } else { | |
| harness.assertWithinBaseline(result); | |
| } | |
| }); | |
| }); | |
| }); | |
| async function generateSharedLargeChatData(tempDir: string) { | |
| const resumeResponsesPath = join(tempDir, 'large-chat-resume-chat.responses'); | |
| const activeResponsesPath = join(tempDir, 'large-chat-active-chat.responses'); | |
| const historyPath = join(tempDir, 'large-chat-history.json'); | |
| const sourceSessionPath = join(__dirname, 'large-chat-session.json'); | |
| const session = JSON.parse(readFileSync(sourceSessionPath, 'utf8')); | |
| const messages = session.messages; | |
| copyFileSync(sourceSessionPath, historyPath); | |
| // Generate fake responses for active chat | |
| const promptsList: string[] = []; | |
| const activeResponsesStream = createWriteStream(activeResponsesPath); | |
| const complexityResponse = { | |
| method: 'generateContent', | |
| response: { | |
| candidates: [ | |
| { | |
| content: { | |
| parts: [ | |
| { | |
| text: '{"complexity_reasoning":"simple","complexity_score":1}', | |
| }, | |
| ], | |
| role: 'model', | |
| }, | |
| finishReason: 'STOP', | |
| index: 0, | |
| }, | |
| ], | |
| }, | |
| }; | |
| const summaryResponse = { | |
| method: 'generateContent', | |
| response: { | |
| candidates: [ | |
| { | |
| content: { | |
| parts: [ | |
| { text: '{"originalSummary":"large chat summary","events":[]}' }, | |
| ], | |
| role: 'model', | |
| }, | |
| finishReason: 'STOP', | |
| index: 0, | |
| }, | |
| ], | |
| }, | |
| }; | |
| for (let i = 0; i < messages.length; i++) { | |
| const msg = messages[i]; | |
| if (msg.type === 'user') { | |
| promptsList.push(msg.content[0].text); | |
| // Start of a new turn | |
| activeResponsesStream.write(JSON.stringify(complexityResponse) + '\n'); | |
| // Find all subsequent gemini messages until the next user message | |
| let j = i + 1; | |
| while (j < messages.length && messages[j].type === 'gemini') { | |
| const geminiMsg = messages[j]; | |
| const parts = []; | |
| if (geminiMsg.content) { | |
| parts.push({ text: geminiMsg.content }); | |
| } | |
| if (geminiMsg.toolCalls) { | |
| for (const tc of geminiMsg.toolCalls) { | |
| parts.push({ | |
| functionCall: { | |
| name: tc.name, | |
| args: tc.args, | |
| }, | |
| }); | |
| } | |
| } | |
| activeResponsesStream.write( | |
| JSON.stringify({ | |
| method: 'generateContentStream', | |
| response: [ | |
| { | |
| candidates: [ | |
| { | |
| content: { parts, role: 'model' }, | |
| finishReason: 'STOP', | |
| index: 0, | |
| }, | |
| ], | |
| usageMetadata: { | |
| promptTokenCount: 100, | |
| candidatesTokenCount: 100, | |
| totalTokenCount: 200, | |
| promptTokensDetails: [{ modality: 'TEXT', tokenCount: 100 }], | |
| }, | |
| }, | |
| ], | |
| }) + '\n', | |
| ); | |
| j++; | |
| } | |
| // End of turn | |
| activeResponsesStream.write(JSON.stringify(summaryResponse) + '\n'); | |
| // Skip the gemini messages we just processed | |
| i = j - 1; | |
| } | |
| } | |
| activeResponsesStream.end(); | |
| // Generate responses for resumed chat | |
| const resumeResponsesStream = createWriteStream(resumeResponsesPath); | |
| for (let i = 0; i < 5; i++) { | |
| // Doubling up on non-streaming responses to satisfy classifier and complexity checks | |
| resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n'); | |
| resumeResponsesStream.write(JSON.stringify(summaryResponse) + '\n'); | |
| resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n'); | |
| resumeResponsesStream.write( | |
| JSON.stringify({ | |
| method: 'generateContentStream', | |
| response: [ | |
| { | |
| candidates: [ | |
| { | |
| content: { | |
| parts: [{ text: `Resume response ${i}` }], | |
| role: 'model', | |
| }, | |
| finishReason: 'STOP', | |
| index: 0, | |
| }, | |
| ], | |
| usageMetadata: { | |
| promptTokenCount: 10, | |
| candidatesTokenCount: 10, | |
| totalTokenCount: 20, | |
| promptTokensDetails: [{ modality: 'TEXT', tokenCount: 10 }], | |
| }, | |
| }, | |
| ], | |
| }) + '\n', | |
| ); | |
| resumeResponsesStream.write(JSON.stringify(summaryResponse) + '\n'); | |
| } | |
| resumeResponsesStream.end(); | |
| // Wait for streams to finish | |
| await Promise.all([ | |
| new Promise((res) => | |
| activeResponsesStream.on('finish', () => res(undefined)), | |
| ), | |
| new Promise((res) => | |
| resumeResponsesStream.on('finish', () => res(undefined)), | |
| ), | |
| ]); | |
| return { | |
| resumeResponsesPath, | |
| activeResponsesPath, | |
| historyPath, | |
| prompts: promptsList.join('\n'), | |
| }; | |
| } | |