Files
goodbuddy/tests/support/streaming-markdown-perf.tsx
mesalogo c8f327fc4e perf: reduce streaming render cost and refresh activity page on demand
- Segment streaming Markdown so completed blocks are reused and only the growing tail is re-parsed; fall back to whole-document parsing for document-wide constructs
- Memoize conversation sidebar rows and cache per-conversation derived activity, ordering and pending approvals
- Activity page loads on entry, refreshes every 15 s while visible, and has a single header refresh button
- Include pending settings, model download source, device sharing and knowledge gateway changes in the working tree
- Add Electron streaming input benchmark and regression tests
2026-10-01 21:49:09 +08:00

192 lines
9.4 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.UnsegmentedMarkdownRenderer>{''}</original.UnsegmentedMarkdownRenderer>
</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.UnsegmentedMarkdownRenderer>{prefix}</original.UnsegmentedMarkdownRenderer>
</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()
}
// Streaming shape: small deltas appended to an already long answer, on
// persistent roots, as ChatTimeline receives them. Alternate variant order.
const deltaSamples = { original: [] as number[], current: [] as number[] }
const deltaHost = document.createElement('div')
deltaHost.className = host.className
deltaHost.style.cssText = host.style.cssText
document.body.append(deltaHost)
const deltaRoot = createRoot(deltaHost)
const deltaStart = Math.floor(longAnswerCorpus.length * 0.8)
const deltaSize = 120
try {
const renderDelta = (variant: 'original' | 'current', prefix: string) => {
if (variant === 'original') {
flushSync(() => deltaRoot.render(<UiLocaleProvider initialPreference="en-US">
<original.UnsegmentedMarkdownRenderer>{prefix}</original.UnsegmentedMarkdownRenderer>
</UiLocaleProvider>))
void deltaHost.offsetHeight
} else {
render(current.MarkdownRenderer, prefix)
void host.offsetHeight
}
}
renderDelta('original', longAnswerCorpus.slice(0, deltaStart))
renderDelta('current', longAnswerCorpus.slice(0, deltaStart))
for (let step = 1; step <= 60; step++) {
const prefix = longAnswerCorpus.slice(0, deltaStart + step * deltaSize)
const order = step % 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()
renderDelta(variant, prefix)
if (step > 5) deltaSamples[variant].push(performance.now() - start)
}
if (step % 10 === 0 && snapshot() !== snapshot(deltaHost)) throw new Error(`Delta DOM mismatch: ${step}`)
}
} finally {
flushSync(() => deltaRoot.unmount())
deltaHost.remove()
}
const streamingDelta = { startCharacters: deltaStart, deltaCharacters: deltaSize,
samplesPerVariant: deltaSamples.current.length,
wholeDocumentMs: stats(deltaSamples.original), segmentedMs: stats(deltaSamples.current) }
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.UnsegmentedMarkdownRenderer, 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, streamingDelta, prefixesChecked, domChecks }
}
Object.assign(window, { streamingMarkdown: run })