资源/Data 重构新增耗时定位
主要结论
主要退化来自资源会话反复校验全部已观察文件,不是语言 Data 的 JSON/TOML 解析。
当前每页组件副本负载的一次主探针耗时 39.581 s,其中 ResourceSession.validate 占 38.394 s(97.0%)。共执行 7,205 次 validate、23,382,587 次已观察文件检查。
所有临时改写仅发生在 /tmp/opencode/notist-resource-profile-20261103/sources/。正式引擎没有修改,原有未提交 SSG 宿主政策及历史矩阵工作保留。
单次主探针:开销位置
当前版本负载 | CLI s | validate s | 文件版本循环 s | 文件检查次数 | 实际物理内容读取 s |
|---|---|---|---|---|---|
完整 XIV DB | 3.6648 | 0.2207 | 0.2069 | 310,435 | 0.000004 |
无模块正文 | 1.9237 | 1.4354 | 1.4351 | 951,867 | 0.001595 |
共享组件 | 6.5402 | 5.6775 | 5.6765 | 3,782,598 | 0.001350 |
每页组件副本 | 39.5810 | 38.3939 | 38.3928 | 23,382,587 | 0.016476 |
宽导出/通配绑定 | 5.3811 | 3.8862 | 3.8856 | 2,583,192 | 0.001423 |
这是完整 CLI 的单次计时探针,用于定位大项,不替代普通二进制的多轮排名。文件内容读取指 FsReader.read_bounded,不是整条源码加载链;validate 与它的文件子循环是包含关系,不能相加。
kind 检查循环在五种负载中均实际迭代零次;计时只有空循环及计数开销。此次不能将退化归因于 kind 目录探测。
为什么会反复检查
调用链中的关键位置:
src/resource_session.rs:353:source_bytes在查询源码字节缓存之前调用validate;缓存命中也要先扫全批。src/resource_session.rs:199:validate遍历全部已观察输入,对每项重新取得 revision。src/vault.rs:370:每页evaluate_in前、后各调用一次validate_inputs。crates/notist-cli/src/publication.rs:123:CLI 的ObservedReader.revision重新拼路径、加锁并向观察集合插入相同路径,然后调用底层 revision。src/resources.rs:358:FsReader.revision重新 capability-open 文件,检查普通文件类型,再读取 metadata 并构造版本。
直观地说:读第 100 个输入前,先重新检查前 99 个;读第 101 个前,再检查前 100 个。加载模块也经过这条路径。等所有页面分析完,每页求值前后又把完整集合各检查一遍。
每页组件副本的扫描分布
阶段 | validate s | 文件检查次数 |
|---|---|---|
初始物理页面读取 | 0.1990 | 135,460 |
分析/导入加载 | 28.5104 | 17,362,521 |
每页求值前后 | 9.6749 | 5,878,964 |
最终构建边界 | 0.0095 | 5,642 |
确定性计数与代码吻合:
首先读取 521 个物理页面,产生
521 × 520 / 2 = 135,460次检查。最终观察集合是 521 个页面、5,120 个模块及一个虚拟目标配置,共 5,642 项。
521 页各在求值前后检查一次,产生
2 × 521 × 5,642 = 5,878,964次检查。重构点
ef38d1b与当前083ff6c的这些检查次数逐情形完全一致。
comemo 的编译/绑定复用没有消除源码版本检查;共享图即使只实际编译 18 个模块,仍有约 378 万次文件检查。
XIV 大量页面由 prepare 作为虚拟文档提供,版本会话最终只观察两个输入(其中一个是虚拟目标配置)。人工站点至少观察 522 项。因此不能只按 HTML 页数推断资源校验成本。
为什么不是 Data 解析
五种完整负载的 data.native、实际 Data parse、Data decode/cache 调用均为零。XIV 的 Python prepare 与模板 site.data 是另一条通道,不是语言 Data API。
另用真实的 JSON/TOML 阳性样例连续调用两次组件,两个版本都测到:
4 次实际 Data 读取调用;
2 次实际解析;
2 次解析缓存命中;
非零的显式类型解码调用。
因此零计数不是探针未接线。这个小样例只验证接线与复用,不构成通用 Data 大文件性能基准。
补充细探针也显示 site.data 载入在本批只是微秒量级。正式 Data 解析器是否有其他负载瓶颈,不属于这次已观察退化的结论。
CLI 包装器的额外放大
对同一批调用进一步逐次计时,观察到:
当前共享组件图,额外细探针 | 包含式耗时 s |
|---|---|
外层 | 6.1588 |
其中路径拼接 | 0.1708 |
其中锁/路径克隆/集合维护 | 1.7859 |
内层 | 3.7310 |
其中 capability open | 2.1512 |
细探针更侵入,不能和主探针或普通 CLI 跨运行相减;父子行不能重复相加。它表明重复扫描不只增加内核 metadata 调用,也反复维护本来已经记录过的观察集合。
诊断性消融与多轮验证
同一临时二进制分别保留全检查、跳过文件循环、跳过 kind 循环、跳过取源码触发的扫描、跳过求值前后扫描,以及跳过全部重复扫描。关闭细粒度资源计数与计时,粗粒度阶段探针保持相同。
这些非 full 模式刻意削弱批次一致性,只用于归因。它们不是候选生产实现;固定输入下产物一致,也不能证明构建中修改时安全。
多轮汇总与完整范围由工具写入结果目录的 report.md、summary.json、summary.csv。普通历史二进制、全检查临时二进制与消融分别记录,不把探针秒数替代普通构建秒数。
普通历史二进制,同批三轮中位数
完整负载 | 重构前 0d81985 s | 重构点 ef38d1b s | 当前 083ff6c s |
|---|---|---|---|
XIV DB | 3.2684 | 3.5970 | 3.6414 |
无模块正文 | 0.4875 | 1.9312 | 1.9180 |
共享组件 | 1.0218 | 6.8296 | 6.5032 |
每页组件副本 | 1.0344 | 39.6486 | 39.3904 |
宽导出/通配绑定 | 3.1510 | 7.4642 | 5.3872 |
同一临时二进制:全检查与跳过重复扫描
当前完整负载 | 全检查 s | 不扫描 s | 差额 s |
|---|---|---|---|
XIV DB | 3.6757 | 3.4546 | 0.2211 |
无模块正文 | 1.9281 | 0.4659 | 1.4622 |
共享组件 | 6.5492 | 0.8047 | 5.7445 |
每页组件副本 | 39.5257 | 1.0665 | 38.4592 |
宽导出/通配绑定 | 5.3849 | 1.4676 | 3.9172 |
重构点的同二进制对照也一致:每页组件副本 39.1828 → 1.0900 s,共享组件 6.7391 → 1.0243 s,无模块正文 1.9198 → 0.4678 s。消融因此不只是证明当前某个后续优化点的偶然现象。
每页组件副本:分别省略什么
当前临时二进制模式 | 三轮中位数 s | min–max s |
|---|---|---|
全检查 | 39.5257 | 39.5182–40.4818 |
只跳过文件 revision 循环 | 1.0707 | 1.0613–1.0739 |
只跳过 kind 循环 | 39.5125 | 39.3240–39.6799 |
跳过取源码触发的全批扫描 | 10.8077 | 10.7824–10.8783 |
跳过求值前后的全批扫描 | 29.8098 | 29.6993–29.8109 |
跳过全部重复扫描 | 1.0665 | 1.0604–1.0764 |
省略取源码扫描回收约 28.7 s,省略求值边界扫描回收约 9.7 s,与独立主探针的分项量级一致;仅省略 kind 没有明显收益。这把主要增量定位到具体的文件 revision 循环,而不只是笼统归因于资源系统。
当前所有消融模式的实际编译/绑定次数也与 full 主探针一致;私有图不扫描时仍完整编译 5,120 个不同模块身份,并未跳过模块工作。
不扫描后仍可能存在共享值/求值/SSG 变更及探针框架的残余成本,例如重构点宽导出消融后为 3.5036 s,重构前为 3.1510 s。本轮没有声称扫描解释了每一微秒的差额。
独立系统调用证据
额外构造 64 详情、66 HTML 的无模块辅助站点,每种模式先完整预热,再 strace -c。它不混入 521 页主计时,追踪时的 wall 不作为性能结果。
当前二进制/模式 | openat2 | statx | close | read |
|---|---|---|---|---|
普通发布二进制 | 15,477 | 32,014 | 15,573 | 493 |
临时全检查 | 15,477 | 32,014 | 15,573 | 493 |
临时不扫描 | 198 | 1,456 | 294 | 493 |
扫描额外增加 15,279 次 openat2、30,558 次 statx、15,279 次 close,但 read 次数不变;正好对应每次版本检查的一次打开、两次元数据查询及关闭。再次印证增加的不是重读内容或解析 JSON。
验证与边界
完整 XIV 为 51,739 HTML;其余各 521 HTML。不删输入、页面、字段或正文,不修改消费者、
site.data或页面枚举模型。三个普通历史点为
0d81985、ef38d1b、083ff6c;核对二进制和源码指纹,不复用旧秒数。所有构建子进程退出后才做逐文件 SHA-256 校验,测量不与编译、探针、strace 或 HTML 校验并发。
主探针按初始读取/分析/求值/其他分组;高频文件调用只计数,额外 detail 运行才逐调用读时钟。
全检查临时副本通过资源会话、读取、路径及 Data 共 58 项 Rust 回归。
43 格各一次预热、三轮计时,共 172 次主构建全部成功,所有产物逐文件 SHA-256 与完整基准一致。另有 14 次资源探针、3 次观察包装器细探针及 5 次预热后的 strace。
工具共 22 项 Python 回归通过;
integrity.json自动核对两版本相同的扫描次数、二次增长公式及系统调用差额。CPU 8、温文件缓存、未锁频、不 fsync;RSS 为 wait4 高水位,不是进程树同时驻留总量。
本轮不作 RSS 排名:wait4 还包括从 Python 启动器 fork→exec 前继承的高水位,部分小负载出现约 165 MiB 下限。独立控制给启动器分配 256 MiB 后,连 true 子进程也报告约 271 MiB;因此原始 RSS 字段不能直接视作 Rust 自身峰值,也不能与旧批次混比。
原始记录、输入、补丁、源码与二进制指纹保存在
/tmp/opencode/notist-resource-profile-20261103。复现说明为tools/RESOURCE_OVERHEAD_PROFILE.md。dashboard.html为交互归因图,区分单次主探针与三轮同二进制消融;summary.json/summary.csv保留完整范围和样本。
后续方向
需要调整的是批次快照/版本与失效策略,避免每次缓存命中和每页求值边界都重查全部集合。不能直接提交 no-scans:要保留授权、单次读取变化检测、跨文档一致性、缺失/错误输入失效、prepared retries,以及既有构建中修改检测契约。
本轮只定位与验证,不修改正式引擎。