Orca 稳态 Worktree 重扫优化:基于 Git-admin 指纹门控的免子进程缓存刷新
【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca
导读
本文深入剖析 Orca 在OrcaRuntimeService.listRepoWorktreesForResolution主进程 worktree 解析缓存上采用的一项稳态优化方案:通过读取仓库 Git 管理目录(admin 目录)的廉价文件系统指纹,在不拉起git worktree list子进程的前提下证明"工作树状态没有变化",从而在稳态下把周期性的全仓库扫描扇出替换为一批 stat/readdir/readFile。你将理解该问题的根因(并非某个请求主动"清掉"缓存)、指纹的构成与判定逻辑、代码落地细节、一致性边界与实测效果,并能基于 docs/reference/worktree-scan-fingerprint.md 与仓库源码复现整条决策链。
问题背景:稳态下的 Git 子进程风暴
设计文档记录了一个来自生产环境的 trace:10 个已注册仓库、3 小时 27 分钟的窗口内,共产生4,272 次git worktree调用、8,663 个 Git 子进程总数。这些调用以"全舰队一轮"的形式出现,周期约30.5 秒一次——即每个仓库每个WORKTREE_SCAN_CACHE_TTL_MS窗口执行一轮扫描。
最初的报告假设是"某个终端/状态/编排请求使 30 秒缓存失效",但根因分析推翻了这一假设,而这直接决定了修复方向。
到底是谁"过期"了缓存
真正的问题出在三级缓存的不同 TTL 与高频轮询者叠加(常量定义见 src/main/runtime/orca-runtime-postlude.ts):
resolvedWorktreeCache(整个舰队快照)TTL 仅为1 秒(RESOLVED_WORKTREE_CACHE_TTL_MS = 1000)。任何轮询频率高于 1 Hz 的调用方都会强制重算快照;computeResolvedWorktrees会向每一个注册仓库扇出,逐仓库调用listRepoWorktreesForResolution;- 每个仓库的调用由
worktreeScanCache支撑,其 TTL 为30 秒(WORKTREE_SCAN_CACHE_TTL_MS = 30_000,定义见 src/main/runtime/runtime-worktree-scan-cache.ts)。一旦过期,下一次轮询就不得不拉起子进程。
因此没有任何请求"主动过期"30 秒缓存——它只按墙钟时间自然过期,过期后第一个到达的轮询者就要承担整轮全仓库git worktree list扇出。那些高频调用方(无 selector 的listTerminals、showTerminal、getWorktreePs、listManagedWorktrees、resolveWorktreeSelector、编排权威刷新等)只决定"由谁来付账",而不决定"付账频率"。稳态子进程流量恒为repos / 30 s,与轮询频率无关——这正是观测到的"每 30.5 秒一轮"。
30 秒 TTL 存在的唯一理由
Orca 内部的变更面其实已经是事件驱动的:create、remove、rename、folder-rename、sparse 编辑、仓库 add/update/remove、SSH 重连、混合版本远程失效等都会调用invalidateWorktreeScanCacheForRepo/invalidateResolvedWorktreeCache(约40 个调用点)。
于是 30 秒 TTL 存在的唯一理由只剩一个:发现 Orca 之外发生的工作树变更——git worktree add/remove/move/prune、在另一个 worktree 里执行git checkout、直接用rm -rf删掉 worktree 目录等。
这本质上是文件系统问题,而文件系统完全可以在不拉起子进程的情况下回答它。这就是整篇设计的出发点。
目标与非目标
目标(完整继承自 docs/reference/worktree-scan-fingerprint.md):
- 消除稳态下周期性的全仓库
git worktree list扇出; - 保持外部创建/删除/移动/prune/lock-unlock/重新 checkout 的工作树约30 秒的发现延迟不变;
- 保留一个有界的对账(bounded reconciliation),让廉价探针观察不到的变更仍能收敛;
- 对 SSH 仓库、WSL 路由仓库、文件夹工作区、bare 仓库、以及探针无法解析 Git admin 布局的主机行为零改动;
- 故障开放(fail open):探针任何错误都必须与现状等价——即真正执行扫描。
非目标:
- 不修改
RESOLVED_WORKTREE_CACHE_TTL_MS或全舰队快照的结构; - 不动 src/main/ipc/worktrees.ts 中面向渲染进程的
worktrees:list/worktrees:listAllIPC 扫描缓存(独立 5 秒缓存,由registerWorktreeChangeInvalidator失效,并非 trace 中的轮询源); - 不把
resolveWorktreeSelector限定到单一仓库(见"被否决的备选方案"); - 不为每个仓库新增文件系统 watcher;
- 不做任何 wire/RPC/持久化 schema 变更。
指纹设计:用文件系统状态回答"有没有变"
核心思路:为本地仓库引入一个廉价、免子进程的 Git worktree admin 指纹,在 TTL 已过期的扫描路径上先读取并比对指纹,指纹未变就延续缓存、跳过子进程。
指纹的输入集合
入口函数readRepoWorktreeAdminFingerprint(repoPath)位于 src/main/runtime/repo-worktree-admin-fingerprint.ts。它先免子进程解析仓库的 Git common 目录(读取.git:若是gitdir:文件则跟随之,再解析其commondir;若.git不存在则将repoPath视为 bare gitdir),随后记录下表输入:
| 输入 | 能捕获的外部变更 |
|---|---|
<commonDir>/worktrees下排序后的条目名 | worktree add、worktree remove、worktree prune |
repoPath是否存在 | 主 checkout 被删除 |
<commonDir>/packed-refs的 mtime + size | 松散 ref 被 pack 走期间移动的 tip |
<commonDir>/reftable的 mtime + size | reftable 后端下移动的 tip |
每个 checkout 的HEAD内容 | 分支切换、detach(detached 的 oid 就写在 HEAD 里) |
| 每个 checkout 中 HEAD 所指向 ref 的内容 | 普通git commit、reset、fetch移动 tip |
每个条目gitdir的内容 | worktree move、worktree repair |
每个条目locked是否存在 | worktree lock/unlock |
gitdir指向路径是否存在 | 用rm -rf删除 worktree 目录(翻转prunable状态) |
"每个 checkout"覆盖主 worktree 与每个 linked worktree。读取 HEAD 指向的 ref tip 是让普通 commit 可见的关键:commit 重写refs/heads/<branch>而不动HEAD,却会改变git worktree list --porcelain输出的 oid。symref 目标只在它是refs/下的相对路径时才被跟随——防止手工编辑过的HEAD把探针引到 ref store 之外(源码中由isSafeRefName保证,见 src/main/runtime/repo-worktree-admin-fingerprint.ts)。
指纹的每个输入都是对"已经很热的 inode"做的一次stat、readdir或小readFile,并且指纹只依赖仓库路径——不注入任何历史扫描结果。每个仓库的并发探针上限为8(LINKED_WORKTREE_PROBE_CONCURRENCY,镜像SPARSE_CHECKOUT_DETECTION_CONCURRENCY)。一个 10 仓库 × 每仓库 10 个 worktree 的舰队,每个 30 秒窗口只花费几百次文件系统调用,而不再是 10 次进程 spawn——进程表抖动正是报告中的症状。
从实现看,指纹是一个NUL 分隔的字符串(FIELD_SEPARATOR = '\u0000',因为 NUL 既不会出现在路径也不会出现在 Git ref 中,字段边界天然无歧义;缺失字段以'-'占位)。任何读取失败都会使整体返回null,调用方把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 * 60_000(5 分钟),与既有WORKTREE_SCAN_AGENT_SCRATCH_TTL_MS先例保持一致(见 src/main/runtime/orca-runtime-postlude.ts);- 一次扫描结果若为失败(
ok: false),该缓存条目永不被延续——瞬时 Git 故障仍会按 30 秒 TTL 重试。
代码落地走读:从常量到缓存接线
探针资格判定:什么仓库可以走指纹
refreshRepoWorktreeScan(见 src/main/runtime/orca-runtime-refresh-repo-worktree-scan.ts)中,fingerprintCapable需要同时满足三个条件:
- 无 SSH connection id(注意:这里用
getRepoSshConnectionId解析执行宿主而非读裸字段——仅带executionHostId: 'ssh:*'的行同样在远端跑 Git,给它做本地指纹会去 stat 客户端路径); - 该仓库的扫描 TTL小于对账间隔(agent-scratch 仓库的 TTL 已达 5 分钟、等于对账间隔,读指纹纯属浪费,直接被排除);
- 无
wslDistro(WSL 路由仓库同样在别处执行 Git)。
SSH / WSL 仓库、文件夹工作区等原本就走真实扫描的路径完全不变。
指纹在扫描前发起:为什么顺序重要
探针在扫描之前发起(startRepoWorktreeAdminFingerprintProbe)。原因是时序上的正确性:存储在扫描结果上的指纹必须在扫描运行前捕获。如果变更发生在扫描执行期间,存储的指纹就会"结构性过期"——下一次探针比对会发现差异并重扫;而若在扫描之后捕获,那次变更会被永久掩盖,直到 5 分钟对账截止。
指纹结果是与缓存条目绑定的:真正走扫描的路径不会等待探针(冷读不能背上文件系统延迟),探针结果通过adminFingerprintProbe的 promise 在扫描完成后异步回填,见 src/main/runtime/orca-runtime-list-known-resolved-worktrees-for-explicit-target.ts 的缓存条目写入逻辑。
单飞探针与超时回退
为避免病态挂载把 libuv 的全部 fs 线程钉死(withTimeout放弃探针并不会取消它,而readdir/stat不接受 AbortSignal),实现用一个worktreeAdminFingerprintProbes集合保证每个仓库同时只有一个探针在途(见 src/main/runtime/orca-runtime-refresh-repo-worktree-scan.ts)。
探针等待还受超时约束:WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS = RESOLVED_WORKTREE_REPO_TIMEOUT_MS - WORKTREE_SCAN_FALLBACK_ALLOWANCE_MS。这里刻意"为回退预留预算而非花在探针上"——探针超时后调用方仍要在同一预算内跑git worktree list,所以回退路径需要自己的余量(WORKTREE_SCAN_FALLBACK_ALLOWANCE_MS = 1500);超时返回null,即既有的"无法证明未变化"哨兵,于是真实扫描运行。由于探针读取的是回退扫描读取集合的子集,探针慢到无法在预算内完成时,扫描本身也不会更快,等待反而更优。
缓存条目与失效语义
缓存键为${repo.id}\0${getRepoExecutionHostId(repo)}的scanScopeKey,条目携带generation、runtimeKey、result、expiresAt、adminFingerprint、scannedAt六个字段;在途扫描也按同一 scope key 去重,invalidateWorktreeScanCacheForRepo删除条目(指纹一并删除)并 bump generation(见 src/main/runtime/orca-runtime-refresh-repo-worktree-scan.ts)。
因此与既有失效机制的交互是零改动的:指纹只会在"TTL 本要刷新条目"的场景下延长该条目,而所有事件驱动的失效路径仍强制下次读取走真实扫描。以notifyBranchRenamed为例,它调用invalidateResolvedWorktreeCache()+invalidateWorktreeScanCacheForRepo(repoId)并通知渲染进程,让分支改名即刻浮出水面(见 src/main/runtime/orca-runtime-refresh-repo-worktree-scan.ts)。
Git 版本兼容性
探针读取的每条路径都是 Git 磁盘布局中远早于 2.25 基线就存在的内容(Orca 的 Git 版本基线见 docs/reference/git-compatibility.md):.git目录或gitdir:文件、commondir、worktrees/<name>/{HEAD,gitdir,locked}、packed-refs、松散refs/。
reftable在 Git 2.45 才出现;在旧版 Git 上它只是 stat 为"缺失",而缺失是一个稳定值,因此无害。本改动不引入任何新 Git 命令——它只是跳过了一条既有命令。
Agent-scratch 仓库已有 5 分钟扫描 TTL,与对账间隔相等,因此指纹门对它们永不触发、行为完全不变。
新鲜度预算:两种有界退化
下表对比改动前后各类变更的可见延迟(引自原文档):
| 变更 | 改动前 | 改动后 |
|---|---|---|
| Orca 发起的 create/remove/rename/sparse/仓库编辑 | 即时(事件) | 即时(事件) |
| SSH 重连 / provider generation bump | 即时(事件) | 即时(事件) |
外部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 |
| 同一 mtime 刻度内、文件大小不变时移动 packed/reftable tip | ≤ 30 s | ≤ 5 min |
| SSH / WSL 仓库、文件夹工作区 | 不变 | 不变 |
两种退化都被对账间隔约束,且都属于 Orca 自身不会发起的变更类型:sparse 模式编辑对指纹不可见,packed-refs/reftable 中的 tip 只能拿到 mtime+size 粗粒度戳。常量注释与 src/main/runtime/orca-runtime-postlude.ts 中的说明完全对应。
主线程成本:探针为何是净优化
扫描与探针都是异步的,都不会"朴素地跑在主线程",但二者在主线程上的开销并不相等。文档在 macOS 上以 1 ms 间隔采样事件循环延迟、对含 20 个 linked worktree 的仓库各跑 30 次测得:
| wall per call | 主线程阻塞/次 | 最坏单次阻塞 | |
|---|---|---|---|
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% 的延迟都在线程外;而uv_spawn、fd/管道建立、stdout 收集与解码是真实同步的主进程工作。10 个仓库同时刷新时,改动前每轮约27 ms的事件循环停顿,改动后约0.1 ms——这减少的是主线程压力而不是增加它,因此把任何一侧搬到 worker 线程都无济于事。
残余风险:UNC 路径
一个注册在 UNC 路径(\\wsl$\...)但由本地 Windows Git runtime 执行的仓库仍会被探针命中——因为从 Orca 的视角它并非 WSL 路由。该场景下探针是正确的、且严格比它替换的子进程更廉价,但其文件系统调用会像既有 sparse-checkout 探针一样跨过 9p 边界。
实测效果与测试证据
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 句柄、继承递归 watch 的平台差异、还须自备休眠/rearm 机制。既有 watcher 基础设施是针对工作区文件的;扩展到 Git admin 目录是更大、风险更差的改动,却只为同样的稳态收益。 - 把
resolveWorktreeSelector限定到所属仓库。原始简报曾要求该方案。listTerminals已通过buildResolvedWorktreeFromId+listKnownResolvedWorktreesForExplicitTarget为显式 worktree id 避开了扇出;把同一思路扩展到resolveWorktreeSelector意味着拆分 runtime 中扇入最高的方法(38 个调用点)并为仓库子集重建 lineage 投影——实质性回归风险。指纹门落地后,针对单仓库调用的剩余扇出成本只是一批 stat 而非子进程,收益已不再匹配风险,故作为 follow-up 而非本改动。
测试计划:指纹与门控各自分层验证
指纹本身的正确性在 src/main/runtime/repo-worktree-admin-fingerprint.test.ts 中用真实临时目录和真实git二进制验证(mock 文件系统只会复述假设):
- 无变化时多次读取结果稳定;
worktree add、worktree remove、worktree move、worktree lock、linked worktree 内checkout、主 worktreecheckout、任意一侧的 commit、rm -rfworktree 目录后指纹均变化;git pack-refs把松散 ref pack 走后仍能追踪被移动的 tip;- linked 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 仓库工作负载的上述测量。
补充一点从源码可见的工程细节:该指纹测试套件同时被列入 pr.yml 的 Windows 边界步骤(见 src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts 注释),以确保门控在 Windows 上的仓库路径处理保持诚实。
小结
这套方案把"30 秒重扫一次是否真的需要跑 Git"这一判断,从"猜(看墙钟)"升级为"查证(看文件系统)":事件驱动的失效路径继续提供毫秒级即时性,指纹探针覆盖 Orca 之外的变更发现并保留 30 秒延迟契约,5 分钟有界对账兜住指纹盲区。设计文档的关键输入表、判定树、新鲜度预算、主线程测量与实测削减数据,都可以在本仓库对应源码与测试文件中逐一找到落点,构成一条从问题 trace → 根因 → 设计 → 实现 → 验证的完整闭环。
【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考