Skip to content

perf(cli): headless startup runs interactive-only probes (IDE process walk, macOS theme) #12599

Closed

Description

@yiliang114

Summary

Follow-up to #12405 (measurements comment). On the way to its first request, a headless run still does work that only an interactive terminal can use:

  1. IDE process-tree walk. Headless and ACP runs load the built-in slash commands. Building /ide calls IdeClient.getInstance(), which runs ps -o ppid=,command= -p <pid> through /bin/sh once for each ancestor process. /ide is interactive-only (supportedModes: ['interactive']), so after paying for the walk the command is filtered out. Stack captured on 0.24.4: ideCommand → IdeClient.getInstance → getIdeProcessInfoForUnix → getProcessInfo, called from BuiltinCommandLoader.loadCommands. How many forks this costs depends on the process ancestry: 2–5 sh+ps pairs per run here.
  2. Synchronous macOS theme probe. When the theme is auto or unset, main() resolves a baseline theme with execSync('defaults read -g AppleInterfaceStyle', { timeout: 3000 }), which blocks the event loop. Output that goes to a pipe (--output-format json, automation) never renders theme colors. The interactive TUI refines the theme later with its own async OSC 11 probe, so it does not need this baseline either.

Proposal

  • Skip building /ide when the config is non-interactive, the same way it is already skipped for execution environments and tool sandboxes.
  • Resolve the synchronous auto baseline only when stdout is a terminal. The --resume picker and TUI still get it, and piped runs keep the default theme.

Out of scope / still open

  • The ~1.04 s zero-CPU wait inside Config.initialize() reported on macOS is not reproduced on Linux (the whole Config.initialize() takes 89 ms there). It needs a macOS profile of the tool_registry_created → gemini_tools_updated window before anything can be changed.
  • The resident cli-entry.js launcher (~49–58 MB RSS) is left as is. Its GB·s cost scales with run time, which the double-boot fix drops by ~48%.
中文说明

概述

#12405 的后续(实测评论)。headless 运行在发出第一个请求之前,还在做只有交互式终端才用得上的事:

  1. IDE 进程树遍历。 headless 和 ACP 运行都会加载内置斜杠命令。构造 /ide 时会调用 IdeClient.getInstance(),它对每个祖先进程都通过 /bin/sh 跑一次 ps -o ppid=,command= -p <pid>。而 /ide 只支持交互模式(supportedModes: ['interactive']),遍历的代价付完之后,这个命令还是会被过滤掉。在 0.24.4 上抓到的调用栈是 ideCommand → IdeClient.getInstance → getIdeProcessInfoForUnix → getProcessInfo,调用方是 BuiltinCommandLoader.loadCommands。要 fork 多少次取决于进程的祖先链:这里每次运行 2–5 对 sh+ps。
  2. 同步的 macOS 主题探测。 主题为 auto 或未设置时,main() 用 execSync('defaults read -g AppleInterfaceStyle', { timeout: 3000 }) 解析一个基线主题,会阻塞事件循环。输出到管道时(--output-format json、自动化场景)根本不渲染主题颜色。交互式 TUI 之后还会用自己的异步 OSC 11 探测来细化主题,所以它也用不上这个基线。

方案

  • config 为非交互时不构造 /ide,与现在对 execution environment 和工具 sandbox 的跳过方式一致。
  • 只有 stdout 是终端时才同步解析 auto 基线。--resume 选择器和 TUI 照常拿到基线,输出到管道的运行保持默认主题。

不在范围内 / 仍待解决

  • macOS 上报告的 Config.initialize() 内 ~1.04 s 零 CPU 等待在 Linux 上复现不出来(Linux 上整个 Config.initialize() 只要 89 ms)。动手之前,要先在 macOS 上 profile tool_registry_created → gemini_tools_updated 这段窗口。
  • 常驻的 cli-entry.js launcher(~49–58 MB RSS)这次不动。它的 GB·s 开销随运行时长变化,而修掉“启动两遍”之后运行时长已经降了 ~48%。

