Repository navigation
perf(effect-runtime): reuse a private source digest across processes - #7748
Conversation
Every short CLI process re-opened all packaged TS/JSON sources to compute the managed runtime fingerprint. On hosts that scan each open this cost 0.15-0.55 s per heartbeat, quota or status command. On POSIX, record the computed digest in the existing private per-user runtime namespace, keyed by release root, interpreter, schema and the complete source stat snapshot. A later process with the same snapshot reuses it; a key mismatch, symlink, shared namespace or untrusted record falls back to the unchanged full-byte hash and freshness recheck. Sources changed within two seconds are hashed but not recorded, and the memo never creates the namespace. Other platforms keep full reads. The bilingual TypeScript migration RFC records the trade-off and the residual metadata-preserving replacement risk. Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
|
Real-state end-to-end evidence for this head (
Limits: a single machine with a warm filesystem. This is not a cross-platform result and does not cover the failing |
JasonBuildAI
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · GPT-6.1 · OpenAI · self_reported
本次评审固定于 7748@249773887ce8c0903208f8f7308a099914010105。我阅读了 4 个改动文件、完整 base-to-head diff、运行时指纹与 locator 消费路径,复跑了相关测试和文档检查,并用 base/head 对照探针验证缓存失效竞态。结论为 REQUEST_CHANGES:跨进程缓存命中路径没有在返回前再次核对源码快照,可能返回与当前源码不一致的旧指纹。
动机
受影响的是频繁启动短生命周期 LoopX CLI 的操作者和自动化链。heartbeat-prompt、quota should-run、status、refresh-state 和 todo 每一步都会启动新的 Python 进程;这些进程都要为受管 TypeScript runtime 计算源码指纹,而旧实现会在每个新进程中重新打开约 380 个已打包的 .ts/.json 文件。作者报告在 macOS 上单个命令因此增加约 0.15–0.55 秒,读取打开成本高于哈希或 stat 成本。
修复后的可观察结果是在源码元数据未变化时复用已经计算过的摘要,避免后续进程重新打开全部源码;源码变化、release root 变化、解释器或 Python 版本变化、记录不可信时仍必须回到完整字节哈希。缓存只保存指纹,不授予执行、租约、任务完成或 runtime authority,也不改变指纹本身的算法和 locator 协议。本 PR 的边界是“复用同一源码修订的摘要”,不是改写 TypeScript runtime 的状态所有权。
正确性要求不只是“正常情况下源码改变会换 key”,还包括缓存命中路径必须像完整哈希路径一样在返回前观察到并发变化;否则调用方会把旧修订当成当前修订。下面这项 P2 正是该边界没有被保持。
改动思路
入口仍是 _runtime_fingerprint():它先通过既有 _runtime_source_snapshot() 取得有序的 (path, mtime_ns, ctime_ns, size) 元数据,再交给既有的 _runtime_fingerprint_for_snapshot()。完整哈希路径仍按顺序读取每个源码字节、更新同一 SHA-256,并在返回前重新扫描快照;快照变化时由 _RuntimeSourceChanged 和现有两次尝试循环重试。新增逻辑只是在函数入口增加一个持久化 memo,命中时直接返回记录值。
memo 放在现有 runtime 私有目录中,键由 schema、解释器、Python 版本、规范化 release root 和完整快照组成。POSIX 上才启用;读取要求父目录属于当前用户且没有 group/other 权限,记录文件通过 O_NOFOLLOW 打开,同时校验 owner、权限、schema、key 和摘要格式。写入只发生在命名空间已经存在、源码至少稳定两秒且完整哈希已经复核之后,并使用同目录临时文件和原子替换。其他平台继续逐进程完整读取。这复用了现有 runtime 生命周期和文件所有权,没有创建第二个持久化 authority,也没有引入新的 CLI 配置面。
架构判断可接受:缓存是 best-effort memo,私有目录和文件校验把不可信记录降级为完整重读,原有 locator、锁、进程和指纹算法仍是唯一 owner。问题在于 memo 命中分支过早返回,绕过了完整路径末尾的 freshness recheck;它需要留在同一个 fingerprint owner 内修复,而不是由调用方补一次判断。本 PR 的边界是“复用同一源码修订的摘要”,不是改写 TypeScript runtime 的状态所有权。
具体改动
关键代码讲解
loopx/control_plane/effect_runtime.py:89新增摘要 schema、4096 字节上限和两秒 settle 窗口。settle 防止粗粒度 ctime 在短时间重写时保持旧时间戳;窗口外的源码才写入缓存,窗口内仍可计算但不会留下跨进程记录。loopx/control_plane/effect_runtime.py:355的_source_digest_location先按 schema、sys.executable、Python 版本和规范化 root 生成固定记录路径,再把完整源码快照加入 key。这样不同解释器、不同 release root 或不同文件集合不会共享一个摘要。loopx/control_plane/effect_runtime.py:388的_read_source_digest校验父目录和文件的 POSIX 所有权/权限,拒绝 symlink、非普通文件和超过上限的读取,并只接受匹配 schema、key 且符合裸 SHA-256 格式的指纹;任一项不满足都返回None并走完整哈希。loopx/control_plane/effect_runtime.py:414的_write_source_digest不创建 runtime 目录;它只在既有私有目录内用O_EXCL | O_NOFOLLOW写 0600 临时文件并原子替换。写入失败是 best-effort,不影响本次指纹结果。loopx/control_plane/effect_runtime.py:451的_runtime_fingerprint_for_snapshot保留原有lru_cache,并在457–461先尝试持久化记录。这里命中后立即返回;完整哈希分支则在484–486重新扫描并比较快照。缓存命中没有等价的二次校验,是本次 P2 的位置。tests/control_plane/test_runtime_source_read_batching.py:114新增跨进程缓存、源码编辑/增加/删除、root 变化、不可信记录和失败不落盘等回归。现有用例在两次完整调用之间修改源码,能证明下一次快照换 key,但没有覆盖“修改发生在第一次 snapshot 之后、cache read 返回之前”的竞态。- 两份迁移 RFC 同步说明 POSIX 摘要复用、两秒 settle、不可信记录回退和 ctime/symlink 残余风险;英文与中文改动对应。
P2 finding
P2:缓存命中可能在源码变化后返回旧指纹。 触发序列是:进程取得快照 S;另一个安装/写入进程把某个源码改为 S';本进程随后读取为 S 生成的缓存记录并直接返回旧摘要。_runtime_fingerprint_for_snapshot 在缓存命中后没有再次调用 _runtime_source_snapshot,因此不会进入既有的 _RuntimeSourceChanged 重试。完整哈希路径本来会在读取全部字节后复查快照,所以 base 在同样的并发变化下会返回 S' 的摘要。
我使用确定性 base/head 对照探针复现:在首次快照返回后、缓存读取前修改一个 .ts 文件。head 只执行一次快照扫描和一次缓存读取,返回旧摘要;base 在完整哈希后的复查发现变化,重新计算并返回当前源码摘要。该窗口很短,但 release 更新、安装脚本并行运行或编辑器落盘时真实存在;错误指纹会影响 locator/runtime revision 的选择,不能仅当作性能缓存的无害误差。
最小修复是在缓存命中返回前重新扫描当前快照并与 key 对应的 snapshot 比较;不相等时抛出既有 _RuntimeSourceChanged(current),让 _runtime_fingerprint() 用新快照重试。另加一个“第一次 snapshot 后、cache read 前修改源码”的回归,要求缓存记录不得被采用。修复后应至少复跑:
python -m pytest tests/control_plane/test_runtime_source_read_batching.py tests/control_plane/test_effect_runtime_integration.py -q
uv run --extra test ruff check loopx/control_plane/effect_runtime.py tests/control_plane/test_runtime_source_read_batching.py
规格映射
规格依据是 docs/architecture/rfcs/typescript-control-plane-migration-v0.md 中“源码指纹读取已准入字节并复查新鲜度”的约束;本 PR 修改了该段并声明缓存复用边界。
| criterion_id | 要求 | 当前 head |
|---|---|---|
reuse-valid-key |
只有完整 key 匹配时才可复用摘要 | implemented;_source_digest_location 与 _read_source_digest |
freshness-recheck-on-hit |
缓存命中返回前仍须确认源码快照未变化 | not_met;_runtime_fingerprint_for_snapshot:457–461 直接返回 |
private-record |
记录必须位于私有目录,拒绝 symlink/foreign mode/损坏内容 | implemented;_private_directory、_read_source_digest |
stable-source-only |
只有稳定源码才能写入跨进程记录,失败不产生部分记录 | implemented;settle 与 _write_source_digest |
non-posix-parity |
非 POSIX 主机保持原有逐次完整读取 | implemented;_source_digest_location 在非 POSIX 返回 None |
对主干的风险
最强失败形态是缓存把旧源码摘要当作当前修订,导致同一 release 的 runtime identity 与磁盘源码不一致:调用方可能复用旧 runtime,或把本应启动新修订的请求归到旧 locator。这个竞态不会损坏缓存文件,也不是权限提升;但它破坏了本 PR 用来证明优化安全的 freshness invariant,因此属于 correctness P2,而不是只补一个测试。
现有正反例覆盖较完整:正常复用、源码编辑/增加/删除、root 变化、不可信记录、失败不落盘和缺失命名空间都有断言;完整集成模块在 Windows 上为 102 passed、9 skipped。Windows 会跳过 8 个 POSIX 缓存用例,所以本次不能把该平台的测试通过当成缓存路径的独立 qualification;作者报告 macOS 上 388 passed、1 skipped,代码审查和 base/head 探针仍独立暴露了上述竞态。
CI 侧当前有 3 个失败:Windows PowerShell 作业失败在未改动的 delegation revalidation 用例,dashboard-browser (2) 失败在既有 lens-switch 预算,checks 是聚合失败;这些路径不经过本 PR 修改的缓存代码,且 Windows 仍按设计走完整读取。它们在本地 base/head 对照中没有形成本 PR 的差分证据,故作为独立 merge-readiness hold 记录,而不是把红灯说成绿色。作者报告的性能收益和文档改动未发现第二个 blocking finding。
语义与 CI 对齐
这次改动没有新增 actor、任务义务、租约或运行时权限;新增的只是私有缓存 schema 和一个清理前的发布步骤。default_off_isolation 不适用:这不是可选功能开关,而是 POSIX 上默认启用的 memo,Windows 显式保持旧路径;因此不能从“没有 feature flag”推导出隔离性,必须依赖平台分支、失败回退和现有指纹测试。缓存命中修复后,文档中“源码变化仍触发完整哈希”的承诺才与实现一致。
我的整体评价
这是一个有价值的性能切片:它针对真实的高频短进程路径,复用既有 runtime/私有目录/指纹 owner,没有把缓存变成第二套状态系统;key、私有文件校验、settle 和失败回退都与现有协议相称,代码量也主要落在可独立验证的 I/O helper 和回归上。repository_reuse 为 reused,change_proportionality 为 proportionate;不建议扩展成第二个后台缓存服务或改写 locator 协议。
但在当前 exact head,缓存命中破坏了源码指纹最重要的 freshness invariant。先加入返回前快照复查和对应竞态回归,再在同一精确 head 重跑上述命令;在修复前我不会批准。当前 CI 的其他红项仍需单独处理,APPROVE 或缓存修复都不授予 merge authority。
English verdict: REQUEST_CHANGES — 249773887ce8c0903208f8f7308a099914010105 adds the intended private POSIX digest reuse, but a cache hit can return the old fingerprint when a source changes between the initial snapshot and the cache read because the hit path skips the full-hash path's freshness recheck; add the recheck and a deterministic race regression, then rerun the focused suites and static checks. Unrelated current CI failures remain a separate merge-readiness hold.
A persisted digest matched the caller's snapshot but skipped the full hash's freshness recheck, so a source edited between the snapshot and the record read could return the previous fingerprint. Recheck before adopting the record so the retry path observes the new topology, and add a deterministic race regression. Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
|
Addressed the P2 from review 5722942099 at head
Validation on this head (macOS):
Still open, unchanged by this commit: the existing Windows PowerShell delegation and dashboard-browser lens-budget CI failures do not touch this code path and remain a separate merge-readiness hold. @JasonBuildAI could you re-review the exact head? |
songoow
left a comment
There was a problem hiding this comment.
Reviewer: model_agent | GPT-6 | OpenAI | self_reported
REQUEST_CHANGES — 当前 required 验证有尚未归因的失败,暂缓批准。没有新确认的阻断性代码缺陷。 精确 head:6506e22e06c063f4cf2d8e6b10bf942636f20bb0。
动机 / Motivation
这项修改影响每个心跳链中反复启动短命令的 LoopX 使用者,以及维护本机 TypeScript runtime 的操作者。
此前每个新 Python 进程都会重新打开全部 runtime 源文件;现在 POSIX 主机可复用同一源快照的摘要,文件修改、增删或不可信记录仍走完整计算。
本轮已验证摘要复用、来源变化和失效回退的局部行为,上一轮 cache-hit 窗口内源码变化的问题已修复;端到端收益及当前浏览器恢复验证仍有未关闭的证据缺口。 本 PR 不改变 TypeScript 权威、权限、调度、结算或 CLI 输出合同;也不提供 Windows 加速或对异常文件系统的全内容保证。
剩余缺口:同一 fixture 的不可变基线失败、当前 head 一次通过已经完成,旧 session 的故障注入、恢复与不重复返回断言已执行。剩余最小补证是结合已保留的 CI 失败与本地配对,核对 Node/Chromium 版本、负载及恢复故障触发时序,说明这些风险相关条件是否仍会使当前 head 漏测恢复路径;只补尚未覆盖的条件和完整调用分支,不重复已经通过的场景。
改动思路 / Approach
把派生摘要留在既有 fingerprint 所有者下,比额外引入服务或安装步骤更小;freshness 检查仍是正确性条件,缓存仅减少重复 IO。 本次只交付 POSIX 私有 memo 的复用与失效;原有 locator、完整 hash、非 POSIX 行为和权限边界保留。
从调用链看,公开 CLI 需要 typed Effect 时进入 effect_runtime_request,先解析该安装的 runtime fingerprint。 输入来自规范化 release root、解释器身份、完整 path/mtime_ns/ctime_ns/size 快照以及 TS/JSON 源字节。 effect_runtime.py 保留唯一 fingerprint 和 managed-runtime 生命周期所有者;memo 只是派生缓存。 复用完整快照、字节读取与 hash、runtime 目录以及 source-change 重试;跨进程摘要弥补进程内 LRU 无法覆盖的短命令边界。没有第二个权威数据源或独立调度规则。 因此这是一项有实际消费者的有限优化,不需要增加用户的配置、确认或恢复步骤;量化的完整任务收益仍要依赖可比较的真实入口测量。
具体改动 / Changes
关键代码与合同
本轮按基线 04c5799b88f6c4cf12a224b7fa679f272e1e794e 的 docs/architecture/rfcs/typescript-control-plane-migration-v0.md 阅读既有合同,映射 7. Install, upgrade, and rollback,再核对当前实现与消费者;新增文档不充当自己的独立验收标准。完整差异共 4 个文件,+286/−8,生产、测试与文档分别检查,没有代码搬迁掩盖的第二个决策所有者。
-
_source_digest_location:构造与解释器、版本和 release 绑定的 memo 地址及 key。POSIX 外不启用,其他主机继续全量读。关键条件是只复用完全相同的 snapshot key,结果交给 _read_source_digest/_write_source_digest。 -
_read_source_digest:验证目录、文件、schema/key/digest 后才返回缓存。新读取只接受私有 regular 文件,失败回退。关键条件是O_NOFOLLOW, uid/mode, regular file, bounded JSON,结果交给 _runtime_fingerprint_for_snapshot。 -
_write_source_digest:把已验证完整 hash 作为可丢弃投影写入。2 秒稳定窗和原子 replace;不创建 namespace。关键条件是write errors do not replace source errors,结果交给 later fresh Python process。 -
_runtime_fingerprint_for_snapshot:选择 memo 或原完整字节 hash。当前 head 在 cache hit 返回前新增 unchanged snapshot 检查。关键条件是cached value never bypasses freshness recheck,结果交给 _runtime_fingerprint retry and effect_runtime_request。
英文与中文 TypeScript migration RFC 明确披露每个 POSIX 进程不再读全字节,以及 stat/ctime/同 metadata symlink 风险。 当前 head 对上一轮 cache-hit 变化窗口新增 _require_unchanged_snapshot,对应回归测试通过;此前 finding 不再作为本轮阻断。
验证记录
PASS:pytest -q tests/control_plane/test_runtime_source_read_batching.py,13 passed in 1.45s。负例覆盖:test_source_change_before_a_digest_hit_is_not_adopted PASS;旧 finding 的触发路径不再采纳旧 digest。
关联验证的完整边界:ruff check changed Python and git diff --check passed; no independent full CLI A/B timing run or Windows execution in this review.
默认 sandbox 的本地 Effect socket 权限失败已作为环境观测保留;在允许本地 socket 的独立临时环境中运行相同测试,不修改代码或降低断言。当前原始 CI 快照为 failure=2, pending=6, skipped=6, success=27,跳过不等于通过,聚合 checks failure不重复计为另一独立故障,pending也不单独构成阻断。
补充真实入口与浏览器配对
status/quota status/quota plan的空registry及unknown Goal四组,exit分别0/0/0/1;只归一化投影观测时钟和0/1秒read skew后,完整JSON一致。没有把这个小fixture作为全部有状态工作流的证明。
补充实跑(Linux / Node 24.22.1 / Chromium 138,development browser):不可变基线 04c5799b88f6c4cf12a224b7fa679f272e1e794e 复现相同 droppedRead 未触发错误,场景11.36秒;当前head同一场景18.28秒通过,真实故障注入、恢复、结果不重复及不重启模型断言均执行。这支持问题在基线也存在,但原CI当前head失败与本轮一次head成功构成有界间歇性记录;本轮环境和CI Node/Chromium不同,不据一次green声称根因已修复或完全归为无关。
对主干的风险 / Risks
当前验证缺口 — dashboard-browser (1)。 conversation-return-continuity.mjs:56: Error: The old-session recovery path was not exercised。测试最后验证 droppedRead 时失败。日志证明故障场景未被实际触发;它既不能证明 runtime memo 导致产品回归,也不能充当恢复路径已验证的证据。
最小补齐:同一 fixture 的不可变基线失败、当前 head 一次通过已经完成,旧 session 的故障注入、恢复与不重复返回断言已执行。剩余最小补证是结合已保留的 CI 失败与本地配对,核对 Node/Chromium 版本、负载及恢复故障触发时序,说明这些风险相关条件是否仍会使当前 head 漏测恢复路径;只补尚未覆盖的条件和完整调用分支,不重复已经通过的场景。 已在隔离的本地浏览器环境完成下面的有界配对;没有凭修改文件不相邻、一次成功或其他PR相似失败就将它归为无关。这里要求的是覆盖这个已观察条件的证据,不要求盲目改代码、删除断言或放宽冻结阈值。
代码层最强回归路径是:快照与 memo 读取之间源码变更,旧 digest 指向上一版本 runtime。 当前由命中分支现在执行 _require_unchanged_snapshot;既有三次重试处理 source-change。 相关局部证据为上述测试;同 size/mtime/ctime 的 symlink 重定向或不维护 ctime 的文件系统仍可能隐藏变化;作者已披露,不能将普通 POSIX 测试推广为所有文件系统保证。
P3,非阻断防御建议: 私有 memo 被人工替换为 FIFO 时,os.open(O_RDONLY) 会在 fstat 检查前等待;本轮合成探针中基线约 0.151 秒结束、当前 head 超过 2 秒仍未返回。正常 producer 只原子写 regular file,没有找到真实业务创建 FIFO 的路径,因此这不是本轮阻断理由,可在方便时用非阻塞打开加类型检查防御。
我的整体评价 / Verdict
设计边界和代码规模与重复调用成本相称,现有 owner 得到复用,局部正反例提供了有用支持。但当前失败的不变量尚未有充分归因或当前反证,本轮已完成下面有限的同fixture public-entrypoint 基线/当前head观察,但尚未覆盖完整调用分支与public-path mutation反事实,所以这些证据保持 UNVERIFIED;不把作者A/B或测试数量写成本轮已证明的长程收益。把派生摘要留在既有 fingerprint 所有者下,比额外引入服务或安装步骤更小;freshness 检查仍是正确性条件,缓存仅减少重复 IO。 本次只交付 POSIX 私有 memo 的复用与失效;原有 locator、完整 hash、非 POSIX 行为和权限边界保留。
检查了 effect_runtime.py 及其 CLI/locator 调用路径;本 PR 不修改产品页面布局、文本或交互,视觉审查不适用。 已实跑上述浏览器断言;该变更无新页面视觉设计,本轮未独立逐张审美或全页排版审查。CI与本地浏览器环境不同,不能认证其间歇性已消失。 这项 review hold 与 pending CI、merge授权分别判断;补齐具体证据后可重评同一head,无需为了review形式制造代码改动。
English verdict: REQUEST_CHANGES 6506e22e06c063f4cf2d8e6b10bf942636f20bb0. Focused code tests support the implementation, but the current dashboard-browser (1) failure remains unresolved. No new blocking production-code defect is established; assess the remaining risk-relevant CI/local differences and intermittent triggers while retaining the completed paired scenario evidence. Bounded public CLI pairs agree; complete public-path counterfactual coverage and current CI attribution remain unverified.
Delivery brief
heartbeat-prompt,quota should-run,status,refresh-state,todo) starts a fresh Python process for each step, and each step computes the managed TypeScript runtime fingerprint once..ts/.jsonsources in every process. Fresh-process measurement on macOS shows the cost is dominated byopen(), not by hashing orstat(a stat-only pass is ~1 ms). It was 0.15–0.55 s per command, and 100% of the first explore-validation cost on the measured route.loopx/control_plane/effect_runtime.py(_runtime_fingerprint_for_snapshot). This does not change TS authority, the fingerprint value, runtime locators, scheduling, settlement, defaults or output contracts.Change
On POSIX, after a successful full hash plus the existing freshness recheck, the digest is recorded in the existing private per-user runtime namespace as
source-digest-<id>.json, mode0600, written atomically.sys.executable, Python version, normalized release root and the complete(path, mtime_ns, ctime_ns, size)source snapshot. That snapshot is already computed for the in-process cache.O_NOFOLLOW, it is a regular0600-style file owned by the user, and both the key and thesha256digest validate. Otherwise the code falls back to the unchanged full read.runtime-*.json, the*.ts-effect.lock*names andstart-*.lock. Locator retirement, fixture cleanup and lock observation are therefore unaffected.The bilingual TypeScript migration RFC replaces "still reads every admitted byte" with this trade-off and its residual risk. Another file with identical size, mtime and ctime (for example a retargeted symlink), or a filesystem that does not maintain ctime, can hide a change.
Evidence
Base
04c5799b8and head were run alternately in fresh processes against one isolated private namespace, on macOS with a synthetic SQLite fixture and 0.6 s gaps between runs:_runtime_fingerprint()in a fresh process (n=12)heartbeat-prompt --thintotal (n=6)quota should-runtotal (n=6)statustotal (n=6)The fingerprint value is identical on base and head. After the run,
canaryfixture retirement completed normally, and the namespace contained only the digest record and the existing compile cache.Validation at head
249773887:tests/control_plane/test_runtime_source_read_batching.py, checked against an independent serial reference:pytestover the fingerprint, readiness, status-scan, alltest_effect_runtime_*, managed-fixture, telemetry-isolation, performance-diagnosis, local-delegation and goal-portfolio suites: 388 passed, 1 skipped.ruff checkon changed Python: passed.scripts/check_rfc_bilingual.py --base origin/main --head HEAD: passed.loopx canary premerge --from-git-diff --git-diff-base origin/main(standard tier):Not covered
loopx/**, so it needs maintainer review and merge authorization. It is not self-merged.