Notist
On this page
  1. 整站构建性能与热点评估:9a3c5f7
  2. 结论
  3. 范围、证据与可信度
  4. 构建时间和资源
  5. 过程热点:哪个阶段耗时
  6. 操作热点:CPU 实际执行什么
  7. 频次与分配证据
  8. 问题与优化优先级
  9. 第一优先级:高频通路的不必要构造
  10. 第二优先级:路径构造和有序索引的重复成本
  11. 第三优先级:重复遍历与遍历栈分配
  12. 暂不优先
  13. 边界与复现

整站构建性能与热点评估:9a3c5f7

结论

本次冻结提交为 9a3c5f7bdaeaa8eca31b6160f140a8928704322c。主要问题是百万级求值/内容物化操作携带的短命对象构造、堆分配与销毁,其次是宿主路径比较和重复树遍历。不是最终页面模板渲染太慢,也没有证据支持继续优先扩大模块/数据解析缓存或名称整数化工程。

优先做减少无用构造的小型实验,再处理路径重复工作和遍历栈;不能把采样占比直接当作可获得的加速比例。本轮仅测量和分析,没有改动生产实现,也没有实施优化。

范围、证据与可信度

  • 工作负载:冻结完整站点,51,739 页;产物 78,004 项,共 451,812,631 字节。

  • 操作:CLI notist build,每轮都重新执行整站构建,不是编辑后的增量构建。

  • 从 git archive 导出提交,分别构建普通 release、frame-pointer release、coarse 和 detail 探针版本;各有独立 Cargo target,编译并行度 2。

  • 使用独立输入副本和输出目录,没有改动 consumer,也没有重用其他实验的输出目录。

  • 一次首次发布、五次普通计时、两次 frame-pointer 控制、三次 coarse、两次 detail、两次 CPU 采样、一次分配采样。

  • 共 16 次完整构建,每次均核对全部输出的长度和 SHA-256,全部与冻结基准一致;最终再次核对源码、二进制、输入和输出。

  • CPU:gperftools,499 Hz,采样 Rust 父进程的用户空间调用栈;两次共 8,235 个样本,全部含目标可执行文件帧;展开 inline frame,同一栈内递归函数只计一次 inclusive。

  • 分配:独立 LD_PRELOAD 诊断包装器调用 glibc 原实现,统计成功分配 API 调用,并概率采集调用栈;不统计扩展子进程。122,419 条分配栈均找到应用帧,记录丢失为 0。

  • 工具回归:tools/test_profile_hotspots.py 的 4 项测试通过;tools/test_profile_*.py 共 67 项通过。

证据目录:/tmp/opencode/notist-hotspots-9a3c5f7-20261109。主要文件:manifest.json、samples.json、phase-summary.json、operation-summary.json、cpu-summary.json、allocation-stacks.attribution.json,以及 logs、sources、bin、reproducer。原始 CPU .prof、展开后的调用栈、分配二进制记录和符号缓存均保留。

构建时间和资源

版本

次数

wall 中位数

user CPU 中位数

system CPU 中位数

普通 release

5

8.407 s

7.642 s

1.084 s

frame pointer 控制

2

8.438 s

7.599 s

1.037 s

coarse 阶段探针

3

8.349 s

7.577 s

1.033 s

detail 细粒度探针

2

10.642 s

9.871 s

1.051 s

普通五次为 9.965、8.407、8.406、8.391、8.843 秒,全部保留,未事后剔除慢样本。最慢一次有 12 次 major fault;仅据此不能认定波动完全由缺页造成。首次向空输出目录发布为 12.035 秒,不纳入五次中位数;这不是清空操作系统缓存后的严格冷启动实验。其余运行已有产物,发布主要核对内容是否变化,不代表完整首次写盘成本。

普通运行 maxrss 的中位数约 2.70 GiB。这是峰值资源指标,不是内存泄漏证据,也不是存活对象的精确归因。源码上,分析结果和求值树存在整批持有边界,输出字节也集中装配;若要降低峰值,需另做分阶段 RSS/存活分配实验。