Activity

  1. doudouOUC commented on Sep 24, 2026

    @doudouOUC
    Collaborator

    Confirmed against main ca8146a. All four claims verified against the actual source.

    1. /ide building triggers the ps ancestor walk even in headless mode. ideCommand (packages/cli/src/ui/commands/ideCommand.ts:136-137) is an async function whose first line is await IdeClient.getInstance(). IdeClient.getInstance (packages/core/src/ide/ide-client.ts:120-134) eagerly calls getIdeProcessInfo() at line 124 on first resolution. getIdeProcessInfo dispatches to getIdeProcessInfoForUnix (packages/core/src/ide/process-utils.ts:49-73), which loops up to MAX_TRAVERSAL_DEPTH=32 ancestors calling getProcessInfo (line 22-42), each spawning ps -o ppid=,command= -p <pid> via execAsync (promisified exec → /bin/sh -c). When a shell ancestor is found, two additional getProcessInfo calls are made (grandparent + IDE command), so 3+ sh+ps pairs per run.

    2. /ide is supportedModes: ['interactive'] and filtered AFTER the walk. Every returned command object in ideCommand.ts carries supportedModes: ['interactive'] as const (lines 146, 164, 174, 195, 283, 308). The mode filter is applied later at nonInteractiveCliCommands.ts:501-502 via getCommandsForMode(executionMode) — after loadCommands already paid for the process walk.

    3. BuiltinCommandLoader.loadCommands is the call site. packages/cli/src/services/BuiltinCommandLoader.ts:98-108: loadCommands calls await ideCommand() at line 108 when !getExecutionEnvironment() && !getShellExecutionSandbox() — but there is NO check for non-interactive mode. BuiltinCommandLoader is included in the loader list for non-interactive paths (nonInteractiveCliCommands.ts:451-454, 780-783), and CommandService.create (packages/cli/src/services/CommandService.ts:77) calls loader.loadCommands(signal) for each loader unconditionally.

    4. Synchronous macOS theme probe runs regardless of stdout TTY. main() (packages/cli/src/llm.tsx:418) at lines 613-627: when configuredTheme is auto or unset (the else branch at line 620), themeManager.setActiveTheme(AUTO_THEME_NAME) is called at line 627 with no TTY guard. This resolves through resolveAutoTheme() → detectTerminalTheme() → detectMacOSTheme() (packages/cli/src/ui/themes/detect-terminal-theme.ts:143-153), which calls execSync('defaults read -g AppleInterfaceStyle', { timeout: 3000 }) at line 149, blocking the event loop. The async OSC 11 refinement (llm.tsx:1195-1201) IS correctly guarded by config.isInteractive() && !wasRaw && process.stdin.isTTY (line 1166), but the sync baseline is not.

    Fix direction assessment. Both proposals are sound and follow existing patterns: (a) The BuiltinCommandLoader.ts:104-107 guard already skips ideCommand() for execution environments and sandboxes — adding a non-interactive mode check is the same pattern. (b) detectOsc11Theme() already has a process.stdin.isTTY || process.stdout.isTTY guard (detect-terminal-theme.ts:102-104), so gating the sync detectMacOSTheme() path on stdout being a TTY is consistent. One minor note: the issue's stack omits the intermediary getIdeProcessInfo (process-utils.ts:175-183) that dispatches to getIdeProcessInfoForUnix/getIdeProcessInfoForWindows, but this doesn't affect the root cause.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    category/performancePerformance and optimizationpriority/P2Medium - Moderately impactful, noticeable problemscope/ideIDE integration generalscope/latencyResponse time optimizationscope/macosmacOS specific issuesscope/non-interactiveNon-interactive modescope/themesTheme system and customizationstatus/in-reviewThis issue is currently in review.type/enhancementNon-bug improvement or optimization

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions