Perf/core hot paths - #350
Open
tobegold574 wants to merge 2 commits into
Open
tobegold574 wants to merge 2 commits into
tobegold574 wants to merge 2 commits into
Conversation
光标移动/击键每次触发的事务中,存在大量只取决于文档内容、与光标无关的
重复计算。本提交将其全部缓存化,行为与公开 API 零变化:
- live-preview:build() 缓存升级为 {docText(Text 身份), doc, ast,
codeTokens, ranges},纯光标移动复用全部中间产物;缓存命中判断由
O(n) 字符串比较改为 O(1) Text 身份比较;每次 build 的
doc.toString() 从 2~3 次降为 1 次。
- live-preview:代码块高亮定位 doc.slice(blockFrom) 改为
doc.indexOf,消除每块每次重建 O(文档长度) 的尾部拷贝;全局 token
数组由逐块头部线性重扫(O(块数×token 总数))改为二分查找。
- live-preview-highlight:新增 (lang, code, contentStart) 二级 LRU,
缓存命中不再逐 token 重建 rebase 数组。
- widget-extension:空 widget 定义检查前置到 doc.toString() 之前;
AST 与文档串按 Text 身份 + transform 版本缓存(widget 显隐仍随
光标实时重算,行为不变)。
- transaction-pipeline:无 filter/listener 时整体旁路(与管线接入前
编辑器自身 fallback 一致);documentBefore/After 改惰性记忆 getter
(读取值不变);空注册表返回共享空数组,dispatch 不再做
Array.from+filter+sort。
- event-emitter/editor:新增内部 hasListeners(),无监听者时跳过
斜杠状态全量计算与 selectionChange payload 构造。
- wikilinks:StateField 扫描结果按 Text 身份缓存,光标移动复用
文档串与匹配列表。
- live-preview-table:仅纯计算——estimatedHeight 加 memo(CM6 每次
高度图刷新都会读)、visualLength 改 charCode 区间判断(代理对行为
精确等价)、Math.max(1,...map()) 展开改标量循环;未触碰任何
交互状态/rAF/事件逻辑。
1MB 文档同机配对实测:光标移动 -30.3%(532.9→371.6 ms/次),
击键方差 ±44%→±3.8%(GC 尖峰消除)。
- perf-invariants.test.ts:6 个用例钉住各缓存依赖的不变量—— ranges 对 selection 不敏感、光标进出切换×2(缓存复用路径)、 widget 可见性跨缓存 AST 更新、wikilink 扫描缓存一致性、 惰性 documentBefore/After 取值、高亮 rebase memo 实例稳定 (该用例同时暴露并锁定了 hljs scope/kind 存量 bug 的修复)。 - test/perf/hot-paths.bench.ts:~1MB 合成文档端到端基准 (open+destroy / cursor-move / keystroke,vitest bench)。 - test/perf/micro-costs.bench.ts:逐项度量被消除工作的确定性 微成本基准(无 DOM、无 CodeMirror view)。 基准未接入 CI(jsdom 端到端噪音大,仅供本地配对对比)。
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR 标题:
perf(core): eliminate redundant full-document work on update hot paths
描述正文:
Summary / 摘要
消除编辑器每次事务热路径(光标移动与击键)上重复执行的全文档操作
(字符串物化、AST 重遍历、全文档正则扫描)。公开 API 零变化、渲染
输出完全一致;同时修复一个导致围栏代码块从未真正被语法高亮过的存量
bug(1 行,可独立拆分)。
Motivation / 背景与动机
openspec/changes/add-missing-features任务 4.2("Performance benchmarks")跟踪的基准缺失。
不新增、不修改任何公开 API,装饰输出完全一致(由新增的不变量测试
钉住)。GOVERNANCE §3 要求先开 proposal 的是公开 API 新增/新插件/
破坏性变更/安全敏感工作,均不适用。如维护者认为本次范围属于
"significant performance work",我可以补一个 proposal。
核心发现:
collectLivePreviewRanges()是(ast, doc)的纯函数——它的
selection参数从未被函数体读取(光标感知逻辑在buildDecorations里做)。但在每次光标移动时,编辑器都会重新遍历整棵 AST、执行两次全文档
正则扫描(wikilink + 图片)并重新排序;此外 live-preview、widget 扩展、
wikilinks StateField、事务管线和斜杠菜单路径每次更新合计要做 2~3 次
完整的
doc.toString()全文档物化。Changes / 变更内容
全部功能性改动位于
packages/core(9 个文件,+367/−87):live-preview-ranges.ts:移除从未被读取的selection参数(保留为可选参数以兼容旧调用方),并写明输出与光标无关这一前提。
live-preview.ts:build()缓存升级为{docText (Text 身份), doc, ast, codeTokens, ranges}——纯光标移动复用全部中间产物;缓存命中判断从 O(n) 字符串比较改为 O(1) 身份比较;每次 build 的
toString()从 2~3 次降为 1 次。doc.slice(blockFrom)→doc.indexOf("\n", blockFrom)。(原先每块都从下标 0 线性重扫,O(块数 × token 总数))。
live-preview-highlight.ts:scope || kind):hljs 令牌树遍历器匹配的kind字段在 hljs 实际输出中不存在(实际字段是scope),emit()恒产出 0 个 token——围栏代码块此前从未真正被高亮过。本 PR 之后代码块首次渲染出高亮配色。如维护者希望拆成独立 PR
也很容易,去掉它不影响任何性能改动。
(lang, code, contentStart)二级 LRU,缓存命中不再在每次重建时逐 token 重新分配 rebase 数组。
widget-extension.ts:空 widget 定义检查前置到doc.toString()之前;解析出的 AST 与文档串按 Text 身份 + transform 版本缓存
(widget 范围仍按光标每次重算——其显隐依赖光标,行为不变)。
transaction-pipeline.ts:未注册任何 filter/listener 时整体旁路(与管线接入前编辑器自身的 fallback 行为一致);
documentBefore/After改为惰性记忆 getter(读取时值完全一致);空注册表直接返回共享空数组。
event-emitter.ts+editor.ts:新增内部hasListeners();无监听者时跳过斜杠状态全量计算与 selectionChange payload 构造。
wikilinks.ts:按 Text 身份缓存扫描结果——光标移动复用文档串与匹配列表。
live-preview-table.ts(仅纯计算):estimatedHeight加 memo(source 在实例内不可变);
visualLength从逐字符 RegExp 改为charCode 区间判断(代理对行为精确等价);
Math.max(1, ...map())参数展开改为标量循环。未触碰任何交互状态、rAF 或事件逻辑。
新增文件:
packages/core/test/perf-invariants.test.ts(6 个不变量测试)、
packages/core/test/perf/hot-paths.bench.ts与micro-costs.bench.ts(vitest bench;未接入 CI)。Why this matters at scale / 量级
在 ~1MB 合成文档(模拟 demo 真实内容混合)上,同机背靠背配对测量
(main vs 本分支,
vitest bench):确定性微成本(rme ≤ 1.7%):旧实现在 1MB 文档上每次光标移动仅
collectLivePreviewRanges一项就要付 ~173–309ms,外加 2 次全文档toString()、一次重复的全文档 wikilink 扫描,以及无人监听时的斜杠命令重排序。击键方差从 ±44% 坍缩到 ±3.8% 反映的是 GC 尖峰的消除。
所有成本随文档大小线性放大(98KB 时约 2.4ms/次光标移动)。
复现:
pnpm vitest bench --run packages/core/test/perf/hot-paths.bench.ts。Testing / 测试
pnpm test——packages/core:282/282 全过(18 个文件),含 6 个新增不变量测试。全仓说明:Windows 本机存在 12 个存量
失败(
plugin-host-broker.test.ts的 symlink EPERM 需要管理员权限),以及
app-runtime-integration.test.ts在全量并发下的偶发抖动;两者在干净
main上同样复现(broker 文件 11 failed),与本 PR 无关。
app-runtime-integration在本分支单独运行 8/8 通过。pnpm build全绿);pnpm typecheck全绿。perf-invariants.test.ts钉住各缓存依赖的不变量——ranges 对 selection 不敏感;缓存 range 上光标进出
切换 ×2;widget 跨缓存 AST 更新的可见性切换;wikilink 扫描
缓存一致性;惰性
documentBefore/After取值;高亮 rebase memo实例稳定(正是这个用例暴露了上面的 hljs bug)。
通过(
app-runtime-integration单独运行 8/8),表格交互另有模拟事件单测覆盖(点击进入编辑、表格外失焦、选区恢复——均为
packages/core/test中派发的真实 DOM 事件)。CLAUDE.md 规则 7的 6 条人工交互路径本次未逐一手点:
live-preview-table.ts的diff 只涉及纯计算(高度估算),未触碰任何交互状态 / rAF /
事件代码。如 reviewer 需要,我可以补一轮人工验证。
Commits(2 段,每段独立通过 typecheck 与测试套件)
perf(core): 消除更新热路径上的冗余全文档操作—— 9 个源文件的全部性能改动
test(core): 新增热路径缓存不变量回归测试与基准—— 6 个回归用例 + 2 个 vitest bench 基准
Out of scope(审计中识别但本 PR 刻意不做)
requestAnimationFrame循环)——位于 CLAUDE.md 12 条规则约束的交互状态机内,改动必须人工过全部 6 条鼠标交互路径,超出本 PR
"零行为变化"的边界,宜作为独立 PR 并附人工验证记录。
getBoundingClientRect强制布局,同样位于交互路径内,理由同上。emitChange惰性 AST(无 onChange 监听者时跳过 mdast 重建,getAst()按需物化)——涉及getAst()的求值时机语义,需要单独设计与更完整的时序测试。
不足以支撑 pass/fail 门槛,需要配对测量基建;对应 roadmap 4.2,
建议以 nightly 趋势记录的形式独立推进。
Compliance / 合规自检
不勾选,说明见下
AI 使用说明:本 PR 在热路径审计、实现、测试与基准等环节大量
使用了 AI 辅助,按 GOVERNANCE §6.2 如实披露,而非声称为纯
手写。我对每一行改动做过逐行审阅,在源码内以中文注释标注了
改动原因与作者/日期,能够逐一解释并辩护各设计决策;所有测量
数据可通过随 PR 提交的基准文件复现。
.env/ 个人 vault 数据。Checklist / 自检清单
perf(core):,scope 在许可清单内)。(
EventEmitter#hasListeners为内部方法,类未导出)。live-preview-table.ts→ 已核对 CLAUDE.md 的 12 条表格规则:规则 1–6、8–12 涉及交互状态、rAF 轮询、拖拽、
contentEditable 与表格 StateField——这些代码均未触碰;
规则 7 的覆盖情况见 Testing 一节。
Motivation)。
Screenshots / Recordings
无——无可见 UI 变化。(hljs bug 修复后围栏代码块首次显示出语法
高亮配色,这是修复生效的表现,不是回归。)