Files
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

314 lines
14 KiB
TypeScript

/**
* Benchmark test: Worker vs Direct KaTeX rendering
*
* This test helps determine if Worker-based rendering has advantages over
* direct main-thread rendering in terms of:
* 1. CPU utilization (non-blocking)
* 2. Memory usage
* 3. Overall throughput
* 4. Responsiveness (time-to-interactive)
*/
import { describe, expect, it } from 'vitest'
// Test formulas of varying complexity
const TEST_FORMULAS = {
simple: 'x = y',
medium: '\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}',
complex: '\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}',
veryComplex: `
\\begin{pmatrix}
a_{11} & a_{12} & \\cdots & a_{1n} \\\\
a_{21} & a_{22} & \\cdots & a_{2n} \\\\
\\vdots & \\vdots & \\ddots & \\vdots \\\\
a_{m1} & a_{m2} & \\cdots & a_{mn}
\\end{pmatrix}
`,
}
describe('kaTeX Worker vs Direct Rendering Benchmark', () => {
it('should compare single render performance', async () => {
const katex = await import('katex')
const formula = TEST_FORMULAS.medium
const iterations = 100
// Direct rendering benchmark
const directStart = performance.now()
for (let i = 0; i < iterations; i++) {
katex.renderToString(formula, { throwOnError: false, displayMode: true })
}
const directEnd = performance.now()
const directTime = directEnd - directStart
console.log(`\n📊 Single-threaded Direct Rendering:`)
console.log(` Total: ${directTime.toFixed(2)}ms`)
console.log(` Average per render: ${(directTime / iterations).toFixed(2)}ms`)
console.log(` Throughput: ${(iterations / (directTime / 1000)).toFixed(0)} renders/sec`)
// Note: Worker overhead includes serialization + thread communication
// Expected: Direct is faster for small batches, Worker wins for large batches
expect(directTime).toBeGreaterThan(0)
})
it('should demonstrate main thread blocking issue', async () => {
const katex = await import('katex')
const formula = TEST_FORMULAS.veryComplex
const blockingIterations = 50
// Simulate main thread blocking
const mainThreadStart = performance.now()
let totalBlockingTime = 0
for (let i = 0; i < blockingIterations; i++) {
const renderStart = performance.now()
katex.renderToString(formula, { throwOnError: false, displayMode: true })
const renderEnd = performance.now()
totalBlockingTime += renderEnd - renderStart
}
const mainThreadEnd = performance.now()
const totalTime = mainThreadEnd - mainThreadStart
console.log(`\n⚠️ Main Thread Blocking Analysis:`)
console.log(` Total execution time: ${totalTime.toFixed(2)}ms`)
console.log(` Total blocking time: ${totalBlockingTime.toFixed(2)}ms`)
console.log(` Average blocking per render: ${(totalBlockingTime / blockingIterations).toFixed(2)}ms`)
console.log(` UI unresponsive for: ${totalBlockingTime.toFixed(0)}ms total`)
// Any blocking > 50ms is noticeable to users (60fps = 16.67ms per frame)
const averageBlockingTime = totalBlockingTime / blockingIterations
if (averageBlockingTime > 16.67) {
console.log(` ❌ Average render blocks UI frames (>${16.67}ms)`)
console.log(` ✅ Worker would prevent frame drops`)
}
expect(totalBlockingTime).toBeGreaterThan(0)
})
it('should test Worker overhead vs benefit threshold', async () => {
const katex = await import('katex')
// Measure serialization + communication overhead
const testData = TEST_FORMULAS.medium
const serializedSize = new Blob([testData]).size
const estimatedWorkerOverhead = 1 // ~1-2ms for postMessage roundtrip
// Measure actual render time
const renderStart = performance.now()
katex.renderToString(testData, { throwOnError: false, displayMode: true })
const renderEnd = performance.now()
const renderTime = renderEnd - renderStart
console.log(`\n🎯 Worker Viability Analysis:`)
console.log(` Formula size: ${serializedSize} bytes`)
console.log(` Estimated Worker overhead: ~${estimatedWorkerOverhead}ms`)
console.log(` Actual render time: ${renderTime.toFixed(2)}ms`)
// Rule of thumb: Worker is beneficial when:
// 1. Render time > 10ms (noticeable blocking)
// 2. Render time > 3x Worker overhead
// 3. Many concurrent renders expected
const workerBeneficial = renderTime > 10 && renderTime > estimatedWorkerOverhead * 3
if (workerBeneficial) {
console.log(` ✅ Worker IS beneficial (render time ${renderTime.toFixed(1)}ms > ${estimatedWorkerOverhead * 3}ms threshold)`)
console.log(` Reason: Prevents ${renderTime.toFixed(1)}ms main thread blocking`)
}
else {
console.log(` ⚠️ Worker overhead may not be worth it for this formula`)
console.log(` Consider: Cache hits, batch processing, or complexity threshold`)
}
expect(renderTime).toBeGreaterThan(0)
})
it('should test cache effectiveness', async () => {
const katex = await import('katex')
const formula = TEST_FORMULAS.complex
const cacheSimulation = new Map<string, string>()
// First render (cache miss)
const missStart = performance.now()
const result = katex.renderToString(formula, { throwOnError: false, displayMode: true })
const missEnd = performance.now()
const missTime = missEnd - missStart
// Populate cache
cacheSimulation.set(formula, result)
// Cache hit (no Worker call needed)
const hitStart = performance.now()
const _cached = cacheSimulation.get(formula)
const hitEnd = performance.now()
const hitTime = hitEnd - hitStart
console.log(`\n💾 Cache Effectiveness:`)
console.log(` Cache miss: ${missTime.toFixed(3)}ms (Worker call + render)`)
console.log(` Cache hit: ${hitTime.toFixed(3)}ms (instant return)`)
console.log(` Speedup: ${(missTime / hitTime).toFixed(0)}x faster`)
console.log(` \n 💡 This is why Worker cache is CRITICAL!`)
// Cache should be orders of magnitude faster
expect(hitTime).toBeLessThan(missTime / 100)
})
it('should simulate realistic usage patterns', async () => {
const katex = await import('katex')
// Simulate a markdown document with repeated formulas.
// The base list is intentionally small enough to resemble a real message,
// but we repeat it during the benchmark so the timing signal stays larger
// than sub-millisecond JIT / scheduler noise on fast machines.
const baseDocument = [
TEST_FORMULAS.simple, // repeated 10x
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.simple,
TEST_FORMULAS.medium, // repeated 5x
TEST_FORMULAS.medium,
TEST_FORMULAS.medium,
TEST_FORMULAS.medium,
TEST_FORMULAS.medium,
TEST_FORMULAS.complex, // unique
TEST_FORMULAS.veryComplex, // unique
]
const document = Array.from({ length: 10 }, () => baseDocument).flat()
// Warm up KaTeX before measuring. Without this, the benchmark can become
// flaky because module/JIT startup dominates these very short code paths.
for (const formula of Object.values(TEST_FORMULAS))
katex.renderToString(formula, { throwOnError: false, displayMode: true })
// Without cache (naive approach)
const noCacheStart = performance.now()
for (const formula of document) {
katex.renderToString(formula, { throwOnError: false, displayMode: true })
}
const noCacheEnd = performance.now()
const noCacheTime = noCacheEnd - noCacheStart
// With cache (Worker client behavior)
const cache = new Map<string, string>()
const withCacheStart = performance.now()
let cacheMisses = 0
let cacheHits = 0
for (const formula of document) {
if (cache.has(formula)) {
cacheHits++
cache.get(formula) // instant
}
else {
cacheMisses++
const result = katex.renderToString(formula, { throwOnError: false, displayMode: true })
cache.set(formula, result)
}
}
const withCacheEnd = performance.now()
const withCacheTime = withCacheEnd - withCacheStart
console.log(`\n📄 Realistic Document Rendering (${document.length} formulas):`)
console.log(` Without cache: ${noCacheTime.toFixed(2)}ms`)
console.log(` With cache: ${withCacheTime.toFixed(2)}ms`)
console.log(` Speedup: ${(noCacheTime / withCacheTime).toFixed(1)}x`)
console.log(` Cache stats:`)
console.log(` - Hits: ${cacheHits} (${(cacheHits / document.length * 100).toFixed(1)}%)`)
console.log(` - Misses: ${cacheMisses} (${(cacheMisses / document.length * 100).toFixed(1)}%)`)
console.log(` \n 💡 Real documents have ${(cacheHits / document.length * 100).toFixed(0)}% cache hit rate!`)
expect(withCacheTime).toBeLessThan(noCacheTime)
expect(cacheHits).toBeGreaterThan(cacheMisses)
})
it('should measure memory footprint', async () => {
const katex = await import('katex')
const formulas = Object.values(TEST_FORMULAS)
const cache = new Map<string, string>()
let totalInputSize = 0
let totalOutputSize = 0
for (const formula of formulas) {
const inputSize = new Blob([formula]).size
const output = katex.renderToString(formula, { throwOnError: false, displayMode: true })
const outputSize = new Blob([output]).size
totalInputSize += inputSize
totalOutputSize += outputSize
cache.set(formula, output)
}
const expansionRatio = totalOutputSize / totalInputSize
const cacheOverhead = totalOutputSize - totalInputSize
const estimatedCacheSize200 = (totalOutputSize / formulas.length) * 200 // CACHE_MAX
console.log(`\n💾 Memory Footprint Analysis:`)
console.log(` Input (LaTeX): ${totalInputSize} bytes`)
console.log(` Output (HTML): ${totalOutputSize} bytes`)
console.log(` Expansion ratio: ${expansionRatio.toFixed(1)}x`)
console.log(` Cache overhead per entry: ${(cacheOverhead / formulas.length).toFixed(0)} bytes`)
console.log(` Estimated cache size (200 entries): ${(estimatedCacheSize200 / 1024).toFixed(1)} KB`)
console.log(` \n 💡 Memory cost is MINIMAL (~${(estimatedCacheSize200 / 1024).toFixed(0)}KB for 200 formulas)`)
// Allow a small overhead margin for HTML/string object size and environment variance
// Expect under ~1.2MB for 200 entries to keep memory impact low
expect(estimatedCacheSize200).toBeLessThanOrEqual(1.2 * 1024 * 1024)
})
})
describe('worker Decision Matrix', () => {
it('should provide recommendation based on usage patterns', () => {
console.log(`\n
╔═══════════════════════════════════════════════════════════════════════════╗
║ WORKER vs DIRECT RENDERING DECISION ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ ║
║ ✅ USE WORKER when: ║
║ • Complex formulas (render time > 10ms) ║
║ • Multiple formulas per page (>5) ║
║ • User interaction matters (prevent UI blocking) ║
║ • Document may scroll/re-render frequently ║
║ • High cache hit rate expected (repeated formulas) ║
║ ║
║ ⚠️ CONSIDER DIRECT when: ║
║ • Only simple formulas (render time < 5ms) ║
║ • Single formula per page ║
║ • SSR/Node.js environment (no Workers) ║
║ • Bundle size is critical concern ║
║ ║
║ 🎯 HYBRID APPROACH (CURRENT): ║
║ ✅ Try Worker first (with cache) ║
║ ✅ Fallback to direct render if Worker fails ║
║ ✅ Cache in client to benefit both paths ║
║ ✅ Best of both worlds! ║
║ ║
║ 📊 PERFORMANCE CHARACTERISTICS: ║
║ • Worker overhead: ~1-2ms per call ║
║ • Cache hit time: <0.01ms ║
║ • Medium formula: ~5-15ms render ║
║ • Complex formula: ~20-50ms render ║
║ • Cache memory: ~10-50KB for 200 entries ║
║ ║
║ 💡 KEY INSIGHT: ║
║ Worker + Cache eliminates 99% of blocking time! ║
║ Without cache, Worker overhead outweighs benefit for simple formulas ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════╝
`)
expect(true).toBe(true)
})
})