mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 13:53:02 -07:00
feat(cli): add /bug-memory command and auto-capture heap snapshot in /bug (#25639)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { Readable } from 'node:stream';
|
||||
import {
|
||||
captureHeapSnapshot,
|
||||
MEMORY_SNAPSHOT_AUTO_THRESHOLD_BYTES,
|
||||
} from './memorySnapshot.js';
|
||||
|
||||
const { mkdirMock, pipelineMock, getHeapSnapshotMock, createWriteStreamMock } =
|
||||
vi.hoisted(() => ({
|
||||
mkdirMock: vi.fn(async () => undefined),
|
||||
pipelineMock: vi.fn(async () => undefined),
|
||||
getHeapSnapshotMock: vi.fn(),
|
||||
createWriteStreamMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
||||
return { ...actual, mkdir: mkdirMock };
|
||||
});
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>();
|
||||
return { ...actual, createWriteStream: createWriteStreamMock };
|
||||
});
|
||||
|
||||
vi.mock('node:v8', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:v8')>();
|
||||
return { ...actual, getHeapSnapshot: getHeapSnapshotMock };
|
||||
});
|
||||
|
||||
vi.mock('node:stream/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:stream/promises')>();
|
||||
return { ...actual, pipeline: pipelineMock };
|
||||
});
|
||||
|
||||
describe('captureHeapSnapshot', () => {
|
||||
beforeEach(() => {
|
||||
mkdirMock.mockClear();
|
||||
pipelineMock.mockClear();
|
||||
getHeapSnapshotMock.mockClear().mockReturnValue(Readable.from([]));
|
||||
createWriteStreamMock
|
||||
.mockClear()
|
||||
.mockReturnValue({ write: vi.fn(), end: vi.fn() });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('exports the 2 GB auto-capture threshold', () => {
|
||||
expect(MEMORY_SNAPSHOT_AUTO_THRESHOLD_BYTES).toBe(2 * 1024 * 1024 * 1024);
|
||||
});
|
||||
|
||||
it('creates the target directory and pipelines the V8 snapshot to disk', async () => {
|
||||
const target = '/tmp/gemini-test/snapshot.heapsnapshot';
|
||||
|
||||
await captureHeapSnapshot(target);
|
||||
|
||||
expect(mkdirMock).toHaveBeenCalledWith('/tmp/gemini-test', {
|
||||
recursive: true,
|
||||
});
|
||||
expect(getHeapSnapshotMock).toHaveBeenCalledTimes(1);
|
||||
expect(createWriteStreamMock).toHaveBeenCalledWith(target);
|
||||
expect(pipelineMock).toHaveBeenCalledTimes(1);
|
||||
expect(pipelineMock).toHaveBeenCalledWith(
|
||||
getHeapSnapshotMock.mock.results[0].value,
|
||||
createWriteStreamMock.mock.results[0].value,
|
||||
);
|
||||
});
|
||||
|
||||
it('propagates pipeline failures to the caller', async () => {
|
||||
pipelineMock.mockRejectedValueOnce(new Error('write failed'));
|
||||
|
||||
await expect(
|
||||
captureHeapSnapshot('/tmp/gemini-test/fail.heapsnapshot'),
|
||||
).rejects.toThrow('write failed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { getHeapSnapshot } from 'node:v8';
|
||||
|
||||
/**
|
||||
* RSS threshold at which `/bug` auto-captures a heap snapshot.
|
||||
*/
|
||||
export const MEMORY_SNAPSHOT_AUTO_THRESHOLD_BYTES = 2 * 1024 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Capture a V8 heap snapshot from the current process and write it to disk.
|
||||
*
|
||||
* `v8.getHeapSnapshot()` returns a Readable stream whose producer is V8's
|
||||
* internal snapshot generator. Piping it through `node:stream/promises`'
|
||||
* `pipeline` propagates backpressure end-to-end, so even a multi-gigabyte
|
||||
* heap is written without buffering the serialized snapshot in memory.
|
||||
* Nothing is exposed over a debugger port.
|
||||
*/
|
||||
export async function captureHeapSnapshot(filePath: string): Promise<void> {
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
await pipeline(getHeapSnapshot(), createWriteStream(filePath));
|
||||
}
|
||||
Reference in New Issue
Block a user