Skip to content

Commit 0370de7

Browse files
committed
perf: skip loadCliConfig + refreshAuth in parent for non-sandbox startup
In the common non-sandbox case, the parent process now skips expensive initialization (loadCliConfig, refreshAuth) and relaunches immediately. The child process handles all initialization on its own. Previously, the parent loaded full config (extensions, hierarchical memory search, policy engine) and made 3-5 network calls (OAuth, quota, experiments, admin controls) only to discard everything when spawning the child — which then repeated the same work. This eliminates ~500-1000ms of duplicated work on every startup. The sandbox path is completely unchanged. Closes #24776
1 parent 34b4f1c commit 0370de7

1 file changed

Lines changed: 126 additions & 72 deletions

File tree

packages/cli/src/gemini.tsx

Lines changed: 126 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -348,79 +348,77 @@ export async function main() {
348348
}
349349
}
350350

351-
const partialConfig = await loadCliConfig(settings.merged, sessionId, argv, {
352-
projectHooks: settings.workspace.settings.hooks,
353-
});
354-
adminControlsListner.setConfig(partialConfig);
355-
356-
// Refresh auth to fetch remote admin settings from CCPA and before entering
357-
// the sandbox because the sandbox will interfere with the Oauth2 web
358-
// redirect.
359-
let initialAuthFailed = false;
360-
if (!settings.merged.security.auth.useExternal && !argv.isCommand) {
361-
try {
362-
if (
363-
partialConfig.isInteractive() &&
364-
settings.merged.security.auth.selectedType
365-
) {
366-
const err = validateAuthMethod(
367-
settings.merged.security.auth.selectedType,
368-
);
369-
if (err) {
370-
throw new Error(err);
371-
}
351+
// Determine memory args and sandbox config early — these are needed
352+
// regardless of whether we take the fast (relaunch) or full (sandbox) path.
353+
const memoryArgs =
354+
!process.env['SANDBOX'] &&
355+
!argv.isCommand &&
356+
settings.merged.advanced.autoConfigureMemory
357+
? getNodeMemoryArgs(isDebugMode)
358+
: [];
372359

373-
await partialConfig.refreshAuth(
374-
settings.merged.security.auth.selectedType,
375-
);
376-
} else if (!partialConfig.isInteractive()) {
377-
const authType = await validateNonInteractiveAuth(
378-
settings.merged.security.auth.selectedType,
379-
settings.merged.security.auth.useExternal,
380-
partialConfig,
381-
settings,
382-
);
383-
await partialConfig.refreshAuth(authType);
384-
}
385-
} catch (err) {
386-
if (err instanceof ValidationCancelledError) {
387-
// User cancelled verification, exit immediately.
388-
await runExitCleanup();
389-
process.exit(ExitCodes.SUCCESS);
390-
}
360+
if (!process.env['SANDBOX'] && !argv.isCommand) {
361+
const sandboxConfig = await loadSandboxConfig(settings.merged, argv);
391362

392-
// If validation is required, we don't treat it as a fatal failure.
393-
// We allow the app to start, and the React-based ValidationDialog
394-
// will handle it.
395-
if (!(err instanceof ValidationRequiredError)) {
396-
debugLogger.error('Error authenticating:', err);
397-
initialAuthFailed = true;
398-
}
399-
}
400-
}
363+
if (sandboxConfig) {
364+
// Sandbox path: needs full config + auth before entering sandbox because
365+
// the sandbox will interfere with the OAuth2 web redirect.
366+
const partialConfig = await loadCliConfig(
367+
settings.merged,
368+
sessionId,
369+
argv,
370+
{
371+
projectHooks: settings.workspace.settings.hooks,
372+
},
373+
);
374+
adminControlsListner.setConfig(partialConfig);
401375

402-
const remoteAdminSettings = partialConfig.getRemoteAdminSettings();
403-
// Set remote admin settings if returned from CCPA.
404-
if (remoteAdminSettings) {
405-
settings.setRemoteAdminSettings(remoteAdminSettings);
406-
}
376+
let initialAuthFailed = false;
377+
if (!settings.merged.security.auth.useExternal) {
378+
try {
379+
if (
380+
partialConfig.isInteractive() &&
381+
settings.merged.security.auth.selectedType
382+
) {
383+
const err = validateAuthMethod(
384+
settings.merged.security.auth.selectedType,
385+
);
386+
if (err) {
387+
throw new Error(err);
388+
}
389+
390+
await partialConfig.refreshAuth(
391+
settings.merged.security.auth.selectedType,
392+
);
393+
} else if (!partialConfig.isInteractive()) {
394+
const authType = await validateNonInteractiveAuth(
395+
settings.merged.security.auth.selectedType,
396+
settings.merged.security.auth.useExternal,
397+
partialConfig,
398+
settings,
399+
);
400+
await partialConfig.refreshAuth(authType);
401+
}
402+
} catch (err) {
403+
if (err instanceof ValidationCancelledError) {
404+
await runExitCleanup();
405+
process.exit(ExitCodes.SUCCESS);
406+
}
407407

408-
// Run deferred command now that we have admin settings.
409-
await runDeferredCommand(settings.merged);
408+
if (!(err instanceof ValidationRequiredError)) {
409+
debugLogger.error('Error authenticating:', err);
410+
initialAuthFailed = true;
411+
}
412+
}
413+
}
410414

411-
// hop into sandbox if we are outside and sandboxing is enabled
412-
if (!process.env['SANDBOX'] && !argv.isCommand) {
413-
const memoryArgs = settings.merged.advanced.autoConfigureMemory
414-
? getNodeMemoryArgs(isDebugMode)
415-
: [];
416-
const sandboxConfig = await loadSandboxConfig(settings.merged, argv);
417-
// We intentionally omit the list of extensions here because extensions
418-
// should not impact auth or setting up the sandbox.
419-
// TODO(jacobr): refactor loadCliConfig so there is a minimal version
420-
// that only initializes enough config to enable refreshAuth or find
421-
// another way to decouple refreshAuth from requiring a config.
415+
const remoteAdminSettings = partialConfig.getRemoteAdminSettings();
416+
if (remoteAdminSettings) {
417+
settings.setRemoteAdminSettings(remoteAdminSettings);
418+
}
419+
420+
await runDeferredCommand(settings.merged);
422421

423-
if (sandboxConfig) {
424422
if (initialAuthFailed) {
425423
await runExitCleanup();
426424
process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR);
@@ -442,11 +440,9 @@ export async function main() {
442440
(arg) => arg === '--prompt' || arg === '-p',
443441
);
444442
if (promptIndex > -1 && finalArgs.length > promptIndex + 1) {
445-
// If there's a prompt argument, prepend stdin to it
446443
finalArgs[promptIndex + 1] =
447444
`${stdinData}\n\n${finalArgs[promptIndex + 1]}`;
448445
} else {
449-
// If there's no prompt argument, add stdin as the prompt
450446
finalArgs.push('--prompt', stdinData);
451447
}
452448
}
@@ -461,10 +457,68 @@ export async function main() {
461457
await runExitCleanup();
462458
process.exit(ExitCodes.SUCCESS);
463459
} else {
464-
// Relaunch app so we always have a child process that can be internally
465-
// restarted if needed.
466-
await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings);
460+
// Non-sandbox path: skip loadCliConfig + refreshAuth entirely.
461+
// The child process handles all initialization on its own.
462+
// This eliminates ~500-1000ms of duplicated work (extensions loading,
463+
// hierarchical memory search, policy engine, and 3-5 network calls).
464+
await relaunchAppInChildProcess(memoryArgs, []);
465+
}
466+
} else {
467+
// Non-relaunch path (sandbox env or command mode): auth is needed here
468+
// because we won't be relaunching.
469+
const partialConfig = await loadCliConfig(
470+
settings.merged,
471+
sessionId,
472+
argv,
473+
{
474+
projectHooks: settings.workspace.settings.hooks,
475+
},
476+
);
477+
adminControlsListner.setConfig(partialConfig);
478+
479+
if (!settings.merged.security.auth.useExternal && !argv.isCommand) {
480+
try {
481+
if (
482+
partialConfig.isInteractive() &&
483+
settings.merged.security.auth.selectedType
484+
) {
485+
const err = validateAuthMethod(
486+
settings.merged.security.auth.selectedType,
487+
);
488+
if (err) {
489+
throw new Error(err);
490+
}
491+
492+
await partialConfig.refreshAuth(
493+
settings.merged.security.auth.selectedType,
494+
);
495+
} else if (!partialConfig.isInteractive()) {
496+
const authType = await validateNonInteractiveAuth(
497+
settings.merged.security.auth.selectedType,
498+
settings.merged.security.auth.useExternal,
499+
partialConfig,
500+
settings,
501+
);
502+
await partialConfig.refreshAuth(authType);
503+
}
504+
} catch (err) {
505+
if (err instanceof ValidationCancelledError) {
506+
await runExitCleanup();
507+
process.exit(ExitCodes.SUCCESS);
508+
}
509+
510+
if (!(err instanceof ValidationRequiredError)) {
511+
debugLogger.error('Error authenticating:', err);
512+
}
513+
}
467514
}
515+
516+
const remoteAdminSettings = partialConfig.getRemoteAdminSettings();
517+
if (remoteAdminSettings) {
518+
settings.setRemoteAdminSettings(remoteAdminSettings);
519+
}
520+
521+
await runDeferredCommand(settings.merged);
468522
}
469523

470524
// We are now past the logic handling potentially launching a child process

0 commit comments

Comments
 (0)