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.
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user