frame-pointer 控制中位数比普通约高 0.36%;coarse 略低并不证明探针无开销。detail 比普通约慢 26.6%,仅用于调用次数和路径证据,不用其递归计时给真实构建分配秒数。本轮没有交错测量旧提交,因此不能把与已有报告的绝对时间差解释为提交级性能回归。

过程热点:哪个阶段耗时

以下为 coarse 三次的阶段中位数;比例分母是同组 cli.total 的 8.271 秒,不是普通版本的 8.407 秒。

过程

wall

占阶段总时间

文档求值

3.071 s

37.1%

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

1.886 s

22.8%

文档分析,包含宿主环境装配

0.994 s

12.0%

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

0.795 s

9.6%

发布

0.456 s

5.5%

扩展执行与路由阶段

0.301 s

3.6%

公共模板上下文

0.141 s

1.7%

最终页面模板渲染阶段

0.067 s

0.8%

分析与求值约占一半,正文阶段是第二大块。表未列全装配、目录与析构边界;各项中位数不保证严格相加。detail 中 HTML 调用本身约 0.645 秒、registry 选择约 0.124 秒;正文剩余时间还包含遍历、路径处理、查表和销毁,不能全部归因于链接解析。发布中的 unchanged 比较约 0.402 秒,说明“产物未变化”仍然要付出逐项核对成本。扩展进程阶段约 0.205 秒,包含子进程执行与等待,不等于纯 I/O 等待。

操作热点:CPU 实际执行什么

CPU 比例是两次父进程采样的范围,不是包括扩展子进程的 wall 比例。self 仅计最内层执行位置;inclusive 包含被调用工作,各行有重叠,不可相加。

操作/调用路径

CPU 样本比例

解释

glibc malloc/free/realloc 等自身

27.0~28.2% self

堆管理本身已是显著成本

文档求值 run

37.3~37.4% inclusive

与阶段计时相互印证

present 内容物化

约 35.8% inclusive

含 execute、表达式及构造

execute 调用路径

26.2~26.5% inclusive

实参、字段、派发、构造与分配

expr 表达式求值

13.6~13.7% inclusive

是上述调用路径的一部分

std::path::compare_components

9.9~10.8% inclusive

宿主文件路径/索引键比较,不是语言变量名比较

HTML render_with_diagnostics

7.0~7.5% inclusive

正文阶段的一个子项

Node::descendants

3.6~4.4% inclusive

含遍历栈分配与增长

Dict::insert

2.9~3.2% inclusive

包含查找、键处理、容器及分配

implementation 查找路径

2.9~3.1% inclusive

包含特化键与缓存子项

memcpy/memmove 自身

约 2.3% self

不是全部克隆/复制的总成本

实例/编译期值 key

约 1.5% inclusive

不应与 implementation 重复相加

来源引用校验

1.0~1.3% inclusive

不是首要热点,不应通过省略校验优化

路径比较涉及 Vault::ensure_environment、Assets::insert、DocumentIndex::find、ResourceSession::register_source、Files::inspect 和 publication::unchanged。因此不能把这约一成 CPU 都归给正文链接,也不能认为为语言字段引入 Symbol 就能消除它。

频次与分配证据

detail 两次的主要调用次数一致:execute/implementation 各 6,073,286 次,present 8,440,426 次,bind_content 2,213,972 次,data.native 1,955,784 次。present 含递归,不等于不同内容对象数量。真实 data.parse 为 565 次,parse_hit 为 51,173 次;实际模块编译 1 次、bind_cached 3 次。已有数据解析和模块绑定复用确实生效,主要问题在命中这些复用后仍需执行的百万级小操作。

分配 probe 精确统计父进程成功 API 调用:malloc 132,729,790 次、calloc 37 次、realloc 14,726,628 次。这不是同时存活对象数量,也不是每次 realloc 都发生了物理复制。malloc/calloc 以 1/2048、realloc 以 1/256 概率采集栈;以下按逆概率估计,不是逐调用点精确计数:

最近应用分配点

malloc 次数估计

Shared<Vec>::new

1,672 万

Dict::default

