Notist
On this page
  1. 三组优化合并后的整站热点评估:01649b4
  2. 结论
  3. 方法与证据
  4. 新旧交错对照:总体收益
  5. 过程热点:现在在哪个阶段耗时
  6. 操作热点:剩余 CPU 成本
  7. 分配器占比仍高,不表示分配优化无效
  8. 路径查找改善,但宿主比较仍有成本
  9. 遍历并没有消失,也没有因为换名而漏统计
  10. 原热点是否被消除:分配证据
  11. 还在大量创建什么
  12. 下一轮优先级
  13. 1. 研究内容物化的中间容器,而不是继续修已消除的冗余
  14. 2. 针对剩余宿主路径查找,缩小重复工作范围
  15. 3. 正文阶段的遍历与 HTML 缓冲区,可做独立小实验
  16. 暂不优先
  17. 限制与复现

三组优化合并后的整站热点评估:01649b4

结论

冻结提交 01649b4,对照 9a3c5f7。求值分配、路径复用、遍历游标三组优化合并后:

  • 五组交错对照全部更快,配对 wall 变化中位数为 -8.57%;两组 wall 中位数为 8.254/7.555 秒。

  • malloc 减少 29,764,482 次,即 22.42%;realloc 减少 4,644,120 次,即 31.54%。

  • 三种成功分配 API 调用合计减少 34,408,602 次,即 23.33%。

  • 峰值 RSS 基本不变,仍约 2.70 GiB。减少累计分配不等于减少峰值存活内存。

  • 当前第一过程热点仍是求值,第二是正文处理;对象构造/释放仍是主要操作成本,但已不是原先那些明显冗余构造。

下一步应优先研究“内容计划到最终节点之间的中间容器与重复物化”,其次是宿主有序路径索引的剩余重复工作。不应继续把已经修复的空 native 作用域、临时节点壳、字段键 String 中转、浅树遍历堆栈和路径逐分量扩容列为待修问题。本轮仅测量、分析和新增报告,没有修改生产代码或实施新的优化。

方法与证据

  • 当前提交:01649b4;基线:9a3c5f7bdaeaa8eca31b6160f140a8928704322c。

  • 同一冻结站点:51,739 页,78,004 项产物,共 451,812,631 字节。

  • git archive 冻结源码;普通 release、frame-pointer、coarse、detail 各用独立 Cargo target,编译并行度 2。

  • 复制独立站点输入和输出目录,不修改 consumer 或旧实验目录。

  • 当前版本 16 次完整构建:首次发布、五次普通计时、两次 frame-pointer 控制、三次 coarse、两次 detail、两次 CPU、一次分配。

  • 另做新旧各一次对照预热、五组交错普通计时,以及一次基线分配诊断;共 29 次完整构建。

  • 每次计时结束后核对所有产物长度及 SHA-256,29 次全部与冻结基准一致;最终再验证源码、二进制、输入和输出。

  • 普通 A/B 均无探针,复用已验证的基线二进制;每对交替运行顺序,同一独立站点路径和已有产物条件。

  • CPU 为 499 Hz 的父进程用户空间调用栈采样,两次共 7,530 个样本,全部含目标二进制帧。不是包含扩展子进程的 wall 归因,也不是精确 off-CPU 分解。

  • 分配包装器调用原 glibc 实现,仅统计指定父进程成功 malloc/calloc/realloc,不统计扩展子进程及未拦截入口。当前概率分配栈 89,606 条,记录丢失 0;1 条未找到应用帧,落在标准库线程栈 guard 初始化。

  • Python profiling 工具回归 71 项通过。没有在本轮重新运行全部 Rust 功能回归;生产变更的各组回归记录见相应日期日志。

证据目录:/tmp/opencode/notist-hotspots-01649b4-20261109。主要文件:manifest.json、paired-samples.json、paired-summary.json、samples.json、phase-summary.json、operation-summary.json、refresh-operations.json、allocation-focused.json、allocation-stacks.attribution.json,以及原始 CPU profile、分配二进制、符号缓存和 logs。工具副本在 reproducer;其中 analyze_refresh.py 补充新旧遍历表示的匹配和分配器调用上下文统计,不覆盖原始分析结果。

