Files
qiming/qimingclaw/crates/agent-electron-client/docs/architecture/ACP-ENGINE-PERF-OPTIMIZATION.md

12 KiB
Raw Blame History

version, last-updated, status
version last-updated status
1.0 2026-03-07 stable

ACP 引擎性能优化说明

针对 /computer/chat 首次响应延迟约 10s 的优化引擎预热池、ACP SDK 预加载、SSE 事件缓冲。
适用引擎claude-code-acp-tsqimingcode
最后更新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 根因

  • UnifiedAgentServiceproject_id 懒惰创建 AcpEngine,每个新 project 都会触发一次进程冷启动。
  • rcoderTauri 在 init 时预启动进程并跨项目复用Electron 侧未做等效预热与复用。
  • SSEPOST /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-codeqimingcode避免「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 时的 baseConfigcreateSession() 使用 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():无客户端时,创建或取已有 bufferevents.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 ~10msqimingcode 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.tsloadAcpSdk() 定义。
  • src/main/services/computerServer.tsSSE 缓冲、回放、pushSseEventstopComputerServer 清理。

7. 单测覆盖

  • unifiedAgent.test.tsUnifiedAgentService — 引擎预热池ACP 性能优化)
    • init()loadAcpSdk 被调用
    • getOrCreateEngine 在预热完成后复用池中引擎且配置一致
    • 从池取出的引擎配置不匹配时被 destroy
    • destroy() 清空预热池且不抛
  • computerServer.test.tsComputerServer — SSE 事件缓冲
    • getSseEventBufferSize 在无缓冲时返回 0
    • 无客户端时 pushSseEvent 将事件写入缓冲
    • 缓冲条数上限 50SSE_EVENT_BUFFER_MAX

运行:npm run test:runnpm run test:coverage

8. 可选后续优化

  • SSE buffer TTL:已实现。对长期无客户端连接的 sessionId 做 buffer 过期清理30sSSE_EVENT_BUFFER_TTL_MS),在 pushSseEvent 无客户端路径中调用 pruneExpiredSseEventBuffers(),避免 Map 在极端场景下增长。
  • 预热配置扩展:若未来需按 envmcpServers 区分引擎,可在复用前增加比对,不匹配则销毁取出的预热引擎。
  • qimingcode 进程级 vs session 级 MCPOPENCODE_CONFIG_CONTENT 在进程 spawn 时注入(来自 baseConfig会话级 MCP 以 ACP newSessionmcpServers(来自 this.config,复用路径已由 updateConfig(effectiveConfig) 更新)为准。当前实现以 session 级为准,进程级仅作默认/权限等用途。

9. 日志分析与可优化点2026-03-07

9.1 首包 ensureEngine 耗时分解(典型 qimingcode 冷启动)

阶段 耗时 说明
parseCtxServers 0ms 解析请求 context_servers
syncMcpConfigToProxyAndReload ~700ms 首请求同步 MCP 到 proxy、写 DB、必要时重启 bridge同 project 后续请求已跳过12ms
ensureMemoryReady ~500ms 仅冷路径memory index dirty 时做 fileSync已通过 init 时后台预执行 ensureMemoryReadyForSession() 减少首包等待
getOrCreateEngine (engine.init) ~6s qimingcode 进程冷启动;复用预热池或同 project 复用后降至 0ms 级
同 project 后续请求 12ms 引擎复用生效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 仅发生一次。