chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
// Benchmark: cost of the rich-markdown Table-of-Contents parse that fires on
|
||||
// every debounced content change while typing.
|
||||
//
|
||||
// Before the fix (RichMarkdownEditor.tsx), buildMarkdownTableOfContents() ran a
|
||||
// full-document remark parse on EVERY content change even when the TOC panel was
|
||||
// closed (the default), then discarded the result. This benchmark mirrors that
|
||||
// parse workload across document sizes and reports the main-thread time spent
|
||||
// per "typing burst" so the wasted work is quantified.
|
||||
//
|
||||
// The fix gates the memo on showTableOfContents, so when the panel is closed the
|
||||
// per-burst cost drops to ~0 (a stable empty array, no parse). This script
|
||||
// measures the "panel closed" cost that the gate eliminates.
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import remarkFrontmatter from 'remark-frontmatter'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkParse from 'remark-parse'
|
||||
import { unified } from 'unified'
|
||||
|
||||
const DOC_HEADINGS = Number.parseInt(process.env.ORCA_TOC_BENCH_HEADINGS ?? '400', 10)
|
||||
const PARAGRAPHS_PER_HEADING = Number.parseInt(process.env.ORCA_TOC_BENCH_PARAS ?? '6', 10)
|
||||
// Number of debounced content changes in a sustained typing burst. The editor
|
||||
// debounces serialize at 300ms, so ~200 changes ≈ a minute of steady typing.
|
||||
const CONTENT_CHANGES = Number.parseInt(process.env.ORCA_TOC_BENCH_CHANGES ?? '200', 10)
|
||||
const WARMUP = Number.parseInt(process.env.ORCA_TOC_BENCH_WARMUP ?? '5', 10)
|
||||
|
||||
for (const [name, value] of [
|
||||
['ORCA_TOC_BENCH_HEADINGS', DOC_HEADINGS],
|
||||
['ORCA_TOC_BENCH_PARAS', PARAGRAPHS_PER_HEADING],
|
||||
['ORCA_TOC_BENCH_CHANGES', CONTENT_CHANGES],
|
||||
['ORCA_TOC_BENCH_WARMUP', WARMUP]
|
||||
]) {
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new Error(`${name} must be a positive integer, received ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror of buildMarkdownTableOfContents()'s parse + heading walk
|
||||
// (src/renderer/src/components/editor/markdown-table-of-contents.ts). Kept inline
|
||||
// so the benchmark exercises the real remark pipeline without bundling the TS.
|
||||
function buildMarkdownTableOfContentsLike(markdown) {
|
||||
const tree = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkFrontmatter, ['yaml', 'toml'])
|
||||
.parse(markdown)
|
||||
const headings = []
|
||||
const visit = (node) => {
|
||||
if (node.type === 'heading' && typeof node.depth === 'number') {
|
||||
let title = ''
|
||||
const collect = (n) => {
|
||||
if (typeof n.value === 'string') {
|
||||
title += n.value
|
||||
}
|
||||
for (const child of n.children ?? []) {
|
||||
collect(child)
|
||||
}
|
||||
}
|
||||
collect(node)
|
||||
if (title) {
|
||||
headings.push({ depth: node.depth, title })
|
||||
}
|
||||
}
|
||||
for (const child of node.children ?? []) {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
visit(tree)
|
||||
return headings
|
||||
}
|
||||
|
||||
function buildDocument(headings, parasPerHeading) {
|
||||
const lines = ['---', 'title: Benchmark Document', 'tags: [perf, toc]', '---', '']
|
||||
for (let h = 0; h < headings; h += 1) {
|
||||
const level = (h % 3) + 1
|
||||
lines.push(`${'#'.repeat(level)} Section ${h} \`code\` **bold** [link](https://example.com)`)
|
||||
lines.push('')
|
||||
for (let p = 0; p < parasPerHeading; p += 1) {
|
||||
lines.push(
|
||||
`Paragraph ${p} for section ${h} with *emphasis*, \`inline code\`, and ` +
|
||||
'some filler text to give the parser realistic body content to walk over.'
|
||||
)
|
||||
lines.push('')
|
||||
}
|
||||
if (h % 5 === 0) {
|
||||
lines.push('| Col A | Col B | Col C |', '| --- | --- | --- |', '| 1 | 2 | 3 |', '')
|
||||
}
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function median(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
const mid = Math.floor(sorted.length / 2)
|
||||
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
|
||||
}
|
||||
|
||||
function measureDoc(headings, parasPerHeading) {
|
||||
const baseDoc = buildDocument(headings, parasPerHeading)
|
||||
// Simulate typing: each content change appends one character so `content`
|
||||
// changes identity every time (matching the memo dependency in the editor).
|
||||
const perChange = []
|
||||
for (let i = 0; i < WARMUP + CONTENT_CHANGES; i += 1) {
|
||||
const doc = `${baseDoc}\nedit-${i}`
|
||||
const t0 = performance.now()
|
||||
const toc = buildMarkdownTableOfContentsLike(doc)
|
||||
const elapsed = performance.now() - t0
|
||||
if (i >= WARMUP) {
|
||||
perChange.push(elapsed)
|
||||
}
|
||||
if (toc.length === 0) {
|
||||
throw new Error('benchmark produced an empty TOC; document generation is broken')
|
||||
}
|
||||
}
|
||||
const total = perChange.reduce((sum, value) => sum + value, 0)
|
||||
return {
|
||||
bytes: Buffer.byteLength(baseDoc, 'utf8'),
|
||||
headings,
|
||||
perChangeMedianMs: median(perChange),
|
||||
perChangeMaxMs: Math.max(...perChange),
|
||||
burstTotalMs: total
|
||||
}
|
||||
}
|
||||
|
||||
const sizes = [
|
||||
{ headings: Math.round(DOC_HEADINGS / 8), paras: PARAGRAPHS_PER_HEADING },
|
||||
{ headings: Math.round(DOC_HEADINGS / 2), paras: PARAGRAPHS_PER_HEADING },
|
||||
{ headings: DOC_HEADINGS, paras: PARAGRAPHS_PER_HEADING }
|
||||
]
|
||||
|
||||
console.log('Markdown TOC parse benchmark (cost incurred per content change while typing)')
|
||||
console.log(
|
||||
`changes/burst=${CONTENT_CHANGES} warmup=${WARMUP} paras/heading=${PARAGRAPHS_PER_HEADING}\n`
|
||||
)
|
||||
console.log(
|
||||
' doc size │ headings │ per-change median │ per-change max │ burst total (closed-panel waste)'
|
||||
)
|
||||
console.log(
|
||||
' ─────────┼──────────┼───────────────────┼────────────────┼──────────────────────────────────'
|
||||
)
|
||||
for (const size of sizes) {
|
||||
const result = measureDoc(size.headings, size.paras)
|
||||
const kib = (result.bytes / 1024).toFixed(0).padStart(5)
|
||||
const headingsCol = String(result.headings).padStart(8)
|
||||
const med = `${result.perChangeMedianMs.toFixed(2)} ms`.padStart(17)
|
||||
const max = `${result.perChangeMaxMs.toFixed(2)} ms`.padStart(14)
|
||||
const burst = `${result.burstTotalMs.toFixed(0)} ms`.padStart(10)
|
||||
console.log(` ${kib} KiB │ ${headingsCol} │ ${med} │ ${max} │ ${burst}`)
|
||||
}
|
||||
console.log(
|
||||
'\nWith the fix, every row above costs ~0 ms while the TOC panel is closed' +
|
||||
' (the default state): the parse is skipped and a stable empty array is returned.'
|
||||
)
|
||||
Reference in New Issue
Block a user