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,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>
|
||||
Reference in New Issue
Block a user