zh CN 性能 - chiba233/yumeDSL GitHub Wiki
性能
这一页集中整理 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 |
parseStructural 后 heapUsed 更低;其中 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.0的parseRichText已不具备当前参考意义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 KB从1.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 KB从1.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 快版本,但形态已经更像“部分延续、部分回弹”:parseRichText在200 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 上出现了本轮最漂亮的一档heapUsed1.3.6+:1 MB从~85 MB掉到~77 MB,约 -9%;但2 MB又从~141 MB抬到~160 MB,约 +13% 。这更像是分配形态改了: 中档文档的活跃对象窗口被收紧了,但更大文档会同时保留更多结构对象 / 边界元数据,所以不是“全面更省”,而是新的内存形态1.4.3:小中档内存有改善:200 KB从21.50 MB降到20.58 MB,1 MB从77.07 MB降到71.53 MB。 但大文档和深嵌套方向是反的:2 MB从159.80 MB升到162.32 MB(+1.6%),20k nested从17.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 区间”- 因此如果你关心的是小中型文档 footprint,
1.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 MB 和 20k nested 没有低于 1.1.5 |
1.1.7 |
架构同 1.1.6,优化集中在 render 层和扫描期常数:render 层 trimBlockBoundaryTokens 不再全量 clone children 数组;flushBuffer 对常见 1–2 对 segment 直接拼接避免临时数组 |
200 KB 更低,2 MB 和 20k 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的 rawapplyEdit从1.4.1的~16.00 ms回升到~21.57 ms,约 +35%,所以这不是“零代价清理”1.4.3:bareapplyEdit这次大体是成功的:inline回到~17.96 ms、raw到~15.76 ms、block到~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/block比1.4.3略高,所以它更像“延续同一档位”,而不是继续全面下探
本轮增量补测的统一口径
- 基线文档约
1 MB - 每个场景都把单次编辑控制在约
5% inline:修改普通 inline 标签内容deep-inline:修改深层嵌套 inline 区段内容raw:修改 raw 标签正文block:修改 block 标签正文1.2.0使用 low-levelparseIncremental/updateIncremental1.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.9:deep-inline、raw、block的 diff 成本都很重,尤其deep-inline接近8.4x ~ 8.6x1.4.0+:deep-inline仍然最贵,但已经从接近200 ms掉到60 ~ 80 ms档;raw/block也从80 ~ 100 ms档回到50 ~ 66 msinline这一项反而一直相对稳定,说明问题不在“只要开 diff 就爆炸”,而是在某些结构场景下 diff 精化过深1.4.3:inline diff 这次也跟着改善了(41.44 → 38.40 ms),但deep-inline/raw/block仍然偏重:raw是~76.20 ms,block是~79.69 ms,deep-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.8:applyEditWithDiff(...)从基础 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=20;ops~1064,patches~1064 |
incremental=20;ops~98,patches~98 |
incremental=20;ops~625,patches~625 |
incremental=20;ops~610,patches~610 |
1.3.9 |
incremental=20;ops~1064,patches~1064 |
incremental=20;ops~98,patches~98 |
incremental=20;ops~625,patches~625 |
incremental=20;ops~610,patches~610 |
1.4.0 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~98,patches~98 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~1,patches~1 |
1.4.1 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~98,patches~98 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~1,patches~1 |
1.4.2 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~98,patches~98 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~1,patches~1 |
1.4.3 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~98,patches~98 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~1,patches~1 |
1.4.4 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~98,patches~98 |
incremental=20;ops~1,patches~1 |
incremental=20;ops~1,patches~1 |
这基本说明:
1.3.8/1.3.9对很多场景都在产出大而细的 diff1.4.0/1.4.1/1.4.2/1.4.3对inline/raw/block更愿意在预算不划算时收敛到极小 diffdeep-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.4,1.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 MB1.3.8+:整体再抬高约1.3 ~ 1.5 MB1.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.7deep-inline/raw/block编辑:1.2.4+并没有跟着一起降,说明“纯 inline zone 切分”主要改善的是 inline 型编辑的 retained shape1.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 目前单独重测到了 1000000,parseStructural(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 s10000000:1.4.3/1.4.4都依旧保持全迭代、能跑完,但常数已经不再是这条历史版本线里的最低点。parseStructural(10000000)约 ~21.15 s,比1.1.5/1.1.6的单独重测慢一些;1.4.3的parseRichText(10000000)三次独立进程重跑分别落在 64.12 s / 64.87 s / 64.96 s, 均值约 ~64.65 s;1.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 RSS1.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 消除了三个独立的深嵌套瓶颈:
- 栈溢出——
parseNodes、renderNodes、stripMeta、extractText、materializeTextTokens五条递归路径全部改为显式栈迭代,嵌套深度仅受堆内存限制 materializeTextTokensO(n²) 重复遍历(render 层)——每层 handler 调用都递归遍历 整棵子树。用materializedArraysWeakSet 标记已处理子树,后续调用跳过 → O(n)findInlineClose/findTagArgCloseO(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 本体几乎可以视为免费。