diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index fd19ffa79c..611850bd4a 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -379,15 +379,30 @@ describe('initializeOutputListenersAndFlush', () => { describe('getNodeMemoryArgs', () => { let osTotalMemSpy: MockInstance; let v8GetHeapStatisticsSpy: MockInstance; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let originalConfig: any; beforeEach(() => { osTotalMemSpy = vi.spyOn(os, 'totalmem'); v8GetHeapStatisticsSpy = vi.spyOn(v8, 'getHeapStatistics'); delete process.env['GEMINI_CLI_NO_RELAUNCH']; + + originalConfig = process.config; + Object.defineProperty(process, 'config', { + value: { + ...originalConfig, + variables: { ...originalConfig?.variables, v8_enable_sandbox: 1 }, + }, + configurable: true, + }); }); afterEach(() => { vi.restoreAllMocks(); + Object.defineProperty(process, 'config', { + value: originalConfig, + configurable: true, + }); }); it('should return empty array if GEMINI_CLI_NO_RELAUNCH is set', () => { @@ -400,8 +415,10 @@ describe('getNodeMemoryArgs', () => { v8GetHeapStatisticsSpy.mockReturnValue({ heap_size_limit: 8 * 1024 * 1024 * 1024, // 8GB }); - // Target is 50% of 16GB = 8GB. Current is 8GB. No relaunch needed. - expect(getNodeMemoryArgs(false)).toEqual([]); + // Target is 50% of 16GB = 8GB. Current is 8GB. Relaunch needed for EPT size only. + expect(getNodeMemoryArgs(false)).toEqual([ + '--max-external-pointer-table-size=268435456', + ]); }); it('should return memory args if current heap limit is insufficient', () => { @@ -409,8 +426,11 @@ describe('getNodeMemoryArgs', () => { v8GetHeapStatisticsSpy.mockReturnValue({ heap_size_limit: 4 * 1024 * 1024 * 1024, // 4GB }); - // Target is 50% of 16GB = 8GB. Current is 4GB. Relaunch needed. - expect(getNodeMemoryArgs(false)).toEqual(['--max-old-space-size=8192']); + // Target is 50% of 16GB = 8GB. Current is 4GB. Relaunch needed for both. + expect(getNodeMemoryArgs(false)).toEqual([ + '--max-external-pointer-table-size=268435456', + '--max-old-space-size=8192', + ]); }); it('should log debug info when isDebugMode is true', () => { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index fa22f59267..f77fc11d61 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -111,6 +111,8 @@ export function validateDnsResolutionOrder( return defaultValue; } +const DEFAULT_EPT_SIZE = (256 * 1024 * 1024).toString(); + export function getNodeMemoryArgs(isDebugMode: boolean): string[] { const totalMemoryMB = os.totalmem() / (1024 * 1024); const heapStats = v8.getHeapStatistics(); @@ -130,16 +132,35 @@ export function getNodeMemoryArgs(isDebugMode: boolean): string[] { return []; } + const args: string[] = []; + + // Automatically expand the V8 External Pointer Table to 256MB to prevent + // out-of-memory crashes during high native-handle concurrency. + // Note: Only supported in specific Node.js versions compiled with V8 Sandbox enabled. + const eptFlag = `--max-external-pointer-table-size=${DEFAULT_EPT_SIZE}`; + const isV8SandboxEnabled = + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion + (process.config?.variables as any)?.v8_enable_sandbox === 1; + + if ( + isV8SandboxEnabled && + !process.execArgv.some((arg) => + arg.startsWith('--max-external-pointer-table-size'), + ) + ) { + args.push(eptFlag); + } + if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) { if (isDebugMode) { debugLogger.debug( `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`, ); } - return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`]; + args.push(`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`); } - return []; + return args; } export function setupUnhandledRejectionHandler() {