12 KiB
version, last-updated, status
| version | last-updated | status |
|---|---|---|
| 1.0 | 2026-03-07 | stable |
ACP 引擎性能优化说明
针对
/computer/chat首次响应延迟约 10s 的优化:引擎预热池、ACP SDK 预加载、SSE 事件缓冲。
适用引擎:claude-code-acp-ts、qimingcode。
最后更新:2026-03-07
1. 问题与根因
1.1 现象
使用 qimingcode(或 claude-code)时,新会话首次请求的 HTTP 总耗时约 7.5s,加上模型推理后用户体感接近 10s。
1.2 耗时分解(来自日志)
| 阶段 | 耗时 | 说明 |
|---|---|---|
ensureEngine |
~4.35s | 每次新 project_id 冷启动 ACP 进程(spawn + initialize handshake) |
ACP newSession |
~3.1s | 每次新 project 创建 ACP session(含 MCP 连接等) |
ACP prompt resolved |
~2.7s | 模型推理,无法通过客户端优化 |
| SSE 早期事件 | - | prompt_start 等在 SSE 连接建立前推送,前端收不到 |
1.3 根因
- UnifiedAgentService 按
project_id懒惰创建AcpEngine,每个新 project 都会触发一次进程冷启动。 - rcoder(Tauri) 在 init 时预启动进程并跨项目复用,Electron 侧未做等效预热与复用。
- SSE:
POST /computer/chat先返回session_id,前端再建GET /computer/progress/{session_id},存在时间差,早期事件在无客户端时被丢弃。
2. 优化方案概览
| 优化项 | 文件 | 效果 |
|---|---|---|
| 引擎预热池 | unifiedAgent.ts |
新 project 的 ensureEngine 从 ~4.35s 降至 ~0ms(复用预热引擎) |
| ACP SDK 预加载 | unifiedAgent.ts |
claude-code 首次 init 时 ESM 加载不再占关键路径 |
| SSE 事件缓冲回放 | computerServer.ts |
无客户端时先缓冲,SSE 连接建立后回放,避免丢失早期事件 |
3. 实现说明
3.1 引擎预热池(unifiedAgent.ts)
思路:agentService.init() 完成后,后台同时预热两种引擎(claude-code 与 qimingcode),避免「init 用 claude-code、请求用 qimingcode」时永远无法复用。池按引擎类型存储,新 project 请求到来时按 requestedEngine 取用;若存在且关键配置一致则复用,并立即再预热同类型以补充池子。
新增/修改:
- 字段:
warmEnginePool: Map<AgentEngineType, AcpEngine>(按类型各一)、warmEngineTasks: Map<AgentEngineType, Promise<void>> - startWarmingEngine(engineType):若该类型未在池中且无进行中任务、有 baseConfig,则异步执行:
new AcpEngine(engineType)→init({ ...baseConfig, engine: engineType })→ 成功则pool.set(engineType, engine),失败则 destroy。 - init():末尾调用
startWarmingEngine('claude-code')与startWarmingEngine('qimingcode'),双引擎同时预热。 - getOrCreateEngine():
- 按
requestedEngine = effectiveConfig.engine || this.engineType || 'claude-code'从池中get(requestedEngine),若有则delete取出。 - 若取到且与
effectiveConfig的 apiKey/baseUrl/model 一致,则挂事件、updateConfig(effectiveConfig)、写入engines/engineConfigs、调用startWarmingEngine(requestedEngine)补充、返回。 - 若取到但配置不一致,则对取出的引擎
removeAllListeners+destroy,避免泄漏。
- 按
- destroy():
- 先
await Promise.all([...warmEngineTasks.values()])并清空 tasks,再遍历warmEnginePool.values()并 destroy、清空池。
- 先
配置匹配:按请求的引擎类型从池中取用后,必须 apiKey 与 baseUrl 一致才复用;否则复用的进程仍是 init 时的认证,claude-code-acp-ts 会返回 "Authentication required"、内容为空。复用后调用 updateConfig(effectiveConfig) 同步 model/mcpServers 等;认证不一致时销毁预热引擎并走冷启动。
MCP 与复用:复用预热引擎时,AcpEngine 的 this.config 仍是 init 时的 baseConfig,而 createSession() 使用 this.config.mcpServers。若不复用后更新,本请求的 context_servers(已通过 ensureEngineForRequest 同步到 proxy 并写入 effectiveConfig.mcpServers)不会生效。因此复用路径中在挂事件、写入 engines 之前调用 warm.updateConfig(effectiveConfig)(AcpEngine 新增方法),使后续 chat() → createSession() 使用本请求的 MCP 配置,不影响 MCP 加载。
3.2 ACP SDK 预加载(unifiedAgent.ts)
思路:loadAcpSdk() 首次调用会动态 import('@agentclientprotocol/sdk'),在 Electron CJS 环境有一次性解析/编译开销。在 init() 末尾非阻塞调用一次,将此次开销移到应用启动阶段。
实现:在 init() 中、startWarmingEngine() 之前增加一行:
loadAcpSdk().catch(() => {});
不 await,失败静默忽略,不影响主流程。
3.3 SSE 事件缓冲回放(computerServer.ts)
思路:pushSseEvent(sessionId, eventName, data) 被调用时,若该 sessionId 尚无 SSE 客户端,则将事件写入内存缓冲;当有客户端通过 GET /computer/progress/{session_id} 连接时,先回放缓冲再正常推送新事件。
实现:
- 常量:
SSE_EVENT_BUFFER_MAX = 50,单 session 最多缓冲条数。 - 结构:
sseEventBuffers: Map<sessionId, { events: string[]; createdAt: number }>。 - pushSseEvent():无客户端时,创建或取已有 buffer,若
events.length < SSE_EVENT_BUFFER_MAX则 push 当前 payload 后 return;有客户端时逻辑不变,直接写 response。 - GET /computer/progress/{session_id}:在把
res注册到sseClients之后,若存在该 sessionId 的 buffer,则依次res.write(eventPayload)回放,然后sseEventBuffers.delete(sessionId),并打日志。 - stopComputerServer():在清空
sseClients后调用sseEventBuffers.clear()。 - Cancel 与缓冲:
POST /computer/agent/session/cancel会调用acpEngine.abortSession(sessionId),会中止 ACP 会话(停止产生 SSE 的进程);取消成功后调用clearSseEventBuffer(sessionId)清除该 session 的 SSE 缓冲,避免用户重连GET /computer/progress/{session_id}时仍回放已取消会话的旧事件。 - Stop(重启动/停止)与缓冲:
POST /computer/agent/stop会调用agentService.stopEngine(project_id),会停止该 project 的整个引擎(进程销毁,其下所有 session 终止);停止前先通过acpEngine.listSessions()取得该引擎下所有 session id,并逐一调用clearSseEventBuffer(sessionId)清除这些 session 的 SSE 缓冲,避免之后重连或新建 SSE 时仍回放旧事件。 - 客户端停止/重启所有服务:客户端通过
services.stopAll()或services.restartAll()停止/重启时,会调用serviceManager.stopAllServices()或restartAllServices(),在调用agentService.destroy()之前调用clearAllSseEventBuffers()(computerServer 导出),清空全部 SSE 事件缓冲,避免重启后前端重连仍回放旧会话事件。
4. 并发与生命周期注意点(Review 结论)
- 预热池并发:从池中取引擎必须“先 shift 再使用”;若配置不匹配,对已取出的引擎做 destroy,避免同一实例被多个请求复用或泄漏。
- destroy 顺序:先 await
warmEngineTask,再销毁warmEnginePool中所有引擎并清空池,否则任务结束时 push 进池的引擎可能未被销毁。
5. 预期效果
| 阶段 | 优化前 | 优化后 |
|---|---|---|
| ensureEngine(新 project) | ~4.35s | ~0ms(复用预热时) |
| loadAcpSdk 首次加载(claude-code) | 串在 init 内 | 预加载,已缓存 |
| ACP newSession | ~3.1s | ~10ms(qimingcode v1.1.68+ MCP 懒加载) |
| 模型推理 | ~2.7s | ~2.7s(不变) |
| HTTP 响应总时间 | ~7.5s | ~2.7s |
| SSE 早期事件 | 易丢失 | 缓冲回放,不丢失 |
同项目再次请求仍走现有 session 复用,无变更。
6. 相关文件
src/main/services/engines/unifiedAgent.ts:预热池、SDK 预加载、getOrCreateEngine 复用与 destroy 顺序。src/main/services/engines/acp/acpClient.ts:loadAcpSdk()定义。src/main/services/computerServer.ts:SSE 缓冲、回放、pushSseEvent、stopComputerServer清理。
7. 单测覆盖
- unifiedAgent.test.ts:
UnifiedAgentService — 引擎预热池(ACP 性能优化)init()后loadAcpSdk被调用getOrCreateEngine在预热完成后复用池中引擎且配置一致- 从池取出的引擎配置不匹配时被
destroy destroy()清空预热池且不抛
- computerServer.test.ts:
ComputerServer — SSE 事件缓冲getSseEventBufferSize在无缓冲时返回 0- 无客户端时
pushSseEvent将事件写入缓冲 - 缓冲条数上限 50(
SSE_EVENT_BUFFER_MAX)
运行:npm run test:run 或 npm run test:coverage。
8. 可选后续优化
- SSE buffer TTL:已实现。对长期无客户端连接的 sessionId 做 buffer 过期清理(30s,
SSE_EVENT_BUFFER_TTL_MS),在pushSseEvent无客户端路径中调用pruneExpiredSseEventBuffers(),避免 Map 在极端场景下增长。 - 预热配置扩展:若未来需按
env或mcpServers区分引擎,可在复用前增加比对,不匹配则销毁取出的预热引擎。 - qimingcode 进程级 vs session 级 MCP:
OPENCODE_CONFIG_CONTENT在进程 spawn 时注入(来自 baseConfig);会话级 MCP 以 ACPnewSession的mcpServers(来自this.config,复用路径已由updateConfig(effectiveConfig)更新)为准。当前实现以 session 级为准,进程级仅作默认/权限等用途。
9. 日志分析与可优化点(2026-03-07)
9.1 首包 ensureEngine 耗时分解(典型 qimingcode 冷启动)
| 阶段 | 耗时 | 说明 |
|---|---|---|
| parseCtxServers | 0ms | 解析请求 context_servers |
| syncMcpConfigToProxyAndReload | ~700ms | 首请求同步 MCP 到 proxy、写 DB、必要时重启 bridge;同 project 后续请求已跳过(1–2ms) |
| ensureMemoryReady | ~500ms | 仅冷路径:memory index dirty 时做 fileSync;已通过 init 时后台预执行 ensureMemoryReadyForSession() 减少首包等待 |
| getOrCreateEngine (engine.init) | ~6s | qimingcode 进程冷启动;复用预热池或同 project 复用后降至 0ms 级 |
| 同 project 后续请求 | 1–2ms | 引擎复用生效,ensureEngine 仅做 getEngineForProject + 早期返回 |
9.2 已做优化
- init 时后台预做 memory 同步:
memoryService.ensureMemoryReadyForSession().catch(() => {}),首包 getOrCreateEngine 时 ensureMemoryReady 多为快路径(index 已同步),减少约 500ms。 - 同 project 不误判配置变更:已有引擎且引擎类型未切换时直接复用,避免 detectConfigChange 误判导致每次重建。
- session_id / project_id 查找:
getEngineForProject(engineKey)支持按 session_id 命中「key=project_id 但含该 session」的引擎,同一会话续传复用。
9.3 仍可考虑的优化(未实现)
- syncMcp 首包 ~700ms:当前首请求必须走一次同步;若多 project 共用相同 context_servers,可做「上次同步配置 hash」缓存,相同则跳过 sync,降低多 project 首包成本。
- 预热池命中率:若首请求为 qimingcode 而 qimingcode 预热未就绪(约 6s),仍会冷启动;可考虑在 UI 侧延迟首条 chat 或提示「引擎准备中」,或接受首包 6s 仅发生一次。