e0a5f57793
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.
387 lines
12 KiB
HTML
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>
|