Files
goodbuddy/tests/support/streaming-markdown-perf.tsx
T
mesalogo 13ff182858 fix: reduce redundant work during chat and note startup
Avoid hidden activity renders, no-op message saves and attachment scans when pinning conversations. Reuse note body inspection and skip unchanged attachment reference writes while preserving recovery and cleanup behavior.

Skip unnecessary Markdown normalization and memoize subagent lookups without delaying live rich text. Add storage operation counts and real Electron rendering equivalence benchmarks; full-render speedup remains unproven.

Validation: 5397 tests passed, 84 skipped; typecheck passed. Lint passed excluding two existing untracked sidebar probe files.

Release note: Reduce repeated rendering and disk work during chat, conversation pinning and note startup while preserving existing content and interactions.
2026-09-28 20:53:43 +08:00

150 lines
7.3 KiB
TypeScript

import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client'
import * as current from '../../src/renderer/src/MarkdownRenderer'
import { UiLocaleProvider } from '../../src/renderer/src/i18n/UiLocaleProvider'
import { equivalenceCorpus, longAnswerCorpus, originalNormalize, performanceCorpus } from './streaming-markdown-corpus'
import '../../src/renderer/src/fonts'
import '../../src/renderer/src/styles.css'
type Instrumented = typeof current & { benchmarkNormalize: (content: string) => string }
const normalize = (current as Instrumented).benchmarkNormalize
const host = document.getElementById('root')!
host.className = 'markdown-content'
host.style.cssText = 'width: 800px; padding: 16px'
const root = createRoot(host)
function snapshot(container = host): string {
const clone = container.cloneNode(true) as HTMLElement
const ids = new Map<string, string>()
// Separate component instances receive different React useId values.
// Canonicalize only those IDs/references; retain all content and iframe srcdoc.
for (const element of clone.querySelectorAll('*')) {
for (const name of ['id', 'aria-controls', 'aria-labelledby', 'aria-describedby']) {
const value = element.getAttribute(name)
if (value && /^_r_[a-z0-9]+_$/u.test(value)) {
if (!ids.has(value)) ids.set(value, `react-id-${ids.size}`)
element.setAttribute(name, ids.get(value)!)
}
}
}
return clone.innerHTML
}
const stats = (values: number[]) => {
const sorted = [...values].sort((a, b) => a - b)
return { median: (sorted[Math.floor((sorted.length - 1) / 2)]! + sorted[Math.floor(sorted.length / 2)]!) / 2,
p95: sorted[Math.ceil(sorted.length * 0.95) - 1] }
}
async function run() {
const originalPath = '/src/renderer/src/MarkdownRenderer.tsx?streaming-original'
const original = await import(/* @vite-ignore */ originalPath) as Instrumented
const render = (Component: typeof current.MarkdownRenderer, content: string, renderHtml = false) => {
flushSync(() => root.render(<UiLocaleProvider initialPreference="en-US">
<Component renderHtml={renderHtml}>{content}</Component>
</UiLocaleProvider>))
}
// Warm fonts, styles, React, Markdown, and KaTeX before recording samples.
render(current.MarkdownRenderer, performanceCorpus[1]!.content)
await document.fonts.ready
const results = []
for (const corpus of performanceCorpus) {
const prefixes = Array.from({ length: 24 }, (_, i) => corpus.content.slice(0, Math.ceil(corpus.content.length * (i + 1) / 24)))
const renderTimes: number[] = []
const normalizeTimes: number[] = []
let checksum = 0
for (let round = 0; round < 4; round++) {
render(current.MarkdownRenderer, '')
for (const prefix of prefixes) {
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()))
const start = performance.now()
render(current.MarkdownRenderer, prefix)
// Include the synchronous DOM commit and Chromium style/layout, not frame waiting.
void host.offsetHeight
if (round > 0) renderTimes.push(performance.now() - start)
}
}
// Batch sub-millisecond calls to exceed Chromium timer resolution.
for (let round = 0; round < 4; round++) {
for (const prefix of prefixes) {
const start = performance.now()
for (let i = 0; i < 100; i++) checksum += normalize(prefix).length
if (round > 0) normalizeTimes.push((performance.now() - start) / 100)
}
}
results.push({ corpus: corpus.name, characters: corpus.content.length, samples: renderTimes.length,
renderMs: stats(renderTimes), normalizeMs: stats(normalizeTimes), checksum })
}
const originalHost = document.createElement('div')
originalHost.className = host.className
originalHost.style.cssText = host.style.cssText
document.body.append(originalHost)
const originalRoot = createRoot(originalHost)
const longSamples = { original: [] as number[], current: [] as number[] }
const prefixLengths = Array.from({ length: 6 }, (_, i) => Math.ceil(longAnswerCorpus.length * (i + 1) / 6))
let longDomChecks = 0
try {
// One warm-up sweep, three measured sweeps: 48 renders total, on persistent roots.
for (let round = 0; round < 4; round++) {
render(current.MarkdownRenderer, '')
flushSync(() => originalRoot.render(<UiLocaleProvider initialPreference="en-US">
<original.MarkdownRenderer>{''}</original.MarkdownRenderer>
</UiLocaleProvider>))
for (const [index, length] of prefixLengths.entries()) {
const prefix = longAnswerCorpus.slice(0, length)
if (normalize(prefix) !== originalNormalize(prefix)) throw new Error(`Long normalizer mismatch: ${length}`)
const order = (round + index) % 2 === 0
? ['original', 'current'] as const : ['current', 'original'] as const
for (const variant of order) {
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()))
const start = performance.now()
if (variant === 'original') {
flushSync(() => originalRoot.render(<UiLocaleProvider initialPreference="en-US">
<original.MarkdownRenderer>{prefix}</original.MarkdownRenderer>
</UiLocaleProvider>))
void originalHost.offsetHeight
} else {
render(current.MarkdownRenderer, prefix)
void host.offsetHeight
}
if (round > 0) longSamples[variant].push(performance.now() - start)
}
// Compare outside timing, including the complete 100 KiB answer on every sweep.
if (snapshot() !== snapshot(originalHost)) throw new Error(`Long DOM mismatch: ${length}`)
longDomChecks++
}
}
if (!host.querySelector('h2') || !host.querySelector('pre code') || !host.querySelector('table')) {
throw new Error('Long-answer prose/code/table formatting missing')
}
} finally {
flushSync(() => originalRoot.unmount())
originalHost.remove()
}
const longAnswer = { characters: longAnswerCorpus.length, bytes: new TextEncoder().encode(longAnswerCorpus).length,
prefixLengths, warmupSweeps: 1, measuredSweeps: 3, samplesPerVariant: longSamples.current.length,
originalRenderMs: stats(longSamples.original), currentRenderMs: stats(longSamples.current),
rawRenderMs: longSamples, domChecks: longDomChecks, finalDomEquivalent: true }
let prefixesChecked = 0
let domChecks = 0
for (const content of equivalenceCorpus) {
for (let end = 0; end <= content.length; end++) {
const prefix = content.slice(0, end)
if (normalize(prefix) !== originalNormalize(prefix)) throw new Error(`Normalizer mismatch: ${JSON.stringify(prefix)}`)
prefixesChecked++
for (const renderHtml of [false, true]) {
render(original.MarkdownRenderer, prefix, renderHtml)
const expected = snapshot()
render(current.MarkdownRenderer, prefix, renderHtml)
if (snapshot() !== expected) throw new Error(`DOM mismatch (${renderHtml}): ${JSON.stringify(prefix)}`)
domChecks++
}
}
}
render(current.MarkdownRenderer, '# Live\n\n**formatted** and \\(x\\)')
if (!host.querySelector('h1') || !host.querySelector('strong') || !host.querySelector('.katex')) {
throw new Error('Real Markdown formatting missing')
}
return { results, longAnswer, prefixesChecked, domChecks }
}
Object.assign(window, { streamingMarkdown: run })