稳态工作树重扫描:Git-admin 指纹门¶
状态¶
已采用于主进程工作树解析缓存
(OrcaRuntimeService.listRepoWorktreesForResolution)。它保留了现有
30 秒新鲜度契约,适用于外部创建、删除、移动、锁定和
重新检出的工作树,同时移除了此前每 30 秒为每个已注册仓库运行的
git worktree list 子进程。
上下文¶
一条生产环境跟踪(10 个已注册仓库,3 小时 27 分钟)记录了 4,272 次
git worktree 调用和 8,663 次 Git 子进程总数。这些调用
以大约每 30.5 秒一次的全量仓库扫描到达——每个仓库每个
WORKTREE_SCAN_CACHE_TTL_MS 一次扫描。
实际使缓存过期的是什么¶
原始报告假设某个特定的终端/状态/编排 请求正在使 30 秒缓存失效。事实并非如此,而这一 区别改变了修复方式:
resolvedWorktreeCache(全量仓库快照)具有 1 秒 TTL (RESOLVED_WORKTREE_CACHE_TTL_MS)。任何以高于 1 Hz 轮询的调用方 都会重新计算快照。computeResolvedWorktrees对 每个 已注册仓库进行扇出 (src/main/runtime/orca-runtime.ts),对每个仓库调用listRepoWorktreesForResolution。- 该每仓库调用由
worktreeScanCache支撑,具有 30 秒 TTL。 当它过期时,下一次轮询会启动 shell 子进程。
因此,没有任何请求“使”30 秒缓存过期。它按实际时间过期,
之后最先到达的轮询者承担一次全量仓库 git worktree list
扇出。许多高频调用方——没有选择器的 listTerminals、
showTerminal、getWorktreePs、listManagedWorktrees、
resolveWorktreeSelector、编排权限刷新——只决定
谁 承担,而不是 多久一次。因此稳态子进程量是
repos / 30 s,与轮询速率无关,这正好是观察到的
~1 次扫描 / 30.5 秒。
Orca 内部的变更面已经是事件驱动:创建、删除、重命名、
文件夹重命名、稀疏编辑、仓库添加/更新/删除、SSH 重连,以及
混合版本远程失效都会调用
invalidateWorktreeScanCacheForRepo / invalidateResolvedWorktreeCache
(约 40 个调用点)。30 秒 TTL 存在的确切原因是:发现
在 Orca 外部 进行的工作树变更(git worktree add/remove/move/prune、
在另一个工作树中的 git checkout、对工作树目录的 rm -rf)。
这是一个文件系统问题,而文件系统可以在不使用 子进程的情况下回答它。
目标¶
- 移除稳态下周期性的全仓库
git worktree list扇出。 - 保留当前约 30 秒的发现延迟,适用于外部创建、删除、 移动、修剪、锁定/解锁和重新检出的工作树。
- 保留有界对账,使轻量探测无法观察到的任何内容仍然 收敛。
- 对 SSH 仓库、WSL 路由仓库、文件夹工作区、裸仓库, 或探测无法解析 Git 管理布局的主机,不改变任何行为。
- 失败开放:任何探测错误都必须与今天完全一致(运行真实扫描)。
非目标¶
- 更改
RESOLVED_WORKTREE_CACHE_TTL_MS或全量仓库快照形状。 - 更改面向渲染器的
worktrees:list/worktrees:listAllIPC 扫描 缓存(src/main/ipc/worktrees.ts中)(独立的 5 秒缓存,由registerWorktreeChangeInvalidator失效,不是跟踪中的轮询源)。 - 将
resolveWorktreeSelector限定到单个仓库。参见 “Rejected alternatives”。 - 为每个仓库添加文件系统监视器。
- 任何 wire/RPC/持久化模式变更。
设计¶
为本地仓库引入一个廉价、无子进程的 Git 工作树管理指纹, 并在重新运行 TTL 已过期的扫描之前查询它。
指纹输入¶
readRepoWorktreeAdminFingerprint(repoPath) 位于
src/main/runtime/repo-worktree-admin-fingerprint.ts,在不使用子进程的情况下解析仓库的 Git
公共目录(读取 .git;如果它是 gitdir: 文件,
跟随它,然后跟随其 commondir;如果 .git 不存在,则将 repoPath 视为
裸 gitdir),然后记录:
| 输入 | 捕获的外部变更 |
|---|---|
<commonDir>/worktrees 的排序条目名称 |
worktree add、worktree remove、worktree prune |
repoPath 的存在性 |
主检出被删除 |
<commonDir>/packed-refs mtime + 大小 |
其松散引用被打包时 tip 移动 |
<commonDir>/reftable mtime + 大小 |
reftable 后端下 tip 移动 |
每个检出:HEAD 内容 |
分支切换、分离(分离 oid 在 HEAD 本身中) |
| 每个检出:HEAD 所命名引用的内容 | 普通 git commit、reset 或 fetch 移动 tip |
每个条目:gitdir 内容 |
worktree move、worktree repair |
每个条目:locked 存在性 |
worktree lock / unlock |
每个条目:gitdir 命名路径的存在性 |
工作树目录被 rm -rf 删除(翻转 prunable) |
“每个检出”覆盖主工作树和每个关联工作树。读取
HEAD 所命名的引用是使普通提交可见的原因:提交会重写
refs/heads/<branch> 并保持 HEAD 不变,但会移动
git worktree list --porcelain 打印的 oid。只有当 symref 目标是
refs/ 下的相对路径时才会跟随它,因此手工编辑的 HEAD 无法将探测
引导到引用存储之外。
每个输入都是对已热 inode 的 stat、readdir 或小 readFile,
并且指纹仅依赖于仓库路径——不会传入任何先前扫描结果。
每仓库扇出限制为 8 个并发关联工作树
探测,镜像 SPARSE_CHECKOUT_DETECTION_CONCURRENCY。一个包含 10 个仓库、
每个仓库 10 个工作树的集合,每个 30 秒窗口花费几百次文件系统调用,而 10
次进程生成;生成在数量级上占主导,进程表
抖动是报告的症结。
指纹是一个以 NUL 分隔的字符串;任何读取失败都会返回 null,调用方将其视为“无法证明未发生变化”。
缓存决策¶
listRepoWorktreesForResolution 在 TTL 过期路径上增加了一个分支:
cached entry exists, same generation + runtimeKey, TTL expired
└─ probe eligible? (no connectionId, no wslDistro, fingerprint recorded)
├─ no → real scan (today's behaviour)
└─ yes → read fingerprint now
├─ null or different → real scan
├─ equal, last real scan < 5 min → extend TTL, no subprocess
└─ equal, last real scan ≥ 5 min → real scan (bounded reconcile)
WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS 为 5 分钟,与现有的 WORKTREE_SCAN_AGENT_SCRATCH_TTL_MS 先例一致。扫描失败(ok: false)的缓存结果永远不会被延长,因此短暂的 Git 失败仍会在 30 秒 TTL 上重试。
Git 版本兼容性¶
探针读取的每个路径都是 Git 磁盘布局的一部分,远早于 git-compatibility.md 中的 2.25 基线:作为目录的 .git 或 gitdir: 文件、commondir、worktrees/<name>/{HEAD,gitdir,locked}、packed-refs 以及松散 refs/。reftable 在 2.45 中引入;在旧版 Git 上它只是被 stat 为缺失,这是一个稳定值,因此无害。没有引入新的 Git 命令——此变更只是跳过其中一个。
Agent-scratch 仓库已经带有 5 分钟的扫描 TTL(WORKTREE_SCAN_AGENT_SCRATCH_TTL_MS),它等于协调间隔,因此门控对它们从不触发,其行为保持不变。
顺序¶
与扫描结果一起存储的指纹是在扫描运行之前捕获的。因此,在扫描进行期间落地的变更会使存储的指纹在构造上即为过期,下一次探针会看到差异并重新扫描。如果在扫描之后捕获,则可能让该变更被永久掩盖。
与现有失效机制的交互¶
未受影响。invalidateWorktreeScanCacheForRepo 会删除条目(包括指纹)并递增 generation,因此每个事件驱动路径仍会在下一次读取时强制真实扫描。指纹只会_延长_一个仅靠 TTL 就会刷新的条目。
新鲜度预算¶
| 变更 | 之前 | 之后 |
|---|---|---|
| Orca 发起的创建/删除/重命名/sparse/仓库编辑 | 立即(事件) | 立即(事件) |
| SSH 重连 / provider generation 递增 | 立即(事件) | 立即(事件) |
外部 worktree add/remove/move/prune/lock |
≤ 30 s | ≤ 30 s |
任意 worktree 中的外部 git checkout / commit / reset |
≤ 30 s | ≤ 30 s |
外部 rm -rf <worktree> |
≤ 30 s | ≤ 30 s |
| 外部 sparse-checkout 模式编辑 | ≤ 30 s | ≤ 5 min |
| 在文件大小相同的情况下,Packed/reftable 尖端在一个 mtime 刻度内移动 | ≤ 30 s | ≤ 5 min |
| SSH / WSL 仓库、文件夹工作区 | 不变 | 不变 |
这两处退化受协调间隔限制,并且都是 Orca 自身不会做出的变更。
主线程成本¶
扫描和探针都是异步的,因此二者在朴素意义上都不“运行在主线程上”——但它们在那里并非同样无成本。在 macOS 上测量,以 1 ms 间隔采样事件循环延迟,同时每个针对一个具有 20 个链接 worktree 的仓库运行 30 次:
| 每次调用墙钟时间 | 每次调用主线程停顿 | 最坏单次停顿 | |
|---|---|---|---|
git worktree list |
18.66 ms | 2.69 ms | 3.02 ms |
| 指纹探针 | 1.66 ms | 0.01 ms | 0.04 ms |
fs/promises 会分发到 libuv 线程池,因此探针约 99 % 的延迟在线程外。启动 Git 则不会:uv_spawn、fd 和管道设置,以及 stdout 收集和解码是真正的同步主进程工作。因此,在变更之前,10 个仓库同时刷新每轮会造成 ≈27 ms 的事件循环停顿,变更后为 ≈0.1 ms——这降低了主线程压力,而不是增加它,这就是为什么将任一侧移到 worker 线程都不会有帮助。
残余风险¶
注册在 UNC 路径(\\wsl$\...)但由本地 Windows Git 运行时执行的仓库仍会被探针检查,因为从 Orca 的角度看它不是 WSL 路由的。探针在那里是正确的,并且严格比它所替换的子进程更便宜,但其文件系统调用会跨越 9p 边界,就像现有的 sparse-checkout 探针已经做的那样。
实测效果¶
src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts 驱动所报告的稳态——10 个空闲本地仓库,一个以 1 Hz 轮询 30 个模拟分钟的调用方——并统计 git worktree list 调用次数:
每 30 分钟 git worktree list |
每小时 | |
|---|---|---|
| 仅 TTL(之前) | 600 | 1,200 |
| 指纹门控(之后) | 60 | 120 |
减少了 90 %,剩余部分是有界协调。具有真实外部活动的仓库会因指纹翻转而继续以 30 秒频率重新扫描。
外推到原始 trace 的形态(10 个仓库,3 小时 27 分钟):4,272 次 git worktree 调用将变为 ≈427 次。
被拒绝的替代方案¶
将 WORKTREE_SCAN_CACHE_TTL_MS 提高到 5 分钟。 一行代码,相同的子进程减少,但它会将_所有_外部变更延迟劣化为 5 分钟,包括常见的“我在终端中运行了 git worktree add”情况。指纹以没有该退化的方式获得相同的减少。
每个仓库的 <commonDir>/worktrees 上的 fs.watch。 延迟更低,但会为每个仓库增加持久化 watcher 句柄,会继承递归监视的平台差异,并且还需要自己的休眠/重新启用机制。现有 watcher 基础设施的范围限于工作区文件;将其扩展到 Git 管理目录是更大的变更,对于相同的稳态收益,风险状况更差。
将 resolveWorktreeSelector 限定到所属仓库。 原始需求要求这样做。listTerminals 已经通过 buildResolvedWorktreeFromId +
listKnownResolvedWorktreesForExplicitTarget 避免了针对显式 worktree id 的扇出。将其扩展到
resolveWorktreeSelector 意味着拆分运行时中扇入最高的方法(38 个调用点),并为仓库子集重建 lineage 投影——存在实质性的回归风险。一旦指纹门控落地,针对目标调用的剩余扇出成本只是一批 stats,而不是子进程,因此收益不再能证明风险合理。已作为后续事项跟踪,不包含在本次变更中。
测试计划¶
src/main/runtime/repo-worktree-admin-fingerprint.test.ts(真实临时目录、真实
git 二进制文件——模拟文件系统只会重述假设):
- 在无变化时,重复读取保持稳定
- 在以下操作后发生变化:
worktree add、worktree remove、worktree move、worktree lock、在关联 worktree 中的checkout、在主 worktree 中的checkout、任一 worktree 中的提交,以及对 worktree 目录执行rm -rf - 仍能跟踪其松散引用已被
git pack-refs打包的 tip - 关联 worktree 路径和主仓库路径产生相同的指纹
- bare 仓库通过其自身的 gitdir 解析,并且仍能跟踪
worktree add - 对于非 Git 目录和缺失路径返回
null
src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts:
- 未变化的指纹会在超过 30 秒 TTL 后抑制重新扫描并重新启用它
- 变化的指纹会在 30 秒 TTL 时重新扫描
- 5 分钟对账会在指纹未变化时强制重新扫描
notifyBranchRenamed(事件失效)仍会强制立即重新扫描null指纹(探测失败)会回退到扫描- SSH 仓库从不查询该探测
- 失败的扫描永远不会被延长
- 并发调用方共享一次探测和一次扫描
- 上述 1 Hz / 10 个仓库的工作负载测量
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 orca48/orca