fix(cli): optimize startup with lightweight parent process (#24667)

This commit is contained in:
Sehoon Shon
2026-04-08 20:17:32 -04:00
committed by GitHub
parent f1bb2af6de
commit 464bac270c
6 changed files with 170 additions and 49 deletions

View File

@@ -6,9 +6,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { main } from './src/gemini.js';
import { FatalError, writeToStderr } from '@google/gemini-cli-core';
import { runExitCleanup } from './src/utils/cleanup.js';
import { spawn } from 'node:child_process';
import os from 'node:os';
import v8 from 'node:v8';
// --- Global Entry Point ---
@@ -28,44 +28,162 @@ process.on('uncaughtException', (error) => {
// For other errors, we rely on the default behavior, but since we attached a listener,
// we must manually replicate it.
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
process.stderr.write(error.stack + '\n');
} else {
writeToStderr(String(error) + '\n');
process.stderr.write(String(error) + '\n');
}
process.exit(1);
});
main().catch(async (error) => {
// Set a timeout to force exit if cleanup hangs
const cleanupTimeout = setTimeout(() => {
writeToStderr('Cleanup timed out, forcing exit...\n');
process.exit(1);
}, 5000);
async function getMemoryNodeArgs(): Promise<string[]> {
let autoConfigureMemory = true;
try {
await runExitCleanup();
} catch (cleanupError) {
writeToStderr(
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
);
} finally {
clearTimeout(cleanupTimeout);
}
if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
const { readFileSync } = await import('node:fs');
const { join } = await import('node:path');
// Respect GEMINI_CLI_HOME environment variable, falling back to os.homedir()
const baseDir =
process.env['GEMINI_CLI_HOME'] || join(os.homedir(), '.gemini');
const settingsPath = join(baseDir, 'settings.json');
const rawSettings = readFileSync(settingsPath, 'utf8');
const settings = JSON.parse(rawSettings);
if (settings?.advanced?.autoConfigureMemory === false) {
autoConfigureMemory = false;
}
writeToStderr(errorMessage + '\n');
process.exit(error.exitCode);
} catch {
// ignore
}
writeToStderr('An unexpected critical error occurred:');
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
} else {
writeToStderr(String(error) + '\n');
if (autoConfigureMemory) {
const totalMemoryMB = os.totalmem() / (1024 * 1024);
const heapStats = v8.getHeapStatistics();
const currentMaxOldSpaceSizeMb = Math.floor(
heapStats.heap_size_limit / 1024 / 1024,
);
const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);
if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`];
}
}
process.exit(1);
});
return [];
}
async function run() {
if (!process.env['GEMINI_CLI_NO_RELAUNCH'] && !process.env['SANDBOX']) {
// --- Lightweight Parent Process / Daemon ---
// We avoid importing heavy dependencies here to save ~1.5s of startup time.
const nodeArgs: string[] = [...process.execArgv];
const scriptArgs = process.argv.slice(2);
const memoryArgs = await getMemoryNodeArgs();
nodeArgs.push(...memoryArgs);
const script = process.argv[1];
nodeArgs.push(script);
nodeArgs.push(...scriptArgs);
const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
const RELAUNCH_EXIT_CODE = 199;
let latestAdminSettings: unknown = undefined;
// Prevent the parent process from exiting prematurely on signals.
// The child process will receive the same signals and handle its own cleanup.
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
process.on(sig as NodeJS.Signals, () => {});
}
const runner = () => {
process.stdin.pause();
const child = spawn(process.execPath, nodeArgs, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: newEnv,
});
if (latestAdminSettings) {
child.send({ type: 'admin-settings', settings: latestAdminSettings });
}
child.on('message', (msg: { type?: string; settings?: unknown }) => {
if (msg.type === 'admin-settings-update' && msg.settings) {
latestAdminSettings = msg.settings;
}
});
return new Promise<number>((resolve) => {
child.on('error', (err) => {
process.stderr.write(
'Error: Failed to start child process: ' + err.message + '\n',
);
resolve(1);
});
child.on('close', (code) => {
process.stdin.resume();
resolve(code ?? 1);
});
});
};
while (true) {
try {
const exitCode = await runner();
if (exitCode !== RELAUNCH_EXIT_CODE) {
process.exit(exitCode);
}
} catch (error: unknown) {
process.stdin.resume();
process.stderr.write(
`Fatal error: Failed to relaunch the CLI process.\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
process.exit(1);
}
}
} else {
// --- Heavy Child Process ---
// Now we can safely import everything.
const { main } = await import('./src/gemini.js');
const { FatalError, writeToStderr } = await import(
'@google/gemini-cli-core'
);
const { runExitCleanup } = await import('./src/utils/cleanup.js');
main().catch(async (error: unknown) => {
// Set a timeout to force exit if cleanup hangs
const cleanupTimeout = setTimeout(() => {
writeToStderr('Cleanup timed out, forcing exit...\n');
process.exit(1);
}, 5000);
try {
await runExitCleanup();
} catch (cleanupError: unknown) {
writeToStderr(
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
);
} finally {
clearTimeout(cleanupTimeout);
}
if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
}
writeToStderr(errorMessage + '\n');
process.exit(error.exitCode);
}
writeToStderr('An unexpected critical error occurred:');
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
} else {
writeToStderr(String(error) + '\n');
}
process.exit(1);
});
}
}
run();

View File

@@ -1907,7 +1907,8 @@ const SETTINGS_SCHEMA = {
category: 'Advanced',
requiresRestart: true,
default: true,
description: 'Automatically configure Node.js memory limits',
description:
'Automatically configure Node.js memory limits. Note: Because memory is allocated during the initial process boot, this setting is only read from the global user settings file and ignores workspace-level overrides.',
showInDialog: true,
},
dnsResolutionOrder: {

View File

@@ -81,10 +81,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { appEvents, AppEvent } from './utils/events.js';
import { SessionError, SessionSelector } from './utils/sessionUtils.js';
import {
relaunchAppInChildProcess,
relaunchOnExitCode,
} from './utils/relaunch.js';
import { relaunchOnExitCode } from './utils/relaunch.js';
import { loadSandboxConfig } from './config/sandboxConfig.js';
import { deleteSession, listSessions } from './utils/sessions.js';
import { createPolicyUpdater } from './config/policy.js';
@@ -439,6 +436,12 @@ export async function main() {
// Set remote admin settings if returned from CCPA.
if (remoteAdminSettings) {
settings.setRemoteAdminSettings(remoteAdminSettings);
if (process.send) {
process.send({
type: 'admin-settings-update',
settings: remoteAdminSettings,
});
}
}
// Run deferred command now that we have admin settings.
@@ -496,10 +499,6 @@ export async function main() {
);
await runExitCleanup();
process.exit(ExitCodes.SUCCESS);
} else {
// Relaunch app so we always have a child process that can be internally
// restarted if needed.
await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings);
}
}