跳转至

稳态工作树重扫描: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:listAll IPC 扫描 缓存(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