feat: add minimal V8 heap snapshot utility for memory diagnostics (#26440)

This commit is contained in:
Coco Sheng
2026-05-04 13:42:42 -04:00
committed by GitHub
parent 704be5a418
commit 790f2cf815
4 changed files with 97 additions and 0 deletions
@@ -0,0 +1,49 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import v8 from 'node:v8';
import fs from 'node:fs';
import { captureHeapSnapshot } from './heap-snapshot.js';
import { debugLogger } from '../utils/debugLogger.js';
vi.mock('node:v8');
vi.mock('node:fs');
vi.mock('../utils/debugLogger.js', () => ({
debugLogger: {
error: vi.fn(),
},
}));
describe('heap-snapshot', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('should capture a heap snapshot to a secure directory', () => {
vi.mocked(fs.mkdtempSync).mockReturnValue('/tmp/gemini-heap-abc123');
const filePath = captureHeapSnapshot();
expect(filePath).toContain('gemini-heap-abc123');
expect(filePath).toContain('.heapsnapshot');
expect(v8.writeHeapSnapshot).toHaveBeenCalledWith(filePath);
});
it('should return null and log an error if capture fails', () => {
vi.mocked(fs.mkdtempSync).mockImplementation(() => {
throw new Error('Disk full');
});
const result = captureHeapSnapshot();
expect(result).toBeNull();
expect(debugLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Failed to capture heap snapshot'),
expect.any(Error),
);
});
});
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import v8 from 'node:v8';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { debugLogger } from '../utils/debugLogger.js';
/**
* Utility to capture a V8 heap snapshot.
* Snapshots are saved to a secure, uniquely named temporary directory.
*
* @returns The absolute path to the generated .heapsnapshot file, or null if it failed.
*/
export function captureHeapSnapshot(): string | null {
try {
const timestamp = Date.now();
const filename = `gemini-heap-${timestamp}.heapsnapshot`;
// Use mkdtempSync for a secure, uniquely named directory (mitigates symlink attacks)
const snapshotsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-heap-'));
const filePath = path.join(snapshotsDir, filename);
// Note: v8.writeHeapSnapshot is a synchronous, blocking operation.
// This is intentional during diagnostics to capture a consistent heap state.
v8.writeHeapSnapshot(filePath);
return filePath;
} catch (error) {
// Telemetry/diagnostic failures should not crash the application
debugLogger.error('Failed to capture heap snapshot:', error);
return null;
}
}
+1
View File
@@ -92,6 +92,7 @@ export {
startGlobalMemoryMonitoring,
stopGlobalMemoryMonitoring,
} from './memory-monitor.js';
export { captureHeapSnapshot } from './heap-snapshot.js';
export type { MemorySnapshot, ProcessMetrics } from './memory-monitor.js';
export {
EventLoopMonitor,
@@ -17,6 +17,7 @@ import {
isPerformanceMonitoringActive,
} from './metrics.js';
import { RateLimiter } from './rate-limiter.js';
import { captureHeapSnapshot } from './heap-snapshot.js';
export interface MemorySnapshot {
timestamp: number;
@@ -386,6 +387,14 @@ export class MemoryMonitor {
this.highWaterMarkTracker.resetAllHighWaterMarks();
}
/**
* Capture a V8 heap snapshot for memory diagnostics.
* @returns The absolute path to the generated .heapsnapshot file, or null if it failed.
*/
captureHeapSnapshot(): string | null {
return captureHeapSnapshot();
}
/**
* Cleanup resources
*/