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:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,101 @@
# Performance Testing Tools
This directory contains tools and documentation for analyzing the performance of KaTeX Worker-based rendering.
## Files
### Benchmark Tests
- **`katex-worker-vs-direct.test.ts`** - Comprehensive benchmark comparing Worker vs direct rendering
- **`katex-performance-demo.html`** - Interactive visual demo for performance testing
### Documentation
- **`katex-worker-performance-analysis.md`** - Complete guide on measuring performance
- **`docs/zh/katex-cache-analysis.md`** - Quick summary (Chinese)
## Quick Start
### Run Benchmarks
```bash
# Run all performance tests
pnpm test test/benchmark/
# Run specific test
pnpm test test/benchmark/katex-worker-vs-direct.test.ts
# With detailed output
pnpm test test/benchmark/katex-worker-vs-direct.test.ts -- --reporter=verbose
```
### Visual Demo
Open `katex-performance-demo.html` in a browser to see an interactive performance comparison.
### Runtime Monitoring
Add to your application:
```typescript
import { enablePerfMonitoring, getPerfReport } from 'markstream-vue/utils/performance-monitor'
enablePerfMonitoring()
// After some usage...
setTimeout(() => getPerfReport(), 30000)
```
Or use browser console:
```javascript
window.__katexPerfReport()
```
## Key Insights
### Cache is Critical
- Cache hit: **~0.01ms**
- Worker render: **5-50ms**
- **Speedup: 500-5000x** 🚀
### Real-World Performance
- Typical cache hit rate: **70-85%**
- Performance improvement: **3-10x**
- Memory overhead: **~36KB for 200 formulas**
### When Worker + Cache Wins
✅ Complex formulas (>10ms render time)
✅ Multiple formulas per page (>5)
✅ Repeated formulas (cache hit rate >30%)
✅ User interaction matters (scrolling, animations)
## Decision Matrix
| Scenario | Recommendation |
|----------|----------------|
| Complex formulas | ✅ Use Worker + Cache |
| Many formulas per page | ✅ Use Worker + Cache |
| High repetition | ✅✅ Cache is highly effective |
| Simple formulas only | ⚠️ Worker overhead acceptable |
| SSR/Node.js | ⚠️ Fallback to direct render |
## Current Architecture (Best Practice)
```typescript
// 1. Check cache first (<0.01ms)
const cached = cache.get(key)
if (cached)
return cached
// 2. Use Worker (non-blocking)
return renderKaTeXInWorker(content)
.then((html) => {
cache.set(key, html) // Populate cache
return html
})
.catch(() => {
// 3. Fallback to direct render
return katex.renderToString(content)
})
```
**This hybrid approach is optimal!** ✅
@@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KaTeX Worker Performance Test</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 { color: #333; }
.controls {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
font-size: 14px;
}
button:hover { background: #45a049; }
button.secondary {
background: #2196F3;
}
button.secondary:hover { background: #0b7dda; }
.results {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 10px;
}
.metric {
display: flex;
justify-content: space-between;
margin: 10px 0;
padding: 8px;
background: #f9f9f9;
border-radius: 4px;
}
.metric-label {
font-weight: 500;
color: #666;
}
.metric-value {
font-weight: 700;
color: #333;
}
.success { color: #4CAF50; }
.warning { color: #ff9800; }
.error { color: #f44336; }
.log {
background: white;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.log-entry {
margin: 5px 0;
padding: 5px;
border-left: 3px solid #ccc;
padding-left: 10px;
}
.log-entry.worker { border-left-color: #2196F3; }
.log-entry.direct { border-left-color: #ff9800; }
.log-entry.cache { border-left-color: #4CAF50; }
.progress {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #45a049);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
}
</style>
</head>
<body>
<h1>🧪 KaTeX Worker Performance Test</h1>
<div class="controls">
<h3>Test Controls</h3>
<button onclick="runBenchmark('simple')">Test Simple Formulas</button>
<button onclick="runBenchmark('complex')">Test Complex Formulas</button>
<button onclick="runBenchmark('mixed')">Test Mixed (Realistic)</button>
<button class="secondary" onclick="clearResults()">Clear Results</button>
<button class="secondary" onclick="exportResults()">Export Data</button>
</div>
<div class="results">
<div class="card">
<h3>⚡ Performance Metrics</h3>
<div class="metric">
<span class="metric-label">Total Renders:</span>
<span class="metric-value" id="total-renders">0</span>
</div>
<div class="metric">
<span class="metric-label">Cache Hits:</span>
<span class="metric-value success" id="cache-hits">0</span>
</div>
<div class="metric">
<span class="metric-label">Cache Hit Rate:</span>
<span class="metric-value" id="cache-rate">0%</span>
</div>
<div class="metric">
<span class="metric-label">Avg Worker Time:</span>
<span class="metric-value" id="worker-time">0ms</span>
</div>
<div class="metric">
<span class="metric-label">Avg Cache Time:</span>
<span class="metric-value success" id="cache-time">0ms</span>
</div>
</div>
<div class="card">
<h3>📊 Comparison</h3>
<div class="metric">
<span class="metric-label">Worker Calls:</span>
<span class="metric-value" id="worker-calls">0</span>
</div>
<div class="metric">
<span class="metric-label">Time Saved:</span>
<span class="metric-value success" id="time-saved">0ms</span>
</div>
<div class="metric">
<span class="metric-label">Speedup:</span>
<span class="metric-value success" id="speedup">1.0x</span>
</div>
<div style="margin-top: 20px;">
<div><strong>Cache Efficiency:</strong></div>
<div class="progress">
<div class="progress-bar" id="cache-progress" style="width: 0%">0%</div>
</div>
</div>
</div>
<div class="card">
<h3>💡 Recommendation</h3>
<div id="recommendation" style="padding: 15px; background: #f0f0f0; border-radius: 4px; margin-top: 10px;">
Run a test to get recommendations...
</div>
</div>
</div>
<div class="log">
<h3>📝 Test Log</h3>
<div id="log-content"></div>
</div>
<script>
// Mock data structures for testing
const mockCache = new Map()
const mockMetrics = []
const testFormulas = {
simple: [
'x = y',
'a + b',
'x',
'y',
'z',
'x = y', // repeat
'a + b', // repeat
],
complex: [
'\\\\sum_{i=1}^{n} i = \\\\frac{n(n+1)}{2}',
'\\\\int_{-\\\\infty}^{\\\\infty} e^{-x^2} dx = \\\\sqrt{\\\\pi}',
'\\\\begin{pmatrix} a & b \\\\\\\\ c & d \\\\end{pmatrix}',
'\\\\frac{d}{dx}\\\\left(\\\\int_{a}^{x} f(t)\\\\,dt\\\\right) = f(x)',
],
mixed: [
'x', 'x', 'x', 'x', 'x', // repeat 5x
'y', 'y', 'y', // repeat 3x
'\\\\sum_{i=1}^{n}', '\\\\sum_{i=1}^{n}', // repeat 2x
'\\\\int f(x) dx',
'\\\\frac{a}{b}',
'E = mc^2', 'E = mc^2', 'E = mc^2', // repeat 3x
]
}
function log(message, type = 'info') {
const logDiv = document.getElementById('log-content')
const entry = document.createElement('div')
entry.className = `log-entry ${type}`
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`
logDiv.appendChild(entry)
logDiv.scrollTop = logDiv.scrollHeight
}
function simulateRender(formula, useCache = true) {
const cacheKey = formula
// Check cache
if (useCache && mockCache.has(cacheKey)) {
const startTime = performance.now()
mockCache.get(cacheKey) // simulate retrieval
const duration = performance.now() - startTime
mockMetrics.push({
type: 'cache-hit',
duration,
formulaLength: formula.length,
timestamp: Date.now(),
success: true
})
log(`Cache hit for "${formula.substring(0, 20)}..." (${duration.toFixed(3)}ms)`, 'cache')
return duration
}
// Simulate Worker render
const complexity = formula.length + (formula.match(/\\\\/g) || []).length * 10
const baseTime = Math.random() * 2 + complexity * 0.1
const workerOverhead = 1.5
const duration = baseTime + workerOverhead
// Add to cache
if (useCache) {
mockCache.set(cacheKey, `<rendered>${formula}</rendered>`)
}
mockMetrics.push({
type: 'worker',
duration,
formulaLength: formula.length,
timestamp: Date.now(),
success: true
})
log(`Worker render "${formula.substring(0, 20)}..." (${duration.toFixed(2)}ms)`, 'worker')
return duration
}
function calculateStats() {
if (mockMetrics.length === 0) return null
const workerMetrics = mockMetrics.filter(m => m.type === 'worker')
const cacheMetrics = mockMetrics.filter(m => m.type === 'cache-hit')
const totalRenders = mockMetrics.length
const cacheHits = cacheMetrics.length
const cacheRate = (cacheHits / totalRenders * 100).toFixed(1)
const avgWorkerTime = workerMetrics.length > 0
? (workerMetrics.reduce((sum, m) => sum + m.duration, 0) / workerMetrics.length).toFixed(2)
: 0
const avgCacheTime = cacheMetrics.length > 0
? (cacheMetrics.reduce((sum, m) => sum + m.duration, 0) / cacheMetrics.length).toFixed(3)
: 0
const totalTime = mockMetrics.reduce((sum, m) => sum + m.duration, 0)
const estimatedNoCache = totalRenders * avgWorkerTime
const timeSaved = (estimatedNoCache - totalTime).toFixed(2)
const speedup = estimatedNoCache > 0 ? (estimatedNoCache / totalTime).toFixed(1) : 1.0
return {
totalRenders,
cacheHits,
cacheRate,
workerCalls: workerMetrics.length,
avgWorkerTime,
avgCacheTime,
timeSaved,
speedup
}
}
function updateUI() {
const stats = calculateStats()
if (!stats) return
document.getElementById('total-renders').textContent = stats.totalRenders
document.getElementById('cache-hits').textContent = stats.cacheHits
document.getElementById('cache-rate').textContent = `${stats.cacheRate}%`
document.getElementById('worker-calls').textContent = stats.workerCalls
document.getElementById('worker-time').textContent = `${stats.avgWorkerTime}ms`
document.getElementById('cache-time').textContent = `${stats.avgCacheTime}ms`
document.getElementById('time-saved').textContent = `${stats.timeSaved}ms`
document.getElementById('speedup').textContent = `${stats.speedup}x`
// Update progress bar
const progressBar = document.getElementById('cache-progress')
progressBar.style.width = `${stats.cacheRate}%`
progressBar.textContent = `${stats.cacheRate}%`
// Update recommendation
let recommendation = ''
const rate = parseFloat(stats.cacheRate)
const workerTime = parseFloat(stats.avgWorkerTime)
if (rate > 70 && workerTime < 50) {
recommendation = '✅ <strong>Excellent!</strong> Worker + Cache is highly effective. Cache hit rate is great and Worker overhead is acceptable.'
} else if (rate > 50) {
recommendation = '✅ <strong>Good!</strong> Worker + Cache provides solid benefits. Consider keeping this architecture.'
} else if (rate < 30 && workerTime < 5) {
recommendation = '⚠️ <strong>Low cache efficiency.</strong> Formulas are very simple or rarely repeated. Direct rendering might be simpler.'
} else {
recommendation = '✅ <strong>Beneficial.</strong> Worker prevents main thread blocking even with lower cache hit rate.'
}
document.getElementById('recommendation').innerHTML = recommendation
}
async function runBenchmark(type) {
log(`Starting ${type} benchmark...`, 'info')
const formulas = testFormulas[type]
for (let i = 0; i < formulas.length; i++) {
await new Promise(resolve => setTimeout(resolve, 10)) // Simulate async
simulateRender(formulas[i])
updateUI()
}
log(`Benchmark complete! Rendered ${formulas.length} formulas.`, 'info')
updateUI()
}
function clearResults() {
mockCache.clear()
mockMetrics.length = 0
document.getElementById('log-content').innerHTML = ''
updateUI()
log('Results cleared', 'info')
}
function exportResults() {
const stats = calculateStats()
const data = {
stats,
metrics: mockMetrics,
timestamp: new Date().toISOString()
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `katex-benchmark-${Date.now()}.json`
a.click()
log('Results exported', 'info')
}
// Initialize
log('Performance test ready. Click a test button to begin.', 'info')
</script>
</body>
</html>
@@ -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)
})
})
@@ -0,0 +1,131 @@
/**
* Example: How to monitor KaTeX Worker performance in your application
*
* NOTE: This is a reference/example file, not meant to be executed directly.
* Copy relevant snippets into your application code.
*/
import { createApp } from 'vue'
import { enablePerfMonitoring, getPerfReport, perfMonitor } from '../../src/utils/performance-monitor'
// Example 1: Enable monitoring on app start
// Placeholder root component for example purposes
const YourApp = {} as any
const app = createApp(YourApp)
// Enable performance monitoring in development
const __isDev = typeof import.meta !== 'undefined' && (import.meta as any)?.env?.DEV
if (__isDev) {
enablePerfMonitoring()
// Get report after 30 seconds
setTimeout(() => {
const report = getPerfReport()
console.table(report)
}, 30000)
}
app.mount('#app')
// Example 2: Conditional monitoring based on query param
const url = new URLSearchParams(window.location.search)
if (url.get('perf') === '1') {
enablePerfMonitoring()
// Make report available in console
; (window as any).getReport = getPerfReport
console.log('📊 Performance monitoring enabled. Type getReport() to see stats.')
}
// Example 4: Export data for analysis
function exportPerformanceData() {
const data = perfMonitor.exportMetrics()
// Send to analytics service
fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
// Or download locally
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `katex-perf-${Date.now()}.json`
a.click()
}
// Example 5: Automated performance regression testing
async function runPerformanceTest() {
perfMonitor.enable()
perfMonitor.reset()
// Render test formulas
const stats = perfMonitor.getStats()
// Assertions
const cacheHitRate = Number.parseFloat(String(stats.cacheHitRate))
const avgWorkerTime = Number.parseFloat(String(stats.averageWorkerTime))
if (cacheHitRate < 60) {
console.warn('⚠️ Cache hit rate below 60%:', cacheHitRate)
}
if (avgWorkerTime > 100) {
console.warn('⚠️ Average worker time too high:', avgWorkerTime)
}
console.log('✅ Performance test passed:', stats.recommendation)
perfMonitor.disable()
return stats
}
// Example 6: Real-time monitoring dashboard (in DevTools)
function setupRealtimeMonitoring() {
enablePerfMonitoring()
// Update stats every 5 seconds
setInterval(() => {
const stats = perfMonitor.getStats()
console.clear()
console.log('📊 KaTeX Performance (last 5s)')
console.table({
'Total Renders': stats.totalRenders,
'Cache Hit Rate': `${stats.cacheHitRate}%`,
'Avg Worker Time': `${stats.averageWorkerTime}ms`,
'Avg Cache Hit': `${stats.averageCacheHitTime}ms`,
'Time Saved': `${stats.workerSavings}ms`,
})
console.log(stats.recommendation)
}, 5000)
}
// Browser console utilities
if (typeof window !== 'undefined') {
; (window as any).__katexPerf = {
enable: () => enablePerfMonitoring(),
getReport: () => getPerfReport(),
export: () => exportPerformanceData(),
test: () => runPerformanceTest(),
monitor: () => setupRealtimeMonitoring(),
}
console.log(`
📊 KaTeX Performance Monitoring Available!
Commands:
__katexPerf.enable() - Enable monitoring
__katexPerf.getReport() - Get current report
__katexPerf.export() - Export data as JSON
__katexPerf.test() - Run performance test
__katexPerf.monitor() - Real-time monitoring
Or use:
window.__katexPerfReport()
`)
}