|
| 1 | +import type { BenchResult } from '@sim/stress-test'; |
| 2 | +import { |
| 3 | + benchMixedWorkload, |
| 4 | + benchMultiQueryPopulation, |
| 5 | + benchNotQueryChurn, |
| 6 | + benchQueryIteration, |
| 7 | + benchSpawnDestroy, |
| 8 | + benchTraitChurn, |
| 9 | + benchWideArchetype, |
| 10 | +} from '@sim/stress-test'; |
| 11 | + |
| 12 | +const ITERATIONS = 50; |
| 13 | + |
| 14 | +type Suite = { name: string; run: () => BenchResult }; |
| 15 | + |
| 16 | +const suites: Suite[] = [ |
| 17 | + { name: 'spawn & destroy', run: () => benchSpawnDestroy(ITERATIONS) }, |
| 18 | + { name: 'trait churn', run: () => benchTraitChurn(ITERATIONS) }, |
| 19 | + { name: 'query iteration', run: () => benchQueryIteration(ITERATIONS) }, |
| 20 | + { name: 'multi-query population', run: () => benchMultiQueryPopulation(ITERATIONS) }, |
| 21 | + { name: 'wide archetype', run: () => benchWideArchetype(ITERATIONS) }, |
| 22 | + { name: 'not-query churn', run: () => benchNotQueryChurn(ITERATIONS) }, |
| 23 | + { name: 'mixed workload', run: () => benchMixedWorkload(ITERATIONS) }, |
| 24 | +]; |
| 25 | + |
| 26 | +function fmtMs(v: number): string { |
| 27 | + if (v < 0.001) return `${(v * 1000).toFixed(2)} µs`; |
| 28 | + if (v < 1) return `${(v * 1000).toFixed(1)} µs`; |
| 29 | + return `${v.toFixed(3)} ms`; |
| 30 | +} |
| 31 | + |
| 32 | +function createUI(): { |
| 33 | + setStatus: (text: string) => void; |
| 34 | + addResult: (r: BenchResult) => void; |
| 35 | + setDone: () => void; |
| 36 | +} { |
| 37 | + const app = document.getElementById('app')!; |
| 38 | + app.innerHTML = ''; |
| 39 | + |
| 40 | + const style = document.createElement('style'); |
| 41 | + style.textContent = ` |
| 42 | + * { margin: 0; padding: 0; box-sizing: border-box; } |
| 43 | + body { background: #0d1117; color: #c9d1d9; font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 14px; padding: 24px; } |
| 44 | + h1 { color: #58a6ff; font-size: 20px; margin-bottom: 4px; } |
| 45 | + .subtitle { color: #8b949e; font-size: 13px; margin-bottom: 20px; } |
| 46 | + #status { color: #f0883e; margin-bottom: 16px; font-size: 13px; } |
| 47 | + table { border-collapse: collapse; width: 100%; max-width: 1100px; } |
| 48 | + th { text-align: left; padding: 8px 12px; border-bottom: 2px solid #30363d; color: #8b949e; font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; } |
| 49 | + th.num { text-align: right; } |
| 50 | + td { padding: 8px 12px; border-bottom: 1px solid #21262d; } |
| 51 | + td.num { text-align: right; font-variant-numeric: tabular-nums; } |
| 52 | + td.name { color: #58a6ff; font-weight: 500; } |
| 53 | + tr:hover td { background: #161b22; } |
| 54 | + #run-btn { margin-top: 16px; padding: 8px 20px; background: #238636; color: #fff; border: none; border-radius: 6px; font-family: inherit; font-size: 14px; cursor: pointer; } |
| 55 | + #run-btn:hover { background: #2ea043; } |
| 56 | + #run-btn:disabled { background: #21262d; color: #484f58; cursor: not-allowed; } |
| 57 | + `; |
| 58 | + document.head.appendChild(style); |
| 59 | + |
| 60 | + const h1 = document.createElement('h1'); |
| 61 | + h1.textContent = 'Koota ECS Stress Test'; |
| 62 | + app.appendChild(h1); |
| 63 | + |
| 64 | + const sub = document.createElement('div'); |
| 65 | + sub.className = 'subtitle'; |
| 66 | + sub.textContent = `${ITERATIONS} iterations per benchmark · ${suites.length} benchmarks`; |
| 67 | + app.appendChild(sub); |
| 68 | + |
| 69 | + const status = document.createElement('div'); |
| 70 | + status.id = 'status'; |
| 71 | + app.appendChild(status); |
| 72 | + |
| 73 | + const table = document.createElement('table'); |
| 74 | + const thead = document.createElement('thead'); |
| 75 | + const headerRow = document.createElement('tr'); |
| 76 | + const cols = ['Benchmark', 'Mean', 'Median', 'Min', 'Max', 'P99', 'P99.9', 'StdDev', 'ops/s']; |
| 77 | + for (const col of cols) { |
| 78 | + const th = document.createElement('th'); |
| 79 | + th.textContent = col; |
| 80 | + if (col !== 'Benchmark') th.className = 'num'; |
| 81 | + headerRow.appendChild(th); |
| 82 | + } |
| 83 | + thead.appendChild(headerRow); |
| 84 | + table.appendChild(thead); |
| 85 | + |
| 86 | + const tbody = document.createElement('tbody'); |
| 87 | + table.appendChild(tbody); |
| 88 | + app.appendChild(table); |
| 89 | + |
| 90 | + const btn = document.createElement('button'); |
| 91 | + btn.id = 'run-btn'; |
| 92 | + btn.textContent = 'Run Again'; |
| 93 | + btn.disabled = true; |
| 94 | + btn.onclick = () => { |
| 95 | + tbody.innerHTML = ''; |
| 96 | + runAll(); |
| 97 | + }; |
| 98 | + app.appendChild(btn); |
| 99 | + |
| 100 | + return { |
| 101 | + setStatus(text: string) { |
| 102 | + status.textContent = text; |
| 103 | + }, |
| 104 | + addResult(r: BenchResult) { |
| 105 | + const tr = document.createElement('tr'); |
| 106 | + const vals = [ |
| 107 | + { text: r.name, cls: 'name' }, |
| 108 | + { text: fmtMs(r.mean), cls: 'num' }, |
| 109 | + { text: fmtMs(r.median), cls: 'num' }, |
| 110 | + { text: fmtMs(r.min), cls: 'num' }, |
| 111 | + { text: fmtMs(r.max), cls: 'num' }, |
| 112 | + { text: fmtMs(r.p99), cls: 'num' }, |
| 113 | + { text: fmtMs(r.p999), cls: 'num' }, |
| 114 | + { text: fmtMs(r.stddev), cls: 'num' }, |
| 115 | + { text: (1000 / r.mean).toFixed(1), cls: 'num' }, |
| 116 | + ]; |
| 117 | + for (const v of vals) { |
| 118 | + const td = document.createElement('td'); |
| 119 | + td.textContent = v.text; |
| 120 | + td.className = v.cls; |
| 121 | + tr.appendChild(td); |
| 122 | + } |
| 123 | + tbody.appendChild(tr); |
| 124 | + }, |
| 125 | + setDone() { |
| 126 | + status.textContent = 'Done.'; |
| 127 | + btn.disabled = false; |
| 128 | + }, |
| 129 | + }; |
| 130 | +} |
| 131 | + |
| 132 | +const ui = createUI(); |
| 133 | + |
| 134 | +function runAll() { |
| 135 | + const btn = document.getElementById('run-btn') as HTMLButtonElement; |
| 136 | + btn.disabled = true; |
| 137 | + |
| 138 | + let idx = 0; |
| 139 | + |
| 140 | + function next() { |
| 141 | + if (idx >= suites.length) { |
| 142 | + ui.setDone(); |
| 143 | + return; |
| 144 | + } |
| 145 | + |
| 146 | + const suite = suites[idx]; |
| 147 | + ui.setStatus(`Running ${idx + 1}/${suites.length}: ${suite.name}...`); |
| 148 | + |
| 149 | + setTimeout(() => { |
| 150 | + const result = suite.run(); |
| 151 | + ui.addResult(result); |
| 152 | + idx++; |
| 153 | + next(); |
| 154 | + }, 50); |
| 155 | + } |
| 156 | + |
| 157 | + next(); |
| 158 | +} |
| 159 | + |
| 160 | +runAll(); |
0 commit comments