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

387 lines
12 KiB
HTML

<!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>