新旧交错对照:总体收益

指标,中位数

基线 9a3c5f7

当前 01649b4

wall

8.254 s

7.555 s

user CPU

7.545 s

6.859 s

system CPU

0.961 s

0.956 s

maxrss

2,832,736 KiB

2,831,784 KiB

五对 wall 变化:-8.50%、-8.62%、-8.08%、-8.86%、-8.57%。报告配对比值中位数 -8.57%,不将其与两组中位数之比混称;这只是本机该完整站点的一轮对照,不是跨机器置信保证。各优化的单项实验百分比不能相加,本轮只能归因于三组优化的合并效果。

当前版本单独五次普通计时为 12.244、11.337、7.579、8.140、7.858 秒,中位数 8.140 秒,全部保留。它们在编译后早期有明显波动,部分慢运行出现 major fault,但没有足够证据把全部差异归给缺页或 I/O。因此不直接拿上轮 8.407 秒与这组 8.140 秒作收益结论,而使用随后更稳定的新旧交错对照。

首轮向空产物目录发布为 10.384 秒,不纳入上述统计,也不是严格清空系统缓存后的冷启动实验。其余普通构建有完整产物,仍重新执行整站分析和求值,发布主要核对产物,不是编辑后的增量构建。

过程热点:现在在哪个阶段耗时

coarse 三次中位数;比例分母为同组 cli.total 的 7.517 秒,不与普通对照分母混算。

过程

wall

占阶段总时间

文档求值

2.686 s

35.7%

正文链接/嵌入、HTML 及阶段回收

1.638 s

21.8%

文档分析及宿主环境装配

0.917 s

12.2%

资源准备、复制及模板加载

0.746 s

9.9%

发布

0.440 s

5.9%

扩展与路由

0.296 s

3.9%

公共模板上下文

0.139 s

1.8%

最终页面模板阶段

0.069 s

0.9%

求值与正文仍占约 57.5%,没有出现新的单一主导阶段。上轮记录中的求值/正文约为 3.071/1.886 秒,本次为 2.686/1.638 秒,方向与总体改善一致;这些阶段不是新旧交错探针实验,不能将差值当作精确的逐项优化归因。发布中的 unchanged 比较约 0.386 秒;扩展进程执行及等待约 0.201 秒。表未列全装配、目录和返回边界;各项中位数也不保证严格相加。

coarse 外部 wall 中位数为 7.587 秒,detail 为 9.819 秒;detail 明显扰动求值,不用它给真实构建分配精确秒数。frame-pointer 两次为 8.245/7.612 秒,也有波动,因此不声称已精确测出探针的固定开销。

操作热点:剩余 CPU 成本

self 仅计最内层执行位置;inclusive 含被调用工作。各类别相互包含,不能相加,也不能直接换算为可消除的 wall 秒数。

操作/调用路径

两次 CPU 样本比例

glibc 分配/释放/扩容自身

26.2~26.9% self

文档求值 run

35.6~36.1% inclusive

present 内容物化

33.6~33.8% inclusive

execute

23.8~24.2% inclusive

expr

12.9~13.8% inclusive

std::path::compare_components

9.10~9.17% inclusive

HTML render_with_diagnostics

7.6~7.7% inclusive

Dict::insert

2.8~3.0% inclusive

新遍历器及旧入口合并识别

2.5~3.0% inclusive

implementation 查找

2.2~2.6% inclusive

memcpy/memmove 自身

2.4~2.6% self

HTML escape::write

约 1.9% inclusive

来源引用校验

约 1.7% inclusive

实例/编译期值 key

1.2~1.3% inclusive

分配器占比仍高,不表示分配优化无效

