zh CN 性能 - chiba233/yumeDSL GitHub Wiki

性能

Home | API 参考 | 源码位置追踪

这一页集中整理 parser 相关的性能说明,不再把基准分散在多个 wiki 页面里。


日常文档基准

这一节统一按一套连续口径来读,不再拆“历史组 / 补测组”,但仍保留不同年代留下来的表,因为它们回答的问题并不完全相同。

先把口径说清楚:

  • 200 KB 主基准 用的是“博客 / CMS / 编辑器日常文档”风格的 mixed document
  • 内存画像 仍然单列使用 dense inline 与 20k nested inline
  • 本轮扩展方法学:单 case 单进程、--expose-gc、每轮前后强制 GC、每项最多 20 轮

测试环境:

  • 既有表:鲲鹏 920 aarch64 / Node v24.14.0
  • 本轮扩展:鲲鹏 920 aarch64 / Node v24.15.0

Node 小版本已变化,因此跨表读法应看量级与趋势,不要把 1–2 ms 微差读成架构级结论。

版本分组总览(1.1.8+)

这一段不是替代原始数据,而是给后面的长表一个阅读入口。

分组 版本 怎么读
速度优势 1.2.1 / 1.2.2 / 1.2.4 / 1.2.6 / 1.2.7 / 1.4.3 / 1.4.4 这条版本窗代表本轮 full-parse 更强的速度档:1.2.6 / 1.2.7 仍是“全面速度型”代表,而 1.4.3 / 1.4.4 则是后期版本线里少见把 structural 主路径明显拉回来的速度回升点
内存优势 1.1.9 / 1.2.4 / 1.2.6 / 1.3.4 / 1.3.5 parseStructuralheapUsed 更低;其中 1.3.4 / 1.3.5 在 200 KB 与 20k nested 档尤其亮眼
平衡版本 1.1.9 / 1.2.1 / 1.2.2 / 1.2.4 / 1.2.6 速度与内存都没有明显短板,适合作为“综合型”版本看待
功能优先阶段 1.3.0 ~ 1.4.2,尤其 1.3.6+ / 1.4.0+ 这一段更多在为 shorthand、上下文转义、增量 diff 与稳定集成让路;整体上仍不是内存优先路线,后期版本里也普遍要接受更多功能语义带来的复杂度与取舍。1.4.3 是这段之后第一个把重心转回常数优化的版本

全量解析(~200 KB,版本线起点)

API 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7
parseRichText ~4514.2 ms ~40.5 ms ~37.4 ms ~38.1 ms ~34.0 ms ~42.3 ms ~29.9 ms ~30.6 ms
parseStructural ~36.1 ms ~35.0 ms ~33.4 ms ~29.7 ms ~26.2 ms ~34.7 ms ~29.0 ms ~23.3 ms

这组数据主要说明两个阶段性变化:

  • 1.1.0parseRichText 已不具备当前参考意义
  • 1.1.2+ 完成全迭代切换后,主基准才真正进入今天这条版本线的比较区间

