问题类别 · Category: 性能 / 远程连接 · Performance / Remote (Docker)
Agent 框架 · Framework: ZCode Agent(自研)
严重程度 · Severity: 影响体验 · Major(功能可用但连接等待极长 / works but connection takes tens of minutes)
复现频率 · Reproducibility: 必现,时长随 Docker daemon 状态波动 · Always (duration scales with daemon latency)
问题描述 · Description
Docker devcontainer 连接时的部署阶段会对容器做一连串探针(exists() / readFile()),而每一个探针都会重新解析一次容器:execDirect()(packages/server/src/remote/docker-backend.ts)每次调用 resolveContainer() → listDockerContainers({ all: true }),即执行一次完整的 docker ps -a。只有流式 exec() 走了缓存的 resolveInfo(),探针路径完全没有缓存。此外每次 exec() 前还会执行一次 docker version(ensureAvailable()),且所有 docker 元数据调用都没有超时。
一次"所有组件已是最新、全部 skip"的连接会顺序执行约 27 个这样的探针:server 部署决策 ×2(docker 后端总是要付 pre-lock + post-lock 双重检查——只有 SSH 使用 deployLockMode: "caller-serialized")、node-pty、glm 的 9 个官方插件资产 exists() + 身份 marker 读取 + 2 个 exists()、以及 bfs/ripgrep/ugrep 各 2 个探针。在 docker ps -a 较慢的主机上(本机实测 40s–3min,约 170 个容器、daemon 偶发压力时段),27 × 40s–3min = 30–45 分钟的连接等待,且中间没有任何日志输出。同一主机上 VS Code Remote 连接同一容器是秒级的——它只解析一次容器。
Every exists()/readFile() probe during the Docker devcontainer deploy re-resolves the container by running a full docker ps -a (execDirect() → resolveContainer(), uncached; only the streaming exec() path uses the cached resolveInfo()), plus a docker version before every exec(), with no timeout on any of these. A fully up-to-date no-op connection issues ~27 such probes sequentially (server decision ×2 — the docker backend always pays the pre-lock + post-lock double check — node-pty, glm's 9 plugin-asset probes + identity reads, and 2 probes × 3 runtime tools). On a host where docker ps -a takes 40s–3min, that is a 30–45 minute "startup" with skip on every line and silence in between. VS Code Remote connects to the same container instantly on the same host.
复现步骤 · Steps to reproduce
- 准备一台
docker ps -a 较慢的主机(容器数量多或 daemon 处于压力时段;用 time docker ps -a 验证,>30s 即可明显复现)/ Use a host where time docker ps -a takes tens of seconds (many containers, or daemon under pressure)
- 用 ZCode 连接一个已完整部署过的 Docker devcontainer(预期所有组件 skip)/ Connect ZCode to an already fully deployed Docker devcontainer
- 观察
~/.zcode/v2/logs/<date>.log 中相邻 [deploy] 行的时间间隔 / Watch the gaps between [deploy] log lines
实际表现 · Actual behavior
本机实测时间线(ZCode v3.14.3,全程所有步骤最终都是 skip):
13:55:48 [deploy] selected remote resource packages: server-bundle,node-runtime,node-pty,glm,bfs,ripgrep,ugrep
13:55:48 [deploy] checking if deploy needed...
13:57:43 [deploy] remote node exists: true <-- 1m55s(首个探针含 3 次 docker ps)
13:58:22 [deploy] remote server exists: true <-- +39s
13:58:22 [deploy] checking remote version... <-- 秒级(走了缓存的 resolveInfo)
13:58:23 [deploy] checking if deploy needed... <-- 整个决策第二轮
13:59:02 [deploy] remote node exists: true <-- +39s
13:59:40 [deploy] remote server exists: true <-- +38s
14:00:58 [deploy] skipped — remote version matches <-- +78s
14:01:37 [deploy] node-pty prebuild already exists, skip
14:31:39 [deploy] [zcode-agent-deploy] glm: 制品 SHA … 已部署,跳过 <-- 30 分钟,中间无任何日志
14:33:25 [deploy] [tool-deploy] bfs: 远程版本 v4.1.1-2 已是最新,跳过 <-- +1m46s
14:35:28 [deploy] [tool-deploy] ripgrep: 远程版本 v14.1.1-1 已是最新,跳过 <-- +2m03s
其中 30 分钟的静默窗口是 glm 的 skip 路径:12 个顺序探针(9 个插件资产 exists() + 身份 readFile() + 2 个 exists()),路径中不涉及任何下载 / 解压 / 哈希——纯探针往返开销。另外 exists() 会吞掉所有异常返回 false,daemon 慢或超时会把"应 skip"静默误判为"需要全量重部署"。
The silent 30-minute window is glm's skip path: 12 sequential probes, no download/extract/hash involved — pure probe latency. Note exists() swallows all errors and returns false, so a slow/failing daemon silently turns a skip decision into a full redeploy.
期望表现 · Expected behavior
- 探针类操作按连接缓存容器名(成功缓存一次;失败不缓存,devcontainer 仍在启动时可继续重试)
docker version 可用性探测同理按连接缓存
- docker 元数据调用(version / ps)设置超时,daemon 卡顿时快速、显式失败,而不是无限期静默挂起
- no-op 连接应只付 1 次
docker ps -a + 若干次 docker exec(与 VS Code Remote 同量级)
版本与环境 · Version & environment
- ZCode v3.14.3(当前公开源码版本 / current public source)
- Linux(Manjaro)桌面端,Docker 29.7.2(overlay2),主机约 170 个容器
- 连接目标:Docker devcontainer;对照:同一主机同一容器,VS Code Remote 秒级连接
附:修复方案 · Proposed fix
已基于公开源码准备了一个小补丁(2 个文件,+43/−7),要点:
execDirect() 改用按 backend 实例缓存的 resolveContainerName()(成功缓存、失败不缓存,容器启动中的重试语义不变)——单次连接从 ~27 次 docker ps -a 降到 1 次
ensureAvailable() 的 docker version 探测同样按实例缓存(成功缓存、失败可重试)
docker-detect.ts 的 version/ps 元数据调用增加 30s 超时
pnpm tsc -b packages/server 与 oxlint 已通过。如需要可提供补丁文件或 fork 分支 PR。后续可进一步优化(不在本补丁内):把 ~20 个存在性探针合并为一次 docker exec 脚本、用 docker inspect <name> 替代全量列表、docker 后端跳过 pre-lock 双重决策、exists() 区分"探针失败"与"文件不存在"。
感谢!/ Thank you!
问题类别 · Category: 性能 / 远程连接 · Performance / Remote (Docker)
Agent 框架 · Framework: ZCode Agent(自研)
严重程度 · Severity: 影响体验 · Major(功能可用但连接等待极长 / works but connection takes tens of minutes)
复现频率 · Reproducibility: 必现,时长随 Docker daemon 状态波动 · Always (duration scales with daemon latency)
问题描述 · Description
Docker devcontainer 连接时的部署阶段会对容器做一连串探针(
exists()/readFile()),而每一个探针都会重新解析一次容器:execDirect()(packages/server/src/remote/docker-backend.ts)每次调用resolveContainer()→listDockerContainers({ all: true }),即执行一次完整的docker ps -a。只有流式exec()走了缓存的resolveInfo(),探针路径完全没有缓存。此外每次exec()前还会执行一次docker version(ensureAvailable()),且所有 docker 元数据调用都没有超时。一次"所有组件已是最新、全部 skip"的连接会顺序执行约 27 个这样的探针:server 部署决策 ×2(docker 后端总是要付 pre-lock + post-lock 双重检查——只有 SSH 使用
deployLockMode: "caller-serialized")、node-pty、glm 的 9 个官方插件资产exists()+ 身份 marker 读取 + 2 个exists()、以及 bfs/ripgrep/ugrep 各 2 个探针。在docker ps -a较慢的主机上(本机实测 40s–3min,约 170 个容器、daemon 偶发压力时段),27 × 40s–3min = 30–45 分钟的连接等待,且中间没有任何日志输出。同一主机上 VS Code Remote 连接同一容器是秒级的——它只解析一次容器。Every
exists()/readFile()probe during the Docker devcontainer deploy re-resolves the container by running a fulldocker ps -a(execDirect()→resolveContainer(), uncached; only the streamingexec()path uses the cachedresolveInfo()), plus adocker versionbefore everyexec(), with no timeout on any of these. A fully up-to-date no-op connection issues ~27 such probes sequentially (server decision ×2 — the docker backend always pays the pre-lock + post-lock double check — node-pty, glm's 9 plugin-asset probes + identity reads, and 2 probes × 3 runtime tools). On a host wheredocker ps -atakes 40s–3min, that is a 30–45 minute "startup" withskipon every line and silence in between. VS Code Remote connects to the same container instantly on the same host.复现步骤 · Steps to reproduce
docker ps -a较慢的主机(容器数量多或 daemon 处于压力时段;用time docker ps -a验证,>30s 即可明显复现)/ Use a host wheretime docker ps -atakes tens of seconds (many containers, or daemon under pressure)~/.zcode/v2/logs/<date>.log中相邻[deploy]行的时间间隔 / Watch the gaps between[deploy]log lines实际表现 · Actual behavior
本机实测时间线(ZCode v3.14.3,全程所有步骤最终都是 skip):
其中 30 分钟的静默窗口是 glm 的 skip 路径:12 个顺序探针(9 个插件资产
exists()+ 身份readFile()+ 2 个exists()),路径中不涉及任何下载 / 解压 / 哈希——纯探针往返开销。另外exists()会吞掉所有异常返回false,daemon 慢或超时会把"应 skip"静默误判为"需要全量重部署"。The silent 30-minute window is glm's skip path: 12 sequential probes, no download/extract/hash involved — pure probe latency. Note
exists()swallows all errors and returnsfalse, so a slow/failing daemon silently turns a skip decision into a full redeploy.期望表现 · Expected behavior
docker version可用性探测同理按连接缓存docker ps -a+ 若干次docker exec(与 VS Code Remote 同量级)版本与环境 · Version & environment
附:修复方案 · Proposed fix
已基于公开源码准备了一个小补丁(2 个文件,+43/−7),要点:
execDirect()改用按 backend 实例缓存的resolveContainerName()(成功缓存、失败不缓存,容器启动中的重试语义不变)——单次连接从 ~27 次docker ps -a降到 1 次ensureAvailable()的docker version探测同样按实例缓存(成功缓存、失败可重试)docker-detect.ts的 version/ps 元数据调用增加 30s 超时pnpm tsc -b packages/server与oxlint已通过。如需要可提供补丁文件或 fork 分支 PR。后续可进一步优化(不在本补丁内):把 ~20 个存在性探针合并为一次docker exec脚本、用docker inspect <name>替代全量列表、docker 后端跳过 pre-lock 双重决策、exists()区分"探针失败"与"文件不存在"。感谢!/ Thank you!