Stream Parser 与长文本策略
May 14, 2026 · View on GitHub
这份文档描述当前 parser/stream 的默认性能策略、兼容性边界,以及如何验证优化没有破坏原有行为。
设计原则
- 普通调用方继续使用
md.parse(src)/md.render(src),不要求学习新的默认 API。 - 长文本优化优先做成内部自动策略,而不是把选择成本转嫁给调用方。
- 当前仓库已有测试覆盖的行为是硬基线;如果和 upstream
markdown-it存在差异,优先保持本仓库行为。 - 兼容性优先于跑分,不删除
Token/state上已有兼容属性。
默认策略
md.parse(src)
- 小中型字符串默认走 plain full parse。
- 没有插件且 parser ruler 未被修改时,较长的 one-shot 字符串可以自动进入内部大输入路径。
- 一旦调用
.use()或修改 core/block/inline ruler,默认回到 plain full parse;需要 chunked 行为时显式开启experimental.fullChunkedFallback。 autoUnbounded仍然保留,但只在现有阈值命中且满足默认安全条件时才接管。- 诊断信息通过
getParseDiagnostics(env)读取,用于确认最终走的是plain、full-chunk还是auto-unbounded。
md.stream.parse(src)
- 相同源码重复解析优先命中 cache。
- 安全 append 优先尝试 append fast-path。
- 适合重解析尾部容器时优先走 tail reparse。
- 大文档的一次性 stream 首次解析会按配置进入 chunked fallback。
- 长文本纯 append 场景会自动切到内部 unbounded-backed append,只消费新增 delta。
- 中间编辑、不安全边界、reference-definition 风险、merge 失败时立即回退到现有
tail/chunked/full路径。
chunked / streaming 正确性说明
Markdown 并不总是 chunk-local 的语言。某些语法依赖整篇文档状态,例如 reference definitions、footnote definitions、abbreviation definitions,以及插件自定义的全局状态。
chunkedParse() 和完整字符串的 unbounded parsing 默认采用 correctness-first 策略:遇到已知全局状态语法时会 fallback 到 full parse。
Iterable/sink 解析偏 streaming 场景;它不一定能在提交前面的 chunk 前看到后续的 reference/footnote/abbr definition。因此如果需要严格 full-parse 等价,包含全局定义的文档应优先使用完整字符串解析,或避免过早 flush。
你可以显式关闭 fallback:
chunkedParse(md, source, env, {
fallbackOnGlobalState: false,
})
关闭 fallback 属于性能优先模式;对于包含全局状态的文档,输出可能和 full parse 不一致。
md.stream.stats() 里现在会保留:
cacheHitsappendHitsunboundedAppendHitstailHitschunkedParsesfullParseslastMode
这些字段用于确认“结果对了”之外,默认策略是否真的走到了预期路径。
兼容性约束
优化过程中不允许删除、不重命名、或改变以下兼容属性的基本语义:
Token.typeToken.tagToken.attrsToken.mapToken.nestingToken.levelToken.childrenToken.contentToken.markupToken.infoToken.metaToken.blockToken.hiddenState.prototype.TokenStateBlock.prototype.TokenStateInline.prototype.Token
同时避免把现有 token/state 字段替换成插件不兼容的惰性代理、只读对象或不同类型。
诊断与 profiling
Rule profiler
在开发态基准里,可以通过在 env 上打开 __mdtsProfileRules 收集 rule-family profiling。结果会写入:
env.__mdtsRuleProfile
目前覆盖:
coreblockinlineinline2
每条规则会记录:
callshitsinclusiveMsmedianMsmaxMsnormalCallssilentCalls
Strategy diagnostics
import { getParseDiagnostics } from 'markdown-it-ts/experimental'
默认策略决策会写入:
getParseDiagnostics(env)?.strategy
常见路径包括:
plainfull-chunkauto-unboundedstream-cachestream-appendstream-unbounded-appendstream-tailstream-chunkedstream-full
性能门禁
性能验证是独立门禁,不并入普通 pnpm test。
常用命令:
pnpm test
pnpm perf:families
pnpm perf:strategies
pnpm perf:strategy:check
pnpm perf:gate
说明:
pnpm perf:families生成 family 热点报告,输出docs/perf-family-hotspots.json和docs/perf-family-hotspots.mdpnpm perf:strategies生成长文本策略矩阵,输出docs/perf-large-defaults.json、docs/perf-large-defaults.md、docs/parse-strategy-matrix.mdpnpm perf:strategy:check校验默认策略与最佳已测方案的 gap、以及长文本 SLApnpm perf:gate组合执行perf:strategies和perf:strategy:check
如何读报告
docs/perf-family-hotspots.md看每个 fixture 最慢的 rule family,决定先优化哪里。docs/perf-large-defaults.md看不同长度下 full/stream/advanced 路径的完整跑分。docs/parse-strategy-matrix.md看默认 API 在不同长度和场景下的最佳策略解释。
使用建议
- 普通 one-shot 渲染继续优先使用
md.parse(src)/md.render(src)。 - 长文本 append-heavy 编辑器优先使用
MarkdownIt({ stream: true })。 - 如果输入天然就是 chunk 流,或者你明确需要控制内存保留量,再使用
parseIterable、parseIterableToSink、UnboundedBuffer。 - 如果是中间编辑而不是 append,预期仍然会退回 tail/full 路径,不要把 stream 当成任意 diff parser。
验证清单
每次做 parser 优化时,至少确认以下几件事:
- 现有 parser/renderer/stream/plugin tests 全部通过。
Tokenshape 和state.Token兼容回归通过。- stream/full token parity 通过,尤其是
map、hidden、info、attrs、children。 - 固定 seed 的编辑序列测试通过,保证每一步
stream.parse(current)渲染结果都等于md.render(current)。 - family 热点报告能解释本次优化命中的规则。
- 默认策略矩阵没有偏离该长度档已测最佳方案。