总体执行时间和分配次数都下降后,分配器在更小总量中的比例仍可保持较高。历史采样的分配器 self 为 27.0~28.2%,当前为 26.2~26.9%;不能只看百分比得出“没优化成功”。当前分配器 self 的调用上下文按互斥规则划分:

  • 求值 run 栈内:占全部 CPU 样本约 10.0~10.2%。

  • HTML renderer 栈内:约 2.6~3.0%。

  • 其余上下文:约 13.2~14.1%,包括宿主构建/资源/发布及不在前两者栈内的对象销毁。

这三项仅划分分配器叶子样本,不能把其余全部称为最终析构,也不能把 26% 都归给 evaluator。

路径查找改善,但宿主比较仍有成本

当前约 9.1% 的路径比较主要仍来自 Vault::ensure_environment、Assets::insert、Files::inspect、ResourceSession::register_source、资源准备和 publication::unchanged。DocumentIndex::find 不再出现在两次路径比较的主要调用者列表,符合精确索引改为 HashMap 的改动。这不表示文档索引零成本:哈希与相等判断仍执行,只是不再走原来的有序路径比较热点。剩余 ordered map 有输出顺序和冲突检测职责,不能将所有容器机械改为 HashMap。

遍历并没有消失,也没有因为换名而漏统计

原工具只识别 Node::descendants,当前主要迭代工作进入 Descendants::next。补充分析同时匹配新 next 实现和旧入口/闭包,只识别真实函数帧,不把泛型参数提及该类型算进去;当前仍约 2.5~3.0% CPU。历史采样为约 3.6~4.4%,是不同时间的诊断对照,不能承诺精确的绝对耗时下降比例。已解决浅树堆栈分配,不等于消除了多次扫描本身。

原热点是否被消除:分配证据

当前和重新运行的基线均使用 frame-pointer 二进制、相同包装器及已有完整产物条件。分配探针的耗时不进入普通 A/B。

成功 API 调用,父进程精确计数

基线

当前

减少

malloc

132,729,790

102,965,308

22.42%

calloc

37

37

0

realloc

14,726,628

10,082,508

31.54%

合计

147,456,455

113,047,853

23.33%

概率调用栈中的对应证据:

  • 共享 Scope 的 malloc 栈样本:上轮 2,960 条,本轮 37 条。native 空作用域已经不再是百万级分配来源。

  • 遍历路径的 malloc 栈样本:上轮 2,268 条,本轮 42 条;realloc 样本由 4,652 条变为本次未采到。

  • normalize 的 realloc 样本:上轮 10,944 条,本轮未采到;malloc 样本仍有 398 条,规范化并非变成零分配。

以上调用栈比较使用上轮保留的基线概率样本,本轮另跑的基线 API 总数与上轮完全一致。“未采到”不是一般性证明所有输入都不会分配;调用栈事件按概率采集,不能当作精确逐点计数。

还在大量创建什么

malloc 采样率 1/2048、realloc 为 1/256。下表按逆概率估计最近应用分配点,主要说明规模,不表示各组对象全部无用:

当前最近应用 malloc 点

次数估计

Shared<Vec>::new

1,022 万

Shared<Vec>::new

857 万

Dict::default

677 万

present

639 万

Text::from(&str)

578 万

Dict::insert<&String>

433 万

Content::one

408 万

Shared::new

396 万

Lexer::lex_code_string

361 万

Content::one、present 和共享 Piece 存储仍为百万级规模,意味着原优化移除了额外包装,但内容计划、挂载结果与最终树的多层存储仍有成本。字段键借用优化消除了 String 中转,不意味着最终 Text、字段 entries 和 COW 的成本也消失。仅凭 Shared<Vec>::new 的分配栈不能断言容器为空;需要追加长度/所有权分布才能决定是否采用更紧凑表示。词法字符串等也有很多小分配,但数量多不自动意味着应抢在求值通路之前重构 parser。

