perf(cli): Defer TUI runtime from ACP startup - #7182
Conversation
E2E report: ACP
|
| Metric | Control P50 | Candidate P50 | P50 change | Control P95 | Candidate P95 | P95 change |
|---|---|---|---|---|---|---|
| ACP import | 115.06 ms | 52.00 ms | -63.06 ms (-54.8%) | 214.75 ms | 64.14 ms | -150.61 ms |
| Child process to response | 1102.88 ms | 1041.09 ms | -61.80 ms | 1371.30 ms | 1271.86 ms | -99.44 ms |
channel.initialize |
1098.25 ms | 1035.61 ms | -62.64 ms | 1366.75 ms | 1266.75 ms | -100.01 ms |
| Process to first Session | 2046.88 ms | 1980.03 ms | -66.85 ms | 3275.67 ms | 2871.83 ms | -403.83 ms |
| Cold Session request | 1358.95 ms | 1290.23 ms | -68.72 ms | 1649.83 ms | 1598.27 ms | -51.56 ms |
| Warm Session request | 79.78 ms | 83.15 ms | +3.36 ms | 137.53 ms | 104.98 ms | -32.55 ms |
| First health | 688.46 ms | 686.96 ms | -1.50 ms | 962.99 ms | 824.14 ms | -138.85 ms |
| Peak process-tree RSS | 430.74 MiB | 423.02 MiB | -7.73 MiB | 442.27 MiB | 435.70 MiB | -6.57 MiB |
All 120 cold profiles were complete and finite. All runs succeeded and cleaned up without residual processes.
Matched preheated result (30 pairs)
| Metric | Control P50 | Candidate P50 | P50 change | Control P95 | Candidate P95 | P95 change |
|---|---|---|---|---|---|---|
channel.initialize |
1107.68 ms | 1037.20 ms | -70.48 ms | 1469.74 ms | 1097.83 ms | -371.91 ms |
| ACP import | 115.45 ms | 51.80 ms | -63.64 ms (-55.1%) | 128.11 ms | 63.21 ms | -64.89 ms |
| Preheated Session request | 73.90 ms | 73.75 ms | -0.15 ms | 88.38 ms | 76.17 ms | -12.20 ms |
| First health | 690.62 ms | 684.69 ms | -5.92 ms | 1075.31 ms | 704.25 ms | -371.06 ms |
| Peak process-tree RSS | 430.53 MiB | 418.48 MiB | -12.04 MiB | 442.38 MiB | 430.50 MiB | -11.87 MiB |
All 60 preheated profiles were complete and finite. All runs succeeded and cleaned up without residual processes.
Functional and bundle checks
- Concurrent first Sessions: PASS in both formal rounds.
- Telemetry-disabled startup: PASS; no profile was expected or emitted.
- Legacy default
singleSession scope: PASS. - Residual processes: none.
- ACP static metafile closure: 130 outputs, 2413 inputs; Ink, React, React Reconciler, and Yoga inputs were all zero.
- Focused tests: 223 affected command/UI tests, 2 ACP Session lifecycle tests, and 19 bundle-guard tests passed.
- Build/typecheck/release bundle: PASS.
Outlier diagnosis
The first matched run had candidate parent-listener stalls of 1.49 s and 3.68 s before ACP startup; the health HTTP requests themselves took only 6-11 ms. That run's control/candidate first-health P95 was 803.82/1175.67 ms. The diagnostic retry reversed the direction because control had listener stalls, producing 1522.44/727.64 ms. All samples were retained, and the pooled 60-pair values are reported above. Compile/GC stalls also occurred on both variants, while the selected-phase P50 reproduced within 0.6 ms across the two runs.
Rejected or deferred candidates
- Lazy top-level yargs builders: rejected because selected-phase improvement did not reach 30%.
- Extension refresh, hierarchical memory, and response transport: rejected because contribution was below the 10 ms local-change threshold.
- Tool registry/warmup descriptor separation: deferred to a separate design because it exceeds P0's narrow-risk boundary.
Gate
- Selected phase P50 improvement ≥30% and ≥10 ms: PASS (-54.8%, -63.06 ms).
channel.initializeP50 improvement ≥10 ms: PASS (-62.64 ms).- Process-to-first-Session P50 improvement ≥10 ms: PASS (-66.85 ms).
- Warm/preheated/health P95 and RSS gates: PASS on the full retained matched sample.
|
Thanks for the PR! Template looks good ✓ Problem: Real and well-evidenced. The P0-A profile on issue #4748 showed Gemini/ACP module loading at 67.3% of child startup P50, and the import graph analysis demonstrated the headless ACP child was compiling Ink, React, React Reconciler, and Yoga — a TUI stack it never renders. The matched 2C4G measurements (60 cold pairs, same main commit for control and candidate) show concrete improvements: ACP import P50 -54.8%, Direction: Aligned. Startup latency is a core concern for CLI tools, and this directly tracks issue #4748 (daemon cold-start optimization). Claude Code's CHANGELOG has a comparable precedent — "Reduced binary size by ~7 MB and startup memory by ~7 MB by loading a large bundled dependency lazily instead of inlining it" — confirming this optimization pattern is relevant to the category. Size: Not applicable — no core paths ( Approach: The scope feels right — minimal, surgical changes that each directly serve the stated goal. Extract Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:真实且有充分证据。#4748 的 P0-A profile 显示 Gemini/ACP 模块加载占子进程启动 P50 的 67.3%,import graph 分析证实无头 ACP 子进程在编译一整套从不渲染的 TUI 运行时(Ink、React、React Reconciler、Yoga)。匹配 2C4G 实测(60 组交替冷启动,control 和 candidate 使用同一 main commit)显示明确改善:ACP import P50 -54.8%, 方向:对齐。启动延迟是 CLI 工具的核心关注,直接关联 #4748(daemon 冷启动优化)。Claude Code CHANGELOG 有类似先例——"通过延迟加载大型依赖而非内联,减少二进制大小约 7 MB、启动内存约 7 MB"——确认此类优化模式在该品类中合理。 规模:不适用——未触及核心路径( 方案:范围合理——最小化的精准改动,每处都直接服务于目标。将 进入代码审查 🔍 — Qwen Code · qwen3.7-max Reviewed at |
Code reviewClean diff, well-scoped. The approach matches what I'd independently propose for removing TUI modules from the ACP static closure: extract pure utilities from render-coupled modules, defer UI-only imports to dynamic Correctness — no issues found. Each change is consistent:
Security — no concerns. No new network paths, no user input handling, no secrets. Reuse check — clean. The extractions move existing code to standalone modules rather than creating parallel utilities. Tests: new test in VerificationAll 57 tests pass across 5 suites. The bundle closure check confirms zero Ink, React, React Reconciler, and Yoga inputs in the ACP agent's static import closure. Performance evidence (matched 2C4G, same main SHA, 60 cold pairs + 30 preheated):
No P95 or RSS regression on pooled cold or preheated samples. 中文说明代码审查Diff 干净、范围合理。方案与我独立提出的方案一致:将纯工具函数从渲染耦合的模块中提取出来,将仅 UI 使用的导入改为动态 正确性——未发现问题。 每处改动一致:
验证5 个测试套件共 57 个测试全部通过。Bundle 闭包检查确认 ACP agent 静态导入闭包中 Ink、React、React Reconciler、Yoga 的输入均为零。 性能证据(匹配 2C4G,同一 main SHA): ACP import P50 -54.8%, — Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 5/5 — Clean across every stage; a textbook startup optimization PR. This is the kind of PR that's easy to review and hard to find fault with. The problem is real and well-measured (67.3% of ACP startup P50 attributed to module loading that included a full TUI stack the headless child never renders). The fix is minimal and surgical — extract two data-only modules from render components, defer three UI-only imports to dynamic The backward-compatible re-exports in The matched 2C4G profiling (60 cold pairs, same main SHA for control and candidate) shows -54.8% ACP import time, -62.64 ms on LGTM. ✅ 中文说明Confidence: 5/5 — 各阶段均通过,标准的启动优化 PR。 问题真实且有充分测量(ACP 启动 P50 的 67.3% 归因于模块加载,其中包含无头子进程从不渲染的完整 TUI 运行时)。修复最小化且精准——从渲染组件中提取两个纯数据模块、延迟三个仅 UI 导入为动态
匹配 2C4G 实测(60 组冷启动,同一 main SHA)显示 ACP import -54.8%、 LGTM ✅ — Qwen Code · qwen3.7-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: Agent 0: Issue fidelity & root-cause ownership — its prompt was built, but no agent on record was launched with it. Not reviewed: Agent 4: Performance & efficiency — its prompt was built, but no agent on record was launched with it. Not reviewed: Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
— qwen3.7-max via Qwen Code /review
| export function classifyApiError(error: { | ||
| message: string; | ||
| status?: number; | ||
| }): StopFailureErrorType { |
There was a problem hiding this comment.
[Suggestion] classifyApiError was extracted into a standalone module but the tests remain in useGeminiStream.test.tsx (10,000+ lines), violating the project's colocation convention (AGENTS.md: "Tests: Collocated with source (file.test.ts next to file.ts)") — Concrete cost: a developer modifying this file won't find colocated tests and may miss running the useGeminiStream suite, risking an undetected misclassification (e.g., a 403 billing error falling through to invalid_request instead of billing_error).
| export function classifyApiError(error: { | |
| message: string; | |
| status?: number; | |
| }): StopFailureErrorType { | |
| // Move the existing `describe('classifyApiError', ...)` block | |
| // from useGeminiStream.test.tsx into: | |
| // packages/cli/src/utils/classify-api-error.test.ts |
— qwen3.7-max via Qwen Code /review
e16c7ae to
c3c8f07
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
— qwen3.7-max via Qwen Code /review
Maintainer Local Verification ReportEnvironment: macOS (Apple Silicon), Node.js v22.22.2, branch 1. Build & Bundle
2. Bundle Closure Check (Metafile Guard)✅ ACP agent static closure contains zero inputs from Ink, React, React Reconciler, and Yoga. 3. Unit Tests
4. ACP Integration Tests✅ All ACP integration tests passed, including Session lifecycle, daemon bridge, and startup-profile negotiation. 5. TypeScript Type Check6. Interactive Command VerificationTested via built CLI (
All three commands correctly load their UI dependencies on demand via dynamic import. SummaryAll verification items from the Reviewer Test Plan pass locally. The TUI runtime is successfully deferred from the ACP startup closure with no functional regression. 中文验证报告维护者本地验证报告环境: macOS (Apple Silicon), Node.js v22.22.2, 分支 1. 构建与打包
2. Bundle 闭包检查(Metafile 门禁)✅ ACP agent 静态闭包中来自 Ink、React、React Reconciler 和 Yoga 的输入均为零。 3. 单元测试
4. ACP 集成测试✅ 所有 ACP 集成测试通过,包括 Session 生命周期、daemon bridge 和 startup-profile 协商。 5. TypeScript 类型检查6. 交互命令验证通过构建后的 CLI(
三个命令均通过动态导入正确按需加载 UI 依赖。 总结Reviewer Test Plan 中的所有验证项均在本地通过。TUI 运行时已成功从 ACP 启动闭包中延迟加载,无功能回退。 |
|
@qwen-code /triage |
Local Verification — Round 2 (causal A/B + runtime-artifact equivalence)Follow-up to the earlier maintainer report at the same head
To do that I did a matched A/B release build of the PR head ( Environment: macOS (Apple Silicon), Node.js 1. The TUI actually leaves the ACP static import closure (A/B)I ran the PR's own
So the optimization removes edges that were genuinely present before, and the guard would reject the pre-PR bundle — it is not vacuously passing. 2. Repo guard is fail-closed + static-closure size deltaThe real guard passes on the PR (
−1.16 MiB (−6.2%) of JavaScript the headless ACP child no longer statically parses/compiles at startup — a concrete corroboration of the PR's "source-module compilation is the largest CPU cost" rationale behind the −54.8% ACP-import P50 result. 3. Behavior unchanged on the shipped
|
| 树 | checkAcpImportBoundary |
ACP 静态闭包 |
|---|---|---|
BASE dd4d8800(PR 前) |
❌ ok=false,5 个 offender |
acpAgent-*.js 静态导入一个 881 KB 的 chunk,内含 Ink、React(×2)、React-Reconciler、Yoga |
PR fb26377c |
✅ ok=true,0 个 offender |
ACP 静态闭包中 Ink/React/React-Reconciler/Yoga 输入为零 |
即:优化移除的是此前真实存在的边,门禁会拒绝 PR 前的 bundle —— 并非空过。
2. 门禁 fail-closed + 静态闭包体积差
真实门禁在 PR 上通过(exit 0),同一门禁指向 base metafile 时正确失败。量化从 ACP 入口可达的静态闭包(仅静态边,排除 dynamic import):
| chunk 数 | 静态闭包字节 | 含 TUI 的 chunk | |
|---|---|---|---|
| BASE | 202 | 18.75 MiB | 2 |
| PR | 198 | 17.59 MiB | 0 |
无头 ACP 子进程启动时不再静态解析/编译的 JS 减少 1.16 MiB(−6.2%) —— 对 PR 中「源码模块编译是最大 CPU 成本」这一依据、以及 −54.8% ACP-import P50 结果的直接佐证。
3. 最终 --acp 产物行为不变(运行时 A/B)
用真实外部 ClientSideConnection(@agentclientprotocol/sdk 0.14.1,stdio 上的 ndjson)分别驱动两棵树打包后的 node dist/cli.js --acp,隔离 HOME:
initialize与newSession在两者上都成功;契约完全一致 ——protocolVersion=1、相同agentCapabilities、authMethods=["openai"]、相同的 5 个 session 模式(plan, default, auto-edit, auto, yolo)。newSession创建真实 Session,从而实际执行了Session.ts—— 即classifyApiError导入被移动的那个模块 —— 证明被移动的导入在最终产物上可正常加载运行。SIGTERM干净退出,两者均无残留进程。
4. focused 测试 + 纯搬移核验
覆盖改动命令路径与被搬移分类器的 43 个 CLI 测试,加上 19 个门禁测试(含新增 3 个 ACP-boundary 用例),全绿:
classify-api-error.test.ts(18)、initCommand.test.ts(6,含新增导入失败时保留QWEN.md)、approvalModeCommand.test.ts(14,含新增auto 模式提示)、historyCommand.test.ts(5,含expand-now)、serve-fast-path-bundle-check.test.js(19)。- 两处抽取均为逐字节搬移:
classifyApiError函数体与Suggestion接口在新旧位置diff无差异,MAX_SUGGESTIONS_TO_SHOW仍为8。
commit-2 的健壮性加固也成立:approvalModeCommand 在 setApprovalMode 之前完成 dynamic import,导入失败不会留下半应用的模式;initCommand 在无法构造确认提示时返回错误并保持 QWEN.md 不动。上述均有新测试覆盖。
结论 — ✅ 可合并
核心主张已被因果证明(base 的 ACP 静态闭包携带 881 KB 的 TUI,PR 为零),门禁 fail-closed,最终 --acp 产物行为一致,所有 focused/门禁测试全绿。未见功能或协议回归。
Method note: the base tree was built with node_modules from current main; esbuild bundles each worktree's own source (confirmed in both metafiles), and Ink/React resolve from the same shared node_modules in both arms, so the diff is exactly this PR's CLI-source change. TUI-boundary and Session behavior are independent of that shared-vendor detail.
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅




What this PR does
This PR removes TUI-only Ink, React, React Reconciler, and Yoga modules from the ACP agent's static startup closure. Pure API-error and suggestion contracts no longer flow through render modules, React-only command dependencies load only when their interactive actions execute, and React types are erased at compile time. A bundle-metafile guard prevents those four packages from re-entering the ACP static closure while still allowing intentional dynamic imports.
The P0 design document now records the selected narrow optimization, the rejected alternatives, and the matched 2C4G evidence used to pass the implementation gate.
Why it's needed
The P0-A profile showed that Gemini and ACP module loading account for 67.3% of child startup P50. CPU profiles identified source-module compilation as the largest CPU cost, and import-graph analysis found that the headless ACP child was compiling an entire TUI stack it never renders. Removing those accidental static edges reduces startup work without changing the initialize barrier, Config phases, failure semantics, command availability, or Session behavior.
On a matched 2C4G comparison using the same main commit for control and candidate, 60 alternating cold pairs reduced ACP import P50 from 115.06 ms to 52.00 ms (-54.8%),
channel.initializeP50 from 1098.25 ms to 1035.61 ms (-62.64 ms), and process-to-first-Session P50 from 2046.88 ms to 1980.03 ms (-66.85 ms). Thirty preheated pairs and the pooled cold P95/RSS gates showed no regression.Reviewer Test Plan
How to verify
Build and bundle the CLI, then run the startup bundle closure check. The check should pass and the ACP agent's static metafile closure should contain zero inputs from Ink, React, React Reconciler, and Yoga.
Exercise
/initwhen a non-empty QWEN.md needs overwrite confirmation,/approval-mode auto, and/history expand-now. Each action should retain its existing UI result even though its UI-only dependency now loads on demand.Start ACP through the daemon bridge with startup-profile negotiation. Initialization, concurrent first-Session creation, telemetry-disabled startup, and the legacy default
singlescope should all succeed and clean up without residual processes; the response and Session semantics should be unchanged.Evidence (Before & After)
N/A — startup/module-graph optimization with no user-visible or TUI change. Matched 2C4G measurements are included above and in the design document.
Tested on
Environment (optional)
macOS local verification used Node.js v22.22.3 for focused Vitest, build, typecheck, release bundle, and metafile checks. Linux performance and functional verification used Node.js v22.23.1 on an x64 2-vCPU host with 3.56 GiB usable memory and no swap.
Risk & Scope
Linked Issues
Refs #4748
中文说明
本 PR 做了什么
本 PR 从 ACP agent 的静态启动闭包中移除了仅 TUI 使用的 Ink、React、React Reconciler 和 Yoga 模块。纯 API 错误分类与 suggestion 数据契约不再经过渲染模块,仅交互动作需要的 React 依赖改为动作实际执行时加载,React 类型在编译阶段完全擦除。同时新增 bundle metafile 门禁,阻止这四个包再次进入 ACP 静态闭包,并继续允许有意保留的动态导入。
P0 设计文档同步记录了最终选中的窄优化、被拒绝的候选方案,以及用于通过实现门禁的同 SHA 2C4G 实测证据。
为什么需要
P0-A profile 显示 Gemini 与 ACP 模块加载占子进程启动 P50 的 67.3%。CPU profile 进一步确认源码模块编译是最大的 CPU 成本,import graph 分析则发现无头 ACP 子进程会编译一整套它从不渲染的 TUI 运行时。移除这些意外静态边可以减少启动工作,同时不改变 initialize 屏障、Config 阶段、失败语义、命令可用性或 Session 行为。
在 control 与 candidate 使用同一个 main commit 的 2C4G 匹配对照中,60 组交替冷启动将 ACP import P50 从 115.06 ms 降至 52.00 ms(-54.8%),将
channel.initializeP50 从 1098.25 ms 降至 1035.61 ms(-62.64 ms),将 process-to-first-Session P50 从 2046.88 ms 降至 1980.03 ms(-66.85 ms)。另外 30 组 preheated 对照及合并冷启动样本的 P95/RSS 门禁均未发现回退。Reviewer 测试计划
如何验证
构建并 bundle CLI,然后运行启动 bundle 闭包检查。检查应通过,且 ACP agent 的静态 metafile 闭包中来自 Ink、React、React Reconciler 和 Yoga 的输入均应为零。
分别执行以下场景:非空 QWEN.md 触发
/init覆盖确认、执行/approval-mode auto、执行/history expand-now。虽然对应 UI-only 依赖改为按需加载,每个动作都应保持现有 UI 结果。通过 daemon bridge 启动 ACP 并协商 startup profile。初始化、并发创建首个 Session、关闭 telemetry 的启动以及旧版默认
singlescope 都应成功,并在退出后无残留进程;响应与 Session 语义应保持不变。证据(Before & After)
N/A——这是启动/module graph 优化,没有用户可见或 TUI 变化。同 SHA 2C4G 数据已列在上文和设计文档中。
测试平台
环境(可选)
macOS 本地验证使用 Node.js v22.22.3,覆盖 focused Vitest、build、typecheck、release bundle 和 metafile 检查。Linux 性能与功能验证使用 Node.js v22.23.1,运行在 x64、2 vCPU、3.56 GiB 可用内存且无 swap 的主机上。
风险与范围
关联 Issue
Refs #4748