1,070 万

Shared<Vec>::new

1,048 万

Dict::insert<&String>

970 万

Text::from(String)

686 万

present

607 万

Shared<BTreeMap<BindingId, Runtime>>::new

606 万

Node::descendants

464 万

Shared::new

401 万

Content::one

395 万

resources::normalize 另有约 280 万次 realloc 的估计。概率样本中的稀有大分配字节方差较大,不据累计字节给优化排序。

问题与优化优先级

第一优先级:高频通路的不必要构造

  1. crates/notist-eval/src/eval.rs:1139:所有 execute 都先 Scope::new,但只有 code 分支使用输入环境。Scope 的共享容器即使为空也要分配。应实验将作用域构造及输入装配收敛到 Code 路径。

  2. crates/notist-eval/src/eval.rs:1253、1375:先 item_shell 克隆字段/实例并构造空 children,紧接着覆盖 content;present 的 Element 路径也先产生空 children 再赋值。应考虑一次构造最终 payload,而非创建后丢弃中间容器。

  3. crates/notist-core/src/shared.rs:434:Dict::insert 先 Into,查不到再转换 Text;借用键会经历不必要的临时字符串构造,更新已有键也先构造字符串。可实验借用查找、仅在插入时创建最终键。

  4. crates/notist-eval/src/eval.rs:171、656:单元素 Content 和递归 present 分别构造 Piece 存储、输出 Vec;宜先移除确定无用的中间对象,再评估是否需要更大范围的表示调整。

证据链是“百万级调用 + 大量对应分配 + 分配器显著 CPU + 源码中的冗余构造”,不是单凭函数名猜测。仍需独立 A/B 才能量化收益;不能声称 28% 分配器 CPU 全部可消除。必须保持实参求值与错误顺序、调用深度、内容/数据预算、来源链以及重复挂载的独立执行语义。

第二优先级:路径构造和有序索引的重复成本

src/resources.rs:483 的 normalize 逐分量增长 PathBuf;路径作为多处 BTreeMap/BTreeSet 的键反复比较。应先找同一输入视图中重复规范化、拼接和查表的调用链,优先自然复用已规范化表示及已有句柄。不能只把路径换成字符串或全局 ID 就宣称解决问题,也不能缓存授权结果、跳过根目录边界或产物内容核对。

第三优先级:重复遍历与遍历栈分配

crates/notist-core/src/item.rs:689 的 descendants 每次 vec![self],然后 extend children。这一通用辅助操作产生约 464 万次 malloc;就算单次很便宜,调用总量也足以形成 CPU 热点。求值结构检查、正文链接与嵌入检查、标题取文等都有树访问。先检查可合并的遍历职责及可复用的工作空间,再考虑容器调整。必须保留遍历顺序、所有来源/链接检查,不能改成可能无界溢出的递归以换掉堆栈。

暂不优先

  • 扩大模块或数据解析缓存:实际未命中工作已经很少。

  • 全面 FieldId/Symbol/特化键重构:特化键约 1.5% CPU,优先级低于容器和路径;不排除后续小型实验。

  • 最终页面模板并行化:阶段占比不足 1%,不能解决主要串行阶段。

  • 无证据地更换 allocator 或并行化求值:可能掩盖对象 churn,且会带来内存、缓存共享和输入视图语义成本。

边界与复现

本轮没有测增量编辑、serve 响应、LSP/Worker 交互、不同内容复杂度或不同规模站点,不能将本次排序外推为所有操作的结论。也没有精确 off-CPU、磁盘系统调用或硬件缓存 miss 分解;不能用 wall − user − system 推算 I/O 时间。

在提交 9a3c5f7 上使用 tools/profile_hotspots.py 的 prepare、build、measure、cpu、allocations、verify,均传入新的 --out;分析使用 tools/analyze_hotspots.py --out。prepare 后将 site 符号链接替换为独立输入副本、排除 dist,再开始构建和测量;本次首轮因此确为向空产物目录发布。依赖的冻结基准、采样库绝对路径和二进制摘要见 manifest.json;工具副本见 reproducer。