调用次数未变:execute/implementation 各 6,073,286 次,present 8,440,426 次,bind_content 2,213,972 次,data.native 1,955,784 次。实际 data.parse 565 次、parse_hit 51,173 次、模块编译 1 次、bind_cached 3 次。这些优化主要减少每次操作的成本,没有减少逻辑执行次数;现有数据与模块缓存仍然有效。

下一轮优先级

1. 研究内容物化的中间容器,而不是继续修已消除的冗余

crates/notist-eval/src/eval.rs:

  • Content::one 仍创建单元素 Vec,再进入共享存储。

  • native Construct 仍先形成可复用内容计划,随后 present_call 调用 present 获得临时 Vec,最后 extend 到调用者输出。

  • Element 路径虽已不再先造空壳,但仍有计划、子结果与最终共享 children 的表示转换。

最值得先补的是这些容器的空/单元素/多元素分布、唯一所有权比例和调用链分配归因。然后尝试局部减少确定不需要的临时结果,或让最终发射自然写入调用者工作空间,而不是一开始就引入全局池/缓存或全面改容器。这一方向有阶段、CPU、分配三方面证据,但 Content 具有可复用计划语义,不能缓存挂载结果或合并重复执行。必须保留实参及错误顺序、预算/回滚、来源链、词法捕获和重复挂载的独立执行。

2. 针对剩余宿主路径查找,缩小重复工作范围

src/vault.rs 的环境定位、src/publication/mod.rs 的有序输出冲突检查、资源快照与文件检查仍反复比较路径。下一步先测同一规范路径的重复查询次数与调用方已有句柄,寻找能自然复用已有结果的局部边界。DocumentIndex 已经处理,不必继续围绕它重复优化;不建议直接扩大成全局路径 ID、授权缓存或跳过 unchanged 核对。

3. 正文阶段的遍历与 HTML 缓冲区,可做独立小实验

crates/notist-ssg/src/build.rs 仍分别扫描链接和嵌入;遍历存储优化没有合并扫描职责。crates/notist-html/src/escape.rs 的 write 逐字符 push,采样仍可见它下方的 allocator 工作;可以检查安全地批量追加未转义片段及输出容量策略。两者都必须保持顺序、Unicode、HTML 转义及链接/嵌入检查,先做局部 A/B,不把正文阶段全部剩余时间都称为扫描或转义。

暂不优先

  • 模块/data.parse 再加缓存:未命中工作量仍很低。

  • Symbol/FieldId/特化键全面重构:实例键约 1.2~1.3% CPU,不是最大头。

  • 来源校验省略:约 1.7% CPU,不值得以语义和安全换收益。

  • 最终模板并行优化:当前阶段约 0.9%,收益上限有限。

  • 更换 allocator 或并行化求值:不是没有潜力,但本轮证据更支持先降低表示转换和小对象 churn。

  • 内存目标:若关注 2.70 GiB 峰值,应单独测整批分析结果、文档树、资源快照和产物字节的存活边界;不能靠分配计数推断泄漏或节省量。

限制与复现

仅覆盖这一站点的 CLI 完整构建,没有覆盖 serve 编辑、LSP/Worker、不同树形/站点规模或严格冷缓存。未取得精确 off-CPU、磁盘系统调用延迟或硬件缓存 miss 分解;statx 的 CPU 样本不是磁盘等待时间。本轮 CPU 基线沿用上轮原始采样,普通 release 总体收益则是本轮新旧交错测量,二者证据强度不同。

使用 tools/profile_hotspots.py 的 prepare、build、measure、cpu、allocations、verify,传新的 --out;prepare 后将 site 符号链接替换为不含 dist 的独立输入副本。分析使用 tools/analyze_hotspots.py,以及证据目录 reproducer/analyze_refresh.py --out 当前目录 --baseline 上轮目录。普通交错对照通过 tools/profile_source_cost.py 的 run,在两侧预热后五对交替运行 baseline/normal;重新执行基线分配时使用已验证的基线 frame-pointer 二进制和相同包装器。所有输入、源码和二进制摘要见 manifest.json,不覆盖已有日志和测量文件。