全量解析扩展表(1.1.8 ~ 1.4.4

本轮补测继续沿用 mixed document 主基准,并把尺寸扩到 200 KB / 1 MB / 2 MB,方便区分“日常文档速度”和“中大文档伸缩性”。

版本 parseRichText 200 KB parseStructural 200 KB parseRichText 1 MB parseStructural 1 MB parseRichText 2 MB parseStructural 2 MB
1.1.8 ~36.83 ms ~25.86 ms ~141.5 ms ~96.28 ms ~244.9 ms ~163.9 ms
1.1.9 ~34.59 ms ~27.96 ms ~143.2 ms ~97.97 ms ~241.7 ms ~161.0 ms
1.2.0 ~34.22 ms ~25.79 ms ~142.0 ms ~100.8 ms ~257.2 ms ~162.9 ms
1.2.1 ~32.75 ms ~25.53 ms ~142.4 ms ~97.18 ms ~251.4 ms ~163.7 ms
1.2.2 ~32.74 ms ~26.07 ms ~139.3 ms ~97.68 ms ~256.3 ms ~161.0 ms
1.2.3 ~33.24 ms ~27.50 ms ~141.5 ms ~97.45 ms ~253.7 ms ~163.8 ms
1.2.4 ~31.70 ms ~26.56 ms ~141.6 ms ~97.24 ms ~254.5 ms ~164.1 ms
1.2.5 ~33.40 ms ~25.83 ms ~139.9 ms ~98.54 ms ~255.4 ms ~163.2 ms
1.2.6 ~32.22 ms ~27.29 ms ~140.7 ms ~96.57 ms ~251.6 ms ~159.5 ms
1.2.7 ~32.58 ms ~25.39 ms ~142.1 ms ~100.2 ms ~243.5 ms ~162.1 ms
1.3.0 ~34.46 ms ~26.06 ms ~140.0 ms ~97.68 ms ~248.9 ms ~163.3 ms
1.3.1 ~34.28 ms ~27.01 ms ~144.8 ms ~100.1 ms ~253.2 ms ~163.6 ms
1.3.2 ~34.82 ms ~27.10 ms ~145.3 ms ~97.08 ms ~245.1 ms ~162.3 ms
1.3.3 ~34.19 ms ~27.27 ms ~142.6 ms ~96.29 ms ~253.6 ms ~162.1 ms
1.3.4 ~45.70 ms ~37.64 ms ~153.8 ms ~103.7 ms ~254.3 ms ~178.2 ms
1.3.5 ~44.33 ms ~37.47 ms ~153.6 ms ~100.6 ms ~252.6 ms ~183.7 ms
1.3.6 ~51.10 ms ~40.27 ms ~151.1 ms ~112.0 ms ~292.5 ms ~192.7 ms
1.3.7 ~52.58 ms ~40.48 ms ~158.4 ms ~120.2 ms ~279.4 ms ~196.8 ms
1.3.8 ~47.11 ms ~41.60 ms ~160.1 ms ~119.8 ms ~284.2 ms ~198.2 ms
1.3.9 ~52.27 ms ~40.93 ms ~162.0 ms ~119.4 ms ~296.0 ms ~201.7 ms
1.4.0 ~49.41 ms ~40.98 ms ~157.4 ms ~116.1 ms ~292.3 ms ~207.0 ms
1.4.1 ~52.24 ms ~41.51 ms ~152.0 ms ~114.8 ms ~282.7 ms ~206.5 ms
1.4.2 ~51.86 ms ~39.78 ms ~158.6 ms ~119.0 ms ~281.2 ms ~212.9 ms
1.4.3 ~35.09 ms ~22.89 ms ~131.8 ms ~75.28 ms ~245.5 ms ~145.2 ms
1.4.4 ~32.75 ms ~24.37 ms ~134.8 ms ~77.10 ms ~244.3 ms ~152.4 ms

这组补测最值得读的不是单个数字,而是版本窗:

  • 1.1.8 ~ 1.2.7:仍然保持 1.1.7 之后那一档 full-parse 速度;这也和 changelog 一致——1.1.8 本身只是 walkTokens 栈安全与文档补充,不应和 1.1.7 拉开大差距
  • 1.2.1 ~ 1.2.7:是这轮 mixed 文档 full-parse 的速度甜点区
  • 1.3.0 ~ 1.3.3:引入 shorthand,但主基准还没明显掉档
  • 1.3.4:这不是“微抖动”,而是肉眼可见的常数退化。拿 parseStructural 来看,200 KB1.2.x 常见的 ~26 ms 抬到 ~40 ms 左右,约 +50%2 MB~160 ~ 164 ms 抬到 ~200 ~ 213 ms,约 +25% ~ +33%。这一段更多是在为 shorthand 语义定稿、恢复策略统一、上下文转义与后续增量语义铺路
  • 1.4.0 ~ 1.4.2:重点已经转到“增量集成稳定化 / diff 契约清晰化”,不是继续把 full-parse 压回 1.2.x 档位
  • 1.4.3:这次不是“拉回一点”,而是直接把 structural 主路径打到了本轮新低:parseStructural 200 KB1.4.2~39.78 ms 降到 ~22.89 ms,约 -42%1 MB~119.0 ms 降到 ~75.28 ms,约 -37%2 MB~212.9 ms 降到 ~145.2 ms,约 -32%。这轮 current workspace 在 full-parse structural 路径上已经不只是”追回来”,而是超过了之前那条版本窗
  • 1.4.4:仍然属于 1.4.3 拉回来的这档 full-parse 快版本,但形态已经更像“部分延续、部分回弹”:parseRichText200 KB / 2 MB 继续很强,1 MB 小幅回升;parseStructural 三档则都高于 1.4.3,不过总体仍明显优于 1.4.0 ~ 1.4.2

onError 版本差异(1.1.x)

这一组已经统一移到 版本语义说明

版本内存画像(parseStructural

下面单独把 structural parser 的内存差异拆出来说。

注意这里看的都是 afterParse.heapUsed,不是 GC 之后的回落值;目的是描述“parse 刚结束时 public structural tree 的内存形态”。

场景 1.1.4 1.1.5 1.1.6 1.1.7
200 KB dense inline,parse 后 heapUsed 27.37 MB 22.73 MB 21.44 MB 21.60 MB
2 MB dense inline,parse 后 heapUsed 206.96 MB 137.83 MB 138.54 MB 138.51 MB
20k nested inline,parse 后 heapUsed 24.48 MB 17.84 MB 16.53 MB 16.53 MB

同一轮采样到的 sampledPeakRss

场景 1.1.4 1.1.5 1.1.6 1.1.7
200 KB dense inline 104.23 MB 92.22 MB 97.09 MB 96.69 MB
2 MB dense inline 372.37 MB 290.79 MB 294.80 MB 295.41 MB
20k nested inline 83.82 MB 75.59 MB 77.48 MB 78.53 MB

上一轮把多组版本和场景放进同一进程里跑,heapUsed 会受到前一个 case 的残留对象、JIT 状态和 GC 时机影响,尤其容易抬高 200 KB / 20k nested 这两档的读数。本轮改成分版本分进程、单样本独立采样后,数值回到更稳定的区间。

内存扩展表(1.1.8 ~ 1.4.4

版本 200 KB heapUsed 1 MB heapUsed 2 MB heapUsed 20k nested heapUsed
1.1.8 21.46 MB 85.35 MB 138.93 MB 16.31 MB
1.1.9 21.33 MB 84.94 MB 138.90 MB 16.34 MB
1.2.0 21.46 MB 85.14 MB 138.78 MB 16.36 MB
1.2.1 21.50 MB 85.33 MB 138.74 MB 16.33 MB
1.2.2 21.42 MB 85.52 MB 138.91 MB 16.33 MB
1.2.3 21.51 MB 85.57 MB 138.72 MB 16.33 MB
1.2.4 21.31 MB 85.34 MB 138.62 MB 16.47 MB
1.2.5 21.64 MB 85.30 MB 138.62 MB 16.43 MB
1.2.6 21.26 MB 85.52 MB 138.64 MB 16.45 MB
1.2.7 21.47 MB 85.63 MB 138.60 MB 16.51 MB
1.3.0 20.85 MB 85.73 MB 138.87 MB 17.27 MB
1.3.1 21.47 MB 85.70 MB 138.94 MB 17.32 MB
1.3.2 21.48 MB 86.97 MB 142.03 MB 17.90 MB
1.3.3 21.64 MB 90.99 MB 141.94 MB 17.82 MB
1.3.4 17.66 MB 84.39 MB 141.10 MB 16.89 MB
1.3.5 17.59 MB 84.33 MB 141.07 MB 16.03 MB
1.3.6 21.32 MB 76.87 MB 159.64 MB 16.94 MB
1.3.7 21.40 MB 76.87 MB 159.69 MB 17.33 MB
1.3.8 21.45 MB 77.07 MB 159.75 MB 16.99 MB
1.3.9 21.33 MB 77.12 MB 159.77 MB 17.41 MB
1.4.0 21.36 MB 77.05 MB 159.82 MB 17.42 MB
1.4.1 21.48 MB 77.02 MB 159.78 MB 17.39 MB
1.4.2 21.50 MB 77.07 MB 159.80 MB 17.39 MB
1.4.3 20.58 MB 71.53 MB 162.32 MB 18.67 MB
1.4.4 20.75 MB 71.81 MB 162.63 MB 19.52 MB

这张表需要按“档位”读,而不是只盯一列:

  • 1.1.8 ~ 1.2.7:总体仍延续 1.1.6 / 1.1.7 的 structural 内存档位
  • 1.3.4 / 1.3.5:在小文档与 20k nested 上出现了本轮最漂亮的一档 heapUsed
  • 1.3.6+1 MB~85 MB 掉到 ~77 MB,约 -9%;但 2 MB 又从 ~141 MB 抬到 ~160 MB,约 +13% 。这更像是分配形态改了: 中档文档的活跃对象窗口被收紧了,但更大文档会同时保留更多结构对象 / 边界元数据,所以不是“全面更省”,而是新的内存形态
  • 1.4.3:小中档内存有改善:200 KB21.50 MB 降到 20.58 MB1 MB77.07 MB 降到 71.53 MB。 但大文档和深嵌套方向是反的:2 MB159.80 MB 升到 162.32 MB+1.6%),20k nested17.39 MB 升到 18.67 MB+7.4%)。所以 1.4.3 的内存画像是小中档变好、大文档和深嵌套变差,不是全面改善
  • 1.4.4:四档都和 1.4.3 非常接近,但方向上是“小中大文档都略高、20k nested 仍偏高”。所以如果你关心的是 structural parse 的 retained shape,本轮更适合读成“没有换档,只是继续停在 1.4.3 那条偏高的 retained-shape 区间
  • 因此如果你关心的是小中型文档 footprint1.3.4 / 1.3.5 很亮眼;如果你关心的是大文档 2 MB 档1.2.x 这一窗反而更稳
版本 public parseStructural 的内存形态 读法
1.1.1 在 two-phase 重构后的几个版本里,public structural 路径的额外内存负担最高:先建 indexed tree,再走独立 strip 阶段生成 public tree 这一段里 public tree 的峰值还没有专门压缩
1.1.2 public tree 的分配形态与 1.1.1 同类,但已经全迭代、不会再因深嵌套爆调用栈 主要变化是栈安全,不是 public memory 峰值下降
1.1.3 1.1.1 / 1.1.2 更低:stripMeta 不再给整棵树建 Map<IndexedStructuralNode, StructuralNode> public tree 的内存形态开始收敛
1.1.4 structural 内存等级与 1.1.3 基本同类 1.1.3 属于同一档
1.1.5 这一条版本线里第一次把 public API 的“第二棵树”拿掉:扫描结束后不再把 indexed tree 复制成 public tree 主内存台阶在这里下降了一次
1.1.6 1.1.5 属于同一棵 public tree 架构,但扫描期分配策略继续收紧:文本缓冲改成区间/分段累积,不再反复拼接字符串;raw / block 子帧继续共享原始源码区间,不再提前切子串 200 KB 更低,2 MB20k nested 没有低于 1.1.5
1.1.7 架构同 1.1.6,优化集中在 render 层和扫描期常数:render 层 trimBlockBoundaryTokens 不再全量 clone children 数组;flushBuffer 对常见 1–2 对 segment 直接拼接避免临时数组 200 KB 更低,2 MB20k nested 没有低于 1.1.5

位置追踪开销

注意: 以下数据采集时未记录对应版本号,仅保留量级参考。后续补测时应标注版本并与全量解析扩展表对齐。

实测输入约 ~200 KB(204,840 bytes),每项 20 次采样。

API 不开追踪 开追踪 额外开销
parseRichText ~22.45 ms ~34.07 ms ~51.8%
parseStructural ~14.88 ms ~18.49 ms ~24.3%

这一轮的实际值说明:

  • trackPositions 的成本没有失控,但确实不是”零开销”
  • parseRichText 的追踪附加成本明显高于 parseStructural
  • 如果要省预算,优先盯 parseRichText + trackPositions

子字符串解析:baseOffset / tracker

注意: 以下数据采集时未记录对应版本号,仅保留量级参考。

这一组使用固定 53 字符切片,专门测 源码位置追踪 里的子串场景。每项 800 次采样。

API baseOffset tracker 说明
parseRichText slice ~23.78 µs ~20.62 µs 同一量级
parseStructural slice ~14.26 µs ~13.47 µs 同一量级

这组实测值说明:

  • baseOffset / tracker 都维持在几十微秒量级
  • tracker 没有把子串解析放大成毫秒级路径
  • parseStructural 的子串解析仍然比 parseRichText 更轻

增量解析

关于“增量 structural 缓存”(跨 edit 维护 StructuralNode[] / Zone[],避免每次全量 parseStructural ),见:增量解析

旧版“改中间一个 36 字符标签”的微基准已经不再适合作为这页的主入口。本节现在改为更接近编辑器实际使用的口径:

  • 基线文档: ~1 MB
  • 单次编辑:5%
  • 编辑类型: inline / deep-inline / raw / block
  • 方法学: 单 case 单进程、--expose-gc、每轮前后强制 GC、每项 20 轮

版本分组总览(增量)

分组 版本 怎么读
第一代公开增量面 1.2.0 仍是 low-level parseIncremental/updateIncremental 口径,可用但明显慢于后续 session-first 版本
bare 增量速度优势 1.2.4 / 1.2.5 / 1.2.6 / 1.2.7 / 1.3.0 / 1.4.3 / 1.4.4 1.2.4 ~ 1.3.0 是最稳定的低二十毫秒档;1.4.3 / 1.4.4 则是后期版本线里把 bare applyEdit 再明显拉回来的回升窗口
deep-inline 速度优势 1.2.6 / 1.3.0 / 1.3.4 / 1.3.5 1.2.6 / 1.3.0 是最低点;1.3.4 / 1.3.5 则代表 shorthand 引入后的回落修复窗口
raw / block 速度优势 1.3.6 / 1.3.7 / 1.4.0 / 1.4.1 / 1.4.3 按上下文生效的转义与边界稳定化之后,raw / block 成为后期版本里最容易压低的裸更新路径;1.4.3 又把这两项继续压低了一截
diff 成本优势 1.4.0 / 1.4.1 这两版是“结构化 diff 仍可集成”的最稳窗口:比 1.3.8 / 1.3.9 明显便宜,也比 1.4.2 / 1.4.3 更均衡
retained heap 优势 1.2.1 ~ 1.3.7,尤其 1.2.4 ~ 1.3.7 建立态 retained heap 基本稳定在 29.5 ~ 30.7 MB;其中 inline 编辑后的 retained heap 在 1.2.4 ~ 1.3.7 这一窗尤其好看
综合集成版本 1.4.0 / 1.4.1 如果你既关心 session 契约稳定、diff 可消费、又不想把 retained heap 和 diff 成本推到更差一侧,这两版仍然最好讲故事

增量补测主表(1 MB 基线,约 5% 编辑)

版本 引擎 inline deep-inline raw block 备注
1.2.0 low-level ~39.60 ms ~38.87 ms ~33.40 ms ~37.57 ms parseIncremental/updateIncremental
1.2.1 session ~41.16 ms ~40.32 ms ~36.65 ms ~40.95 ms createIncrementalSession(...) 首版
1.2.2 session ~41.69 ms ~41.66 ms ~49.66 ms ~39.48 ms 仍属早期 session 档
1.2.3 session ~46.14 ms ~47.97 ms ~44.50 ms ~48.09 ms 还没吃到纯 inline zone 切分红利
1.2.4 session ~22.88 ms ~26.54 ms ~22.87 ms ~28.83 ms softZoneNodeCap / 内部 zone 切分后第一档明显提速
1.2.5 session ~22.58 ms ~26.56 ms ~22.61 ms ~29.54 ms 延续 1.2.4 档位
1.2.6 session ~21.70 ms ~20.88 ms ~22.86 ms ~28.31 ms deep-inline 最漂亮的一档之一;与签名路径栈安全修复一致
1.2.7 session ~22.31 ms ~26.67 ms ~22.88 ms ~29.32 ms 仍处于低二十毫秒档
1.3.0 session ~22.25 ms ~20.61 ms ~22.76 ms ~28.88 ms shorthand 引入后,增量主档位仍基本守住
1.3.1 session ~22.81 ms ~37.11 ms ~22.14 ms ~29.04 ms shorthand 边界 / depthLimit 修复后,deep-inline 明显变重
1.3.2 session ~22.36 ms ~36.79 ms ~22.09 ms ~28.91 ms deep-inline 仍高
1.3.3 session ~21.91 ms ~38.00 ms ~22.20 ms ~29.64 ms deep-inline 仍高
1.3.4 session ~26.85 ms ~28.19 ms ~21.83 ms ~28.08 ms inline 变重,但 deep-inline 明显回落
1.3.5 session ~27.08 ms ~28.17 ms ~21.75 ms ~28.93 ms 1.3.4 同类
1.3.6 session ~29.71 ms ~28.77 ms ~15.86 ms ~32.25 ms raw 路径明显下降;与“转义按上下文生效”一致
1.3.7 session ~29.80 ms ~28.99 ms ~21.69 ms ~32.28 ms 修复 endTag 吞前缀后,整体档位维持
1.3.8 session ~26.37 ms ~23.10 ms ~15.90 ms ~32.55 ms diff 结构化之后,deep-inline 再次下降
1.3.9 session ~26.65 ms ~22.70 ms ~15.90 ms ~26.39 ms isNoop / 保守 diff 契约补充后的稳定低位
1.4.0 session ~26.48 ms ~22.27 ms ~16.01 ms ~26.14 ms stable 集成面定稿后,速度保持在这一档
1.4.1 session ~27.13 ms ~22.47 ms ~16.00 ms ~25.62 ms 深层损坏 inline 编辑不再卡住;正常编辑档位保持
1.4.2 session ~29.61 ms ~22.63 ms ~21.57 ms ~25.81 ms 本轮最新版本;语义与类型清理优先,速度不再追求新低位
1.4.3 session ~17.96 ms ~29.81 ms ~15.76 ms ~17.73 ms 当前定版版本;inline / raw / block 明显回落,但 deep-inline 仍高于 1.4.0 ~ 1.4.2
1.4.4 session ~17.72 ms ~29.59 ms ~15.90 ms ~17.98 ms 基本延续 1.4.3 的 bare incremental 档位;inline 略好,deep-inline 小幅回落,但 raw / block 略高

这张增量表怎么读

  • 1.2.0 ~ 1.2.3 是“第一代公开增量面”,能用,但离后面那条速度线还有一整档差距
  • 1.2.4 是这节真正的第一个性能拐点:changelog 里的纯 inline 文档 zone 切分,直接把 1 MB / 5% 编辑打进低二十毫秒档
  • 1.2.6 / 1.3.0 是本轮综合最好读的一组:inline / deep-inline / raw 三类都很稳
  • 1.3.1 ~ 1.3.3 的 deep-inline 明显抬高,这和 shorthand 边界、父级闭合归属、歧义守卫那几轮修复是对应的;这里优先保正确性
  • 1.3.6+ 的 raw 编辑明显更快,这和 changelog 里“转义规则改为按上下文生效、raw/block 正文边界更稳定”是同一方向
  • 1.4.2 的 raw applyEdit1.4.1~16.00 ms 回升到 ~21.57 ms,约 +35%,所以这不是“零代价清理”
  • 1.4.3:bare applyEdit 这次大体是成功的:inline 回到 ~17.96 msraw~15.76 msblock~17.73 ms; 但 deep-inline 仍在 ~29.81 ms,明显高于 1.4.0 ~ 1.4.2 那档二十出头毫秒。所以它不是“增量全胜”,而是inline / raw / block 很强,deep-inline 还留了尾巴
  • 1.4.4:继续维持 1.4.3 这档 bare incremental 水平:inline 还略好一点,deep-inline 也只小幅回落;但 raw / block1.4.3 略高,所以它更像“延续同一档位”,而不是继续全面下探

本轮增量补测的统一口径

  • 基线文档约 1 MB
  • 每个场景都把单次编辑控制在约 5%
  • inline:修改普通 inline 标签内容
  • deep-inline:修改深层嵌套 inline 区段内容
  • raw:修改 raw 标签正文
  • block:修改 block 标签正文
  • 1.2.0 使用 low-level parseIncremental/updateIncremental
  • 1.2.1+ 使用 createIncrementalSession(...).applyEdit(...)
  • 本轮所有版本在该测试里都保持 20/20 命中预期模式,没有出现需要额外注释的 fallback reason

增量 diff 成本(1.3.8+

1.3.8 开始,session API 已经有了 applyEditWithDiff(...) 这条“更新 + 产出 diff”的路径。对编辑器集成方来说,这一节通常比裸 applyEdit(...) 更接近真实成本。

版本 inline applyEdit inline applyEditWithDiff 倍率 deep-inline applyEdit deep-inline applyEditWithDiff 倍率 raw applyEdit raw applyEditWithDiff 倍率 block applyEdit block applyEditWithDiff 倍率
1.3.8 ~26.37 ms ~35.17 ms 1.33x ~23.10 ms ~193.7 ms 8.39x ~15.90 ms ~87.61 ms 5.51x ~32.55 ms ~101.0 ms 3.10x
1.3.9 ~26.65 ms ~35.88 ms 1.35x ~22.70 ms ~195.2 ms 8.60x ~15.90 ms ~92.27 ms 5.80x ~26.39 ms ~101.5 ms 3.85x
1.4.0 ~26.48 ms ~39.75 ms 1.50x ~22.27 ms ~63.63 ms 2.86x ~16.01 ms ~52.93 ms 3.31x ~26.14 ms ~66.18 ms 2.53x
1.4.1 ~27.13 ms ~41.12 ms 1.52x ~22.47 ms ~61.68 ms 2.75x ~16.00 ms ~51.30 ms 3.21x ~25.62 ms ~62.94 ms 2.46x
1.4.2 ~29.61 ms ~41.44 ms 1.40x ~22.63 ms ~79.89 ms 3.53x ~21.57 ms ~51.52 ms 2.39x ~25.81 ms ~64.54 ms 2.50x
1.4.3 ~17.96 ms ~38.40 ms 2.14x ~29.81 ms ~85.96 ms 2.88x ~15.76 ms ~76.20 ms 4.84x ~17.73 ms ~79.69 ms 4.50x
1.4.4 ~17.72 ms ~40.55 ms 2.29x ~29.59 ms ~87.81 ms 2.97x ~15.90 ms ~77.36 ms 4.86x ~17.98 ms ~82.30 ms 4.58x

这张表最关键的结论不是“开 diff 会变慢”——这本来就合理——而是 1.4.0 把 diff 成本压回了一个更可集成的区间

  • 1.3.8 / 1.3.9deep-inlinerawblock 的 diff 成本都很重,尤其 deep-inline 接近 8.4x ~ 8.6x
  • 1.4.0+deep-inline 仍然最贵,但已经从接近 200 ms 掉到 60 ~ 80 ms 档;raw / block 也从 80 ~ 100 ms 档回到 50 ~ 66 ms
  • inline 这一项反而一直相对稳定,说明问题不在“只要开 diff 就爆炸”,而是在某些结构场景下 diff 精化过深
  • 1.4.3:inline diff 这次也跟着改善了(41.44 → 38.40 ms),但 deep-inline / raw / block 仍然偏重: raw~76.20 msblock~79.69 msdeep-inline 也到 ~85.96 ms。所以 current workspace 这轮更像是 full parse + bare incremental 优化,不是 diff 全面优化
  • 1.4.4:这次补跑没有延续之前那种“继续往下走”的结论:inline diff 回到 ~40.55 ms,raw / block 也都比 1.4.3 略高;deep-inline 同样还在 1.4.3 那一档,所以它仍然不是“diff 全线回落”的版本

这里还要补一层背景:这组 raw 基准本身是高重复 worst-case。当前脚本会批量重复很多个同形 $$code(...)%...%end$$ 单元,再替换中间一段;这种输入对 diff 的唯一锚点非常不友好,因此更容易把 raw diff 推到悲观区间。额外补做的唯一内容 quick bench (约 1 MB 文档、5% 编辑)里,raw applyEditWithDiff 更接近 ~45 ms,dirty span 也更接近编辑窗口本身,而不是这张主表里的 ~76 ms / 近整篇脏区。所以这里的 raw diff 更适合解读为“高重复输入下的上界压力”,不宜直接当作日常非高重复编辑的代表值。

这和 changelog 是对得上的:

  • 1.3.8applyEditWithDiff(...) 从基础 range 升级到结构化 ops / patches,路径表达力变强,但代价也上来了
  • 1.3.9:补的是 isNoop 和保守 diff 契约,不是降成本,所以重场景还是重
  • 1.4.0:开始给 diff 精化加全局预算(比较数 / 锚点 / op / 子树规模 / wall-clock),不划算时直接退化为 coarse splice 或保守整树 diff,所以成本明显回落

diff 结果形态摘要

这组数据可以帮助理解为什么 1.4.0+ 会明显更快。

版本 inline deep-inline raw block
1.3.8 incremental=20ops~1064patches~1064 incremental=20ops~98patches~98 incremental=20ops~625patches~625 incremental=20ops~610patches~610
1.3.9 incremental=20ops~1064patches~1064 incremental=20ops~98patches~98 incremental=20ops~625patches~625 incremental=20ops~610patches~610
1.4.0 incremental=20ops~1patches~1 incremental=20ops~98patches~98 incremental=20ops~1patches~1 incremental=20ops~1patches~1
1.4.1 incremental=20ops~1patches~1 incremental=20ops~98patches~98 incremental=20ops~1patches~1 incremental=20ops~1patches~1
1.4.2 incremental=20ops~1patches~1 incremental=20ops~98patches~98 incremental=20ops~1patches~1 incremental=20ops~1patches~1
1.4.3 incremental=20ops~1patches~1 incremental=20ops~98patches~98 incremental=20ops~1patches~1 incremental=20ops~1patches~1
1.4.4 incremental=20ops~1patches~1 incremental=20ops~98patches~98 incremental=20ops~1patches~1 incremental=20ops~1patches~1

这基本说明:

  • 1.3.8 / 1.3.9 对很多场景都在产出大而细的 diff
  • 1.4.0 / 1.4.1 / 1.4.2 / 1.4.3inline / raw / block 更愿意在预算不划算时收敛到极小 diff
  • deep-inline 仍然保持较多 ops,所以它在 1.4.0 / 1.4.1 / 1.4.2 / 1.4.3 里依然是最贵的一档
  • 1.4.3 说明小 diff 形态仍不自动等于低 diff 成本:inline 这次吃到了收益,但 raw / block / deep-inline 依然偏贵

如果你的集成场景必须消费结构化 diff,那这页的建议会和裸 applyEdit 略有不同:

  • 只看增量更新本体1.2.4 ~ 1.3.0 依然很亮眼;如果限定在 1.4.0 / 1.4.1 / 1.4.2 / 1.4.3 / 1.4.41.4.3 / 1.4.4 现在是更强的 bare incremental 版本
  • 看“增量 + diff”综合成本1.4.0 / 1.4.1 仍然是 1.4.0 / 1.4.1 / 1.4.2 / 1.4.3 / 1.4.4 里更稳的集成点; 这次补跑后,1.4.4 并没有比 1.4.3 更漂亮:inline / raw / block diff 都略高,deep-inline 也仍然偏重

增量快照内存(1.2.0+

这组补测回答的是另一个很实际的问题:把增量快照真正建立起来、并把 tree / zones 物化之后,保留态内存大概是多少。

这里看的不是 parse 刚结束的瞬时峰值,而是:

  • 建立增量快照
  • 主动访问 doc.tree / doc.zones,避免 lazy getter 把占用低估
  • 强制 GC
  • 再看仍然保留下来的 heapUsed

也就是说,这里更接近“编辑器把增量文档真正拿在手上”时的内存形态。

版本 inline 建立后保留堆 deep-inline 建立后保留堆 raw 建立后保留堆 block 建立后保留堆
1.2.0 28.74 MB 29.68 MB 28.57 MB 28.80 MB
1.2.1 29.76 MB 30.69 MB 29.58 MB 29.80 MB
1.2.2 29.78 MB 30.70 MB 29.60 MB 29.81 MB
1.2.3 30.39 MB 31.31 MB 30.20 MB 30.43 MB
1.2.4 29.70 MB 30.64 MB 29.53 MB 29.76 MB
1.2.5 29.70 MB 30.64 MB 29.53 MB 29.76 MB
1.2.6 29.72 MB 30.65 MB 29.59 MB 29.77 MB
1.2.7 29.72 MB 30.66 MB 29.59 MB 29.78 MB
1.3.0 29.68 MB 30.62 MB 29.51 MB 29.73 MB
1.3.1 29.68 MB 30.62 MB 29.51 MB 29.74 MB
1.3.2 29.69 MB 30.62 MB 29.51 MB 29.74 MB
1.3.3 29.69 MB 30.62 MB 29.51 MB 29.74 MB
1.3.4 29.72 MB 30.66 MB 29.55 MB 29.78 MB
1.3.5 29.73 MB 30.66 MB 29.55 MB 29.78 MB
1.3.6 29.75 MB 30.68 MB 29.57 MB 29.79 MB
1.3.7 29.74 MB 30.68 MB 29.57 MB 29.80 MB
1.3.8 31.13 MB 32.05 MB 30.95 MB 31.18 MB
1.3.9 31.13 MB 32.06 MB 30.95 MB 31.18 MB
1.4.0 31.18 MB 32.10 MB 31.00 MB 31.23 MB
1.4.1 31.18 MB 32.10 MB 31.00 MB 31.23 MB
1.4.2 31.18 MB 32.10 MB 31.00 MB 31.23 MB
1.4.3 31.19 MB 32.11 MB 31.01 MB 31.24 MB
1.4.4 31.19 MB 32.12 MB 31.01 MB 31.24 MB

这张表可以读出两个台阶:

  • 1.2.1 ~ 1.3.7:建立态 retained heap 基本稳定在 29.5 ~ 30.7 MB
  • 1.3.8+:整体再抬高约 1.3 ~ 1.5 MB
  • 1.4.3 / 1.4.4:这两轮常数优化基本没有改变增量快照 retained heap,仍然稳定在 1.4.0 / 1.4.1 / 1.4.2 / 1.4.3 / 1.4.4 这一档更高的 retained-heap 水平

这和前面的 diff 结果其实是同一方向:1.3.8 开始不仅 diff 成本上来了,为了表达更细的结构化 diff,增量快照自身也更重了

一次编辑后的保留堆

下面继续看“建立快照后,再实际执行一次约 5% 编辑,并把更新后的 doc 保留下来”的 retained heap。

版本 inline 编辑后保留堆 deep-inline 编辑后保留堆 raw 编辑后保留堆 block 编辑后保留堆
1.2.0 38.60 MB 40.39 MB 38.28 MB 38.71 MB
1.2.1 39.61 MB 41.90 MB 39.78 MB 40.22 MB
1.2.2 40.16 MB 41.94 MB 39.83 MB 40.26 MB
1.2.3 40.76 MB 43.00 MB 40.44 MB 40.87 MB
1.2.4 33.04 MB 44.00 MB 41.98 MB 42.35 MB
1.2.5 33.05 MB 44.00 MB 41.92 MB 42.36 MB
1.2.6 33.04 MB 44.00 MB 41.92 MB 42.36 MB
1.2.7 33.05 MB 44.01 MB 41.93 MB 42.37 MB
1.3.0 33.01 MB 43.97 MB 41.88 MB 42.32 MB
1.3.1 33.01 MB 43.97 MB 41.90 MB 42.32 MB
1.3.2 33.01 MB 43.97 MB 41.88 MB 42.33 MB
1.3.3 33.01 MB 43.97 MB 41.90 MB 42.33 MB
1.3.4 33.00 MB 44.03 MB 41.93 MB 42.37 MB
1.3.5 33.01 MB 44.03 MB 41.94 MB 42.37 MB
1.3.6 33.02 MB 44.05 MB 41.96 MB 42.40 MB
1.3.7 33.02 MB 44.05 MB 41.96 MB 42.41 MB
1.3.8 34.41 MB 45.42 MB 43.34 MB 43.79 MB
1.3.9 34.41 MB 45.43 MB 43.34 MB 43.78 MB
1.4.0 34.46 MB 45.47 MB 43.39 MB 43.83 MB
1.4.1 34.46 MB 45.48 MB 43.39 MB 43.83 MB
1.4.2 34.46 MB 45.48 MB 43.41 MB 43.85 MB
1.4.3 34.48 MB 45.49 MB 43.42 MB 43.87 MB
1.4.4 34.48 MB 45.49 MB 43.42 MB 43.87 MB

这组数据更有意思:

  • inline 编辑1.2.4 开始突然掉到 33 MB 档,并一直维持到 1.3.7
  • deep-inline / raw / block 编辑1.2.4+ 并没有跟着一起降,说明“纯 inline zone 切分”主要改善的是 inline 型编辑的 retained shape
  • 1.3.8+:四类场景全部又抬了一档,基本都多了 ~1.3 ~ 1.5 MB

所以如果把“增量更新 + 结构化 diff + 保留内存”一起看,版本线的读法会更完整:

  • 1.2.4 ~ 1.3.7:裸增量很快,inline 场景 retained heap 也很好看
  • 1.3.8 / 1.3.9:diff 表达力上来,但速度和 retained heap 都一起抬升
  • 1.4.0+:diff 时间明显收敛,但 retained heap 仍保留在 1.3.8+ 那个更高档位
  • 1.4.3:说明这轮 current workspace 的改动几乎全是时间常数优化,不是 retained heap 优化

如果你只是关心“快照留在内存里到底贵不贵”,结论其实没那么吓人:1 MB 基线下,大多数版本建立态都在 30 MB 左右,做完一次编辑后大多落在 33 ~ 45 MB 这条带里。


病理深嵌套压力测试

输入形状:单链式 inline 嵌套,$$bold($$bold(...x...)$$)$$。 下表各行都使用这个形状,但层数按行分别取 5000 / 2 万 / 20 万 / 100 万 / 1000 万 / 2000 万 / 3000 万 / 4000 万 / 5000 万。

测试环境:

  • 历史列:鲲鹏 920 aarch64 / Node v24.14.0
  • 1.4.3 补测列:鲲鹏 920 aarch64 / Node v24.15.0

depthLimit 设为各层数 + 100(确保不触发降级)。本节的大规模高层数成绩统一使用放宽后的堆预算;具体内存占用和运行条件见表后说明。

API 1.1.0 1.1.1 1.1.2-1.1.4 1.1.5 1.1.6 1.4.3 current 1.4.4 current
parseStructural(5000) 栈溢出 ~7119 ms ~19 ms ~33.73 ms ~36.30 ms ~20.73 ms 同类,未单独重测
parseRichText(5000) ~9731 ms ~17216 ms ~23 ms ~41.12 ms ~34.58 ms ~29.05 ms ~35.50 ms(三次重跑均值)
parseStructural(20000) ~61 ms ~44.79 ms ~37.89 ms ~50.95 ms 同类,未单独重测
parseRichText(20000) ~61 ms ~134.35 ms ~53.58 ms ~87.53 ms ~125.85 ms(三次重跑均值)
parseStructural(200000) ~667 ms ~361.20 ms ~421.45 ms ~457.05 ms 同类,未单独重测
parseRichText(200000) ~844 ms ~1096.81 ms ~1209.64 ms ~1.15 s ~1.13 s(三次重跑均值)
parseStructural(1000000) ~2.8 s ~2.39 s ~2.50 s ~2.39 s 同类,未单独重测
parseRichText(1000000) ~6.4 s ~6.20 s ~6.00 s ~5.46 s ~5.49 s(三次重跑均值)
parseStructural(10000000) ~28.6 s ~17.3 s(单独重测) ~18.7 s(单独重测) ~21.15 s(单独重测) 同类,未单独重测
parseRichText(10000000) ~50 s 同类,未单独重测 同类,未单独重测 ~64.65 s(三次重跑均值) ~65.50 s(三次重跑均值)
parseStructural(20000000) ~89.9 s--max-old-space-size=12288 同类,未单独重测 同类,未单独重测 同类,未单独重测 同类,未单独重测
parseStructural(30000000) ~163.1 s--max-old-space-size=18432 同类,未单独重测 同类,未单独重测 同类,未单独重测 同类,未单独重测
parseStructural(40000000) ~210.0 s--max-old-space-size=24576 同类,未单独重测 同类,未单独重测 同类,未单独重测 同类,未单独重测
parseStructural(50000000) ~224.1 s--max-old-space-size=32768 同类,未单独重测 同类,未单独重测 同类,未单独重测 同类,未单独重测

注:1.1.5 / 1.1.6 目前单独重测到了 1000000parseStructural(10000000) 也有单独实测;1.4.3 这次则把 <= 10000000 的所有行都单独补测 了。1.4.4 这次只单独重测了 parseRichText(5000 / 20000 / 200000 / 1000000 / 10000000),其余 1.4.4 行仍按同类路径理解。 20000000+ 以上高层数行仍按 1.1.2+ 的全迭代同类路径理解,没有为本页单独重复跑分。

这次 1.4.3 补测可以按两段读:

  • 5000 ~ 1000000:current workspace 仍然处在"深链可跑、而且小中档常数不差"的区间,parseStructural(5000) 已经接近 1.1.2 ~ 1.1.4 那档,parseRichText(1000000) 也压到 ~5.46 s
  • 100000001.4.3 / 1.4.4 都依旧保持全迭代、能跑完,但常数已经不再是这条历史版本线里的最低点。 parseStructural(10000000)~21.15 s,比 1.1.5 / 1.1.6 的单独重测慢一些; 1.4.3parseRichText(10000000) 三次独立进程重跑分别落在 64.12 s / 64.87 s / 64.96 s, 均值约 ~64.65 s1.4.4 这次则落在 68.00 s / 62.24 s / 66.25 s,均值约 ~65.50 s。 这说明它不是偶发抖动,而是当前功能语义下稳定更重的路径; 也就是说后期版本的深链尾部成本并没有像 mixed document structural 主路径那样被完全拉回去

注:现在 public parseStructural(2000 万) 已可完成,因为 stripMeta 改成了直接构造 public tree, 不再经过 Map<IndexedStructuralNode, StructuralNode> 中间映射。上面的 2000 万 / 3000 万 / 4000 万 / 5000 万层成绩分别使用 NODE_OPTIONS=--max-old-space-size=12288 / 18432 / 24576 / 32768,结束时大约占用:

  • 1.4.3 parseStructural(1000 万):本次单独实测使用 --max-old-space-size=12288,parse 结束时约 5.59 GB heap / 5.83 GB RSS
  • 1.4.3 parseRichText(1000 万):本次单独实测使用 --max-old-space-size=13312,parse 结束时约 7.65 GB heap / 9.86 GB RSS;三次重跑的 RSS / heap 也几乎贴在同一档位
  • 1.4.4 parseRichText(1000 万):本次单独实测同样使用 --max-old-space-size=13312,三次重跑均值约 7.85 GB heap / 9.87 GB RSS
  • 2000 万:10.7 GB heap / 11.9 GB RSS
  • 3000 万:16.1 GB heap / 17.0 GB RSS
  • 4000 万:21.4 GB heap / 22.7 GB RSS
  • 5000 万:26.8 GB heap / 28.0 GB RSS

如果仍使用 Node 默认堆上限,高层 public tree 路径仍可能因内存预算不足而 OOM。

各版本爆栈阈值(同一环境,二分探测,±50 层):

API 1.1.0 1.1.1 1.1.2+
parseStructural ~3172 层 ~1611 层 无上限(全迭代)
parseRichText ~3269 层 ~3221 层 无上限(全迭代)

1.1.2 消除了三个独立的深嵌套瓶颈:

  1. 栈溢出——parseNodesrenderNodesstripMetaextractTextmaterializeTextTokens 五条递归路径全部改为显式栈迭代,嵌套深度仅受堆内存限制
  2. materializeTextTokens O(n²) 重复遍历(render 层)——每层 handler 调用都递归遍历 整棵子树。用 materializedArrays WeakSet 标记已处理子树,后续调用跳过 → O(n)
  3. findInlineClose / findTagArgClose O(n²) 前扫(structural 层)——每层 inline 标签都要前扫到匹配的关闭符。改为 lazy close:inline 子帧在父帧 text 上继续逐字符 扫描,遇到 )$$ 自动完成;gating 确定 inline-only 时跳过 getTagCloserType → O(n)

5000 层 parseRichText:1.1.1 ~17 s → 1.1.2 ~23 ms(提升 ~740 倍)。

1.1.2 以后,这一条版本线在“是否会爆栈”这个问题上已经完成代际切换:

  • 1.1.0 / 1.1.1 需要担心调用栈上限
  • 1.1.2+ 主要变成“堆预算够不够”的问题

因此这部分应该当成 栈安全最坏路径复杂度 的长期说明,而不是每个补丁版都要重新刷一遍的日常主基准。


轻量工具操作

测试输入约 ~200 KB(200,067 字符,6,321 个结构节点,9,165 个渲染 token)。数据为 5 次独立运行的平均值。

操作 耗时 说明
printStructural ~2.00 ms 无损 round-trip
buildZones ~0.74 ms 产出 1,897 个 zone
walkTokens ~0.50 ms 访问 9,165 次
mapTokens 恒等 ~1.21 ms 主要是包装成本
mapTokens 变换 ~1.55 ms 将整棵树中的 bold 改名为 strong

在编辑器管线里,这些工具操作相对 parser 本体几乎可以视为免费。