Skip to content

SPEC: perf(workflows) — replay 指纹廉价检查前置重排(源自 #294) #295

Description

@samsen3

本 spec 由 #294 的讨论与设计共识综合而成,作为 agent 执行契约发布。来源 issue:#294(问题、价值与验证数据见该 issue)。

Problem Statement

在含 ignored 文件的仓库里——也就是所有有 node_modules、构建产物或 .env 的真实仓库——replay 机制本来就会因 ignored 检查失败而整体禁用,但每次 replay-safe 的 workflow agent() 调用(最热路径:主 agent 正在改代码、脏 diff 很大时 fan-out 的只读 explorer 子代理)仍然会为注定被丢弃的结果先跑一次全量 dirty diff(--binary,缓冲上限 32 MiB)及其余指纹命令。实测这类仓库 52% 的指纹耗时是纯浪费(32 次调用共 8568 ms,其中 4454 ms 白算),且全部同步阻塞在宿主事件循环上,最差单次约 274 ms。

Solution

把便宜的终止性检查挪到昂贵的 diff 之前:ignored 文件检查尽早失败,tracked symlink/gitlink 检查随后,之后才依次执行 HEAD 解析、全量 diff、untracked 收集与哈希。含 ignored 文件的仓库从每次 5–6 个 git 子进程降到约 2 个,完全不跑 diff。所有失败路径本就汇入同一个 catch → undefined,成功与失败路径的可观察行为均不变。

User Stories

  1. As a 在脏工作区 fan-out 只读子代理的 workflow 用户,I want replay 指纹对注定禁用 replay 的仓库尽早失败,so that 子代理启动延迟不随我的脏 diff 大小增长。
  2. As a 主 agent 正在改代码的用户,I want 每次 replay-safe 子代理调用不为注定丢弃的 diff 付费,so that fan-out 总耗时近乎减半(实测 8568 ms → 4114 ms)。
  3. As a TUI 操作员,I want 指纹失败时行为与从前完全一致(静默禁用 replay、调用照常执行),so that 这次优化不改变任何可观察行为。
  4. As a 干净仓库用户,I want replay 真正生效时指纹值与重排前逐位一致,so that 已 journal 的 replay 条目继续有效。
  5. As a 依赖事件循环响应性的宿主,I want 含 ignored 文件仓库的单次指纹耗时显著下降,so that 最差单次同步阻塞明显缩短。
  6. As a 维护者,I want 有回归测试锁死「存在 disqualifier(ignored 文件、tracked symlink/gitlink)的仓库绝不执行 diff」这一属性,so that 将来的重构不会悄悄把昂贵命令挪回前面。
  7. As a 维护者,I want git 执行器可注入且生产路径默认行为不变,so that 命令顺序可测而生产代码语义零改动。
  8. As a 维护者,I want 新增测试沿用现有指纹测试的真实临时 git 仓库 fixture 风格,so that 测试套件保持同质、易维护。
  9. As a reviewer,I want PR 附改动前后的实测基准数据与复现口径,so that 性能主张可独立验证。
  10. As a 后续要做异步化的贡献者,I want 本改动不触碰 createReplayIdentity() 的导出签名与调用点,so that 异步化仍可作为独立变更评审。
  11. As a Windows 用户,I want 优化在 Windows 10 / Node 22 / git 2.45(基准实测环境)上成立,so that 最热路径在该环境受益。
  12. As a CI 守门人,I want 全量 check 与全量测试套件在改动后保持绿色,so that 线性历史的合并前提不被破坏。
  13. As a replay journal 的下游消费者,I want 指纹的构造语义不变(相同输入收集到相同字段集合、相同哈希),so that 跨版本兼容性不受顺序重排影响。

Implementation Decisions

  • 目标模块:workflows 扩展中负责 replay 安全的模块内的指纹函数 repositoryFingerprint()(未导出的私有函数,本改动将其导出)。
  • 最终命令顺序(决定 A):仓库根解析(rev-parse --show-toplevel + realpath 规范化)→ ignored 文件检查(ls-files --others --ignored --exclude-standard --directory,非空即 throw)→ HEAD 校验 → tracked 模式检查(ls-files -s,发现 symlink/gitlink 即 throw)→ 全量 dirty diff(--binary,32 MiB 缓冲上限)→ untracked 收集(lstat 校验:symlink/非文件/总量超限即 throw;随后哈希)。
  • 顺序不变量(写进 PR Approach):所有 git 调用共享同一个 5 秒 deadline,调用集合不变则总和约束不变,git 调用之间的重排不改变任何输入下的成败;非 git 的无界工作(realpath、lstat、readFile 哈希)重排前后均全部位于 git 调用之后,不侵占 git 预算。因此对任何输入,重排前后指纹值与成败完全一致。选项 (b)(把 untracked 哈希也挪到 diff 前)被否决:它会把无界哈希工作挪进共享预算,使「重 untracked + 慢 diff」的仓库从可 replay 变为超时不可 replay,违反零行为变化承诺。
  • 测试 seam(决定 B)repositoryFingerprint(cwd, runGit?) 导出,runGit 缺省为现有绑定 deadline 的 git 执行器;createReplayIdentity() 的导出签名与行为不变。这是本 spec 唯一新增的 seam。
  • 注释随代码块移动,保留各失败路径上原有注释的语义(symlink/gitlink、ignored 文件 fail-closed 的理由注释不得丢失)。
  • 提交与交付:基于最新 origin/main 建 topic 分支;单个英文 conventional commit(perf(workflows): 前缀);ready-for-review PR,英文,遵循仓库 PR 模板五段(Problem/Value/Approach/Validation/Impact),Fixes #294,Validation 附改动前后实测数据表。
  • 基准数据:由本地基准脚本实测产出(脚本不入库,见 Out of Scope),PR 中给出环境口径(Windows 10 / Node v22.23.2 / git 2.45.1)。

Testing Decisions

  • 好测试只测外部行为:现有全部指纹/identity 测试的断言一行不改、原样通过——这层证明「任何输入结果不变」。
  • 两层测试结构:
    1. 行为层(现有 seam,最高):经 createReplayIdentity() 观察返回 identity 或 undefined 的外部行为。重排不得要求任何现有断言改动。
    2. 顺序层(新 seam,本 spec 唯一新 seam):注入记录型 runGit(包装真实 git 执行并记录参数),断言:(a) 含 ignored 文件的仓库记录中不出现 diff 命令;(b) 含 tracked symlink 的仓库记录中不出现 diff 命令;(c) 干净仓库按新顺序完整出现全部命令。命令顺序在行为层不可见(任何顺序产生相同结果),因此「性能属性」这一外部承诺只能落在此 seam 观测——这是引入新 seam 的唯一理由。
  • Prior art:现有 replay-safety 测试以真实临时 git 仓库为 fixture(mkdtemp + init + commit + 场景文件布置),新测试沿用同一风格;测试运行器为 node:test(由仓库测试脚本统一调度,--experimental-strip-types)。
  • 验证命令(仓库契约):全量 bun run check + bun run test;单文件冒烟 bun test <replay-safety 测试文件> 可作为快速反馈,但不替代全量。

Out of Scope

  • createReplayIdentity() 异步化(execFileSync 阻塞事件循环的根治):涉及导出签名、两个调用点与 workspace lease 交错语义,应独立评审;不发 follow-up issue。
  • 基准脚本入库:benchmarks/ 的定位是回归测试脚本,性能微基准保留在本地(本地已有复现脚本),只有实测数据进 PR。
  • fail-closed 语义的任何改动:ignored 文件可被只读子代理观察时仍然整体禁用 replay;本改动只解决「为注定被丢弃的工作付费」。
  • worktree-handoff 的 --binary diff(有意为之,捕获 worktree patch,每次 handoff 一次)。
  • deadline(5 秒)语义与 REPLAY_IDENTITY_TIMEOUT_MS 的任何改动。

Further Notes

  • 顺序不变量的可证明性论证(共享 deadline 预算)已与作者达成共识,须完整写进 PR 的 Approach 段。
  • 执行环境注意(本机):bun 未安装,需先安装 bun@1.3.14(钉住 packageManager 版本)并 bun install;测试套件实际运行在 Node test runner 上;git 远端操作需追加 -c http.sslBackend=schannel(本机存在 TLS 拦截代理,OpenSSL CA 链验证失败);GitHub CLI 已安装并登录(完整路径 C:\Program Files\GitHub CLI\gh.exe,不在 Git Bash PATH 中)。
  • 基线:origin/main 865f66e(2026-08-30)。已核实目标模块及其测试文件在该基线上与来源 issue 引用的 efd804d 无差异;上游另 5 个新提交不触碰目标模块。
  • 语言口径:commit 与 PR 英文(仓库惯例);来源 issue 与本 spec 为中文(作者工作语言),两者均为仓库既有先例。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions