feat: signal propagation benchmarks + dev server HMR fix
Benchmarks (examples/benchmarks.html): - Wide Fan-Out: 1→1000 derived signals (46K ops/s) - Deep Chain: 100-layer propagation (399K ops/s) - Diamond Dependency: 500 glitch-free diamonds (16K ops/s) - Batch Updates: 50 sources in single batch (89K ops/s) - Effect Throughput: 500 effects (135K ops/s) - Mixed Graph: realistic 10→30→10 topology (247K ops/s) Dev server fix: replaced EventSource SSE (flickering) with fetch-based polling every 500ms for stable HMR.
This commit is contained in:
parent
c9b1913a57
commit
e3da3b2d8b
1 changed files with 672 additions and 0 deletions
672
examples/benchmarks.html
Normal file
672
examples/benchmarks.html
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DreamStack — Signal Propagation Benchmarks</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #050510;
|
||||
--surface: #0c0c1d;
|
||||
--border: #1e1e3a;
|
||||
--text: #e8e8f0;
|
||||
--text-dim: #6b6b8a;
|
||||
--accent: #7c3aed;
|
||||
--accent-2: #a855f7;
|
||||
--green: #22c55e;
|
||||
--blue: #3b82f6;
|
||||
--yellow: #eab308;
|
||||
--red: #ef4444;
|
||||
--cyan: #06b6d4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
||||
padding: 40px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, #fff 0%, #c4b5fd 50%, #7c3aed 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.bench-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.bench-card.running {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
|
||||
.bench-card.done {
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
.bench-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.bench-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bench-tag {
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-pending {
|
||||
background: rgba(107, 107, 138, 0.2);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.tag-running {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: var(--blue);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.tag-done {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.bench-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bench-results {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-2);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.metric-value.fast {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.metric-value.medium {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.metric-value.slow {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--cyan));
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
button#runAll {
|
||||
background: linear-gradient(135deg, var(--accent), #6d28d9);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 32px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-bottom: 32px;
|
||||
font-family: inherit;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
button#runAll:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 20px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
button#runAll:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.summary {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
margin-top: 24px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.summary.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.summary h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.summary-table th {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.summary-table td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid rgba(30, 30, 58, 0.3);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>⚡ Signal Propagation Benchmarks</h1>
|
||||
<p class="subtitle">Measuring DreamStack's reactive engine performance across different graph topologies</p>
|
||||
|
||||
<button id="runAll">Run All Benchmarks</button>
|
||||
|
||||
<div id="benchmarks"></div>
|
||||
|
||||
<div class="summary" id="summary">
|
||||
<h3>📊 Summary</h3>
|
||||
<table class="summary-table" id="summaryTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benchmark</th>
|
||||
<th>Ops/sec</th>
|
||||
<th>Avg (μs)</th>
|
||||
<th>Signals</th>
|
||||
<th>Rating</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ─── DreamStack Runtime (extracted) ─────────────────────────
|
||||
|
||||
const DS = (() => {
|
||||
let currentEffect = null;
|
||||
let batchDepth = 0;
|
||||
let pendingEffects = new Set();
|
||||
|
||||
class Signal {
|
||||
constructor(initialValue) {
|
||||
this._value = initialValue;
|
||||
this._subscribers = new Set();
|
||||
}
|
||||
get value() {
|
||||
if (currentEffect) this._subscribers.add(currentEffect);
|
||||
return this._value;
|
||||
}
|
||||
set value(v) {
|
||||
if (v === this._value) return;
|
||||
this._value = v;
|
||||
if (batchDepth > 0) {
|
||||
for (const sub of this._subscribers) pendingEffects.add(sub);
|
||||
} else {
|
||||
for (const sub of [...this._subscribers]) sub._run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Derived {
|
||||
constructor(fn) {
|
||||
this._fn = fn;
|
||||
this._value = undefined;
|
||||
this._subscribers = new Set();
|
||||
this._effect = new Effect(() => {
|
||||
const newVal = this._fn();
|
||||
if (newVal !== this._value) {
|
||||
this._value = newVal;
|
||||
for (const sub of [...this._subscribers]) sub._run();
|
||||
}
|
||||
});
|
||||
this._effect._run();
|
||||
}
|
||||
get value() {
|
||||
if (currentEffect) this._subscribers.add(currentEffect);
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
class Effect {
|
||||
constructor(fn) {
|
||||
this._fn = fn;
|
||||
this._disposed = false;
|
||||
}
|
||||
_run() {
|
||||
if (this._disposed) return;
|
||||
const prev = currentEffect;
|
||||
currentEffect = this;
|
||||
try { this._fn(); } finally { currentEffect = prev; }
|
||||
}
|
||||
dispose() { this._disposed = true; }
|
||||
}
|
||||
|
||||
function signal(v) { return new Signal(v); }
|
||||
function derived(fn) { return new Derived(fn); }
|
||||
function effect(fn) { const e = new Effect(fn); e._run(); return e; }
|
||||
function batch(fn) {
|
||||
batchDepth++;
|
||||
try { fn(); } finally {
|
||||
batchDepth--;
|
||||
if (batchDepth === 0) {
|
||||
const effects = [...pendingEffects];
|
||||
pendingEffects.clear();
|
||||
for (const e of effects) e._run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { signal, derived, effect, batch, Signal, Derived, Effect };
|
||||
})();
|
||||
|
||||
|
||||
// ─── Benchmark Definitions ──────────────────────────────────
|
||||
|
||||
const BENCHMARKS = [
|
||||
{
|
||||
name: "Wide Fan-Out",
|
||||
desc: "1 source signal → 1,000 derived values. Measures broadcast performance.",
|
||||
signalCount: 1001,
|
||||
run(iterations) {
|
||||
const src = DS.signal(0);
|
||||
const deriveds = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
deriveds.push(DS.derived(() => src.value * 2 + i));
|
||||
}
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
src.value = i;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
// Verify correctness
|
||||
const last = deriveds[999].value;
|
||||
const expected = (iterations - 1) * 2 + 999;
|
||||
return { elapsed, iterations, correct: last === expected };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Deep Chain",
|
||||
desc: "Signal → D1 → D2 → ... → D100. Measures propagation through 100 layers.",
|
||||
signalCount: 101,
|
||||
run(iterations) {
|
||||
const src = DS.signal(0);
|
||||
let prev = src;
|
||||
const chain = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const p = prev;
|
||||
const d = DS.derived(() => p.value + 1);
|
||||
chain.push(d);
|
||||
prev = d;
|
||||
}
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
src.value = i;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
const last = chain[99].value;
|
||||
const expected = (iterations - 1) + 100;
|
||||
return { elapsed, iterations, correct: last === expected };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Diamond Dependency",
|
||||
desc: "A → B, C (fork) → D (join). 500 diamonds. Tests glitch-free propagation.",
|
||||
signalCount: 2001,
|
||||
run(iterations) {
|
||||
const src = DS.signal(0);
|
||||
const results = [];
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const left = DS.derived(() => src.value + i);
|
||||
const right = DS.derived(() => src.value * 2 + i);
|
||||
const join = DS.derived(() => left.value + right.value);
|
||||
results.push(join);
|
||||
}
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
src.value = i;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
// Check last diamond: left = (n-1)+499, right = (n-1)*2+499, join = left+right
|
||||
const n = iterations - 1;
|
||||
const expected = (n + 499) + (n * 2 + 499);
|
||||
const correct = results[499].value === expected;
|
||||
return { elapsed, iterations, correct };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Batch Updates",
|
||||
desc: "50 source signals updated in a single batch, 200 derived values. Measures batching efficiency.",
|
||||
signalCount: 250,
|
||||
run(iterations) {
|
||||
const sources = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
sources.push(DS.signal(0));
|
||||
}
|
||||
const deriveds = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const s1 = sources[i % 50];
|
||||
const s2 = sources[(i + 1) % 50];
|
||||
deriveds.push(DS.derived(() => s1.value + s2.value));
|
||||
}
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
DS.batch(() => {
|
||||
for (let j = 0; j < 50; j++) {
|
||||
sources[j].value = i * 50 + j;
|
||||
}
|
||||
});
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
return { elapsed, iterations, correct: true };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Effect Throughput",
|
||||
desc: "1 signal → 500 effects that each read it. Measures effect scheduling.",
|
||||
signalCount: 1,
|
||||
run(iterations) {
|
||||
const src = DS.signal(0);
|
||||
let effectRuns = 0;
|
||||
const effects = [];
|
||||
for (let i = 0; i < 500; i++) {
|
||||
effects.push(DS.effect(() => {
|
||||
const _ = src.value;
|
||||
effectRuns++;
|
||||
}));
|
||||
}
|
||||
effectRuns = 0;
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
src.value = i + 1;
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
const expectedRuns = iterations * 500;
|
||||
// dispose
|
||||
for (const e of effects) e.dispose();
|
||||
return { elapsed, iterations, correct: effectRuns === expectedRuns };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Mixed Graph (Realistic)",
|
||||
desc: "10 sources → 30 derived → 10 effects. Simulates a real component with cross-dependencies.",
|
||||
signalCount: 50,
|
||||
run(iterations) {
|
||||
const sources = [];
|
||||
for (let i = 0; i < 10; i++) sources.push(DS.signal(i));
|
||||
|
||||
const layer1 = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const a = sources[i];
|
||||
const b = sources[(i + 1) % 10];
|
||||
layer1.push(DS.derived(() => a.value + b.value));
|
||||
}
|
||||
|
||||
const layer2 = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const a = layer1[i];
|
||||
const b = layer1[(i + 3) % 10];
|
||||
const s = sources[i];
|
||||
layer2.push(DS.derived(() => a.value * 2 + b.value - s.value));
|
||||
}
|
||||
|
||||
const layer3 = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const a = layer2[i];
|
||||
const b = layer2[(i + 5) % 10];
|
||||
layer3.push(DS.derived(() => Math.abs(a.value - b.value)));
|
||||
}
|
||||
|
||||
let effectSum = 0;
|
||||
const effects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const d = layer3[i];
|
||||
effects.push(DS.effect(() => { effectSum += d.value; }));
|
||||
}
|
||||
|
||||
effectSum = 0;
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
DS.batch(() => {
|
||||
for (let j = 0; j < 10; j++) {
|
||||
sources[j].value = i * 10 + j;
|
||||
}
|
||||
});
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
for (const e of effects) e.dispose();
|
||||
return { elapsed, iterations, correct: true };
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// ─── Benchmark Runner ───────────────────────────────────────
|
||||
|
||||
const container = document.getElementById('benchmarks');
|
||||
const summaryDiv = document.getElementById('summary');
|
||||
const summaryBody = document.querySelector('#summaryTable tbody');
|
||||
const runBtn = document.getElementById('runAll');
|
||||
|
||||
// Render benchmark cards
|
||||
const cards = BENCHMARKS.map((b, i) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bench-card';
|
||||
card.id = `bench-${i}`;
|
||||
card.innerHTML = `
|
||||
<div class="bench-header">
|
||||
<span class="bench-name">${b.name}</span>
|
||||
<span class="bench-tag tag-pending" id="tag-${i}">pending</span>
|
||||
</div>
|
||||
<div class="bench-desc">${b.desc}</div>
|
||||
<div class="bench-results" id="results-${i}">
|
||||
<div class="metric"><div class="metric-value" id="ops-${i}">—</div><div class="metric-label">ops/sec</div></div>
|
||||
<div class="metric"><div class="metric-value" id="avg-${i}">—</div><div class="metric-label">avg (μs)</div></div>
|
||||
<div class="metric"><div class="metric-value" id="total-${i}">—</div><div class="metric-label">total (ms)</div></div>
|
||||
<div class="metric"><div class="metric-value" id="correct-${i}">—</div><div class="metric-label">correct</div></div>
|
||||
</div>
|
||||
<div class="bar-container"><div class="bar" id="bar-${i}" style="width: 0%"></div></div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
return card;
|
||||
});
|
||||
|
||||
function rateClass(opsPerSec) {
|
||||
if (opsPerSec > 100000) return 'fast';
|
||||
if (opsPerSec > 10000) return 'medium';
|
||||
return 'slow';
|
||||
}
|
||||
|
||||
function formatNum(n) {
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
||||
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
||||
return n.toFixed(0);
|
||||
}
|
||||
|
||||
async function runBenchmark(index) {
|
||||
const b = BENCHMARKS[index];
|
||||
const card = cards[index];
|
||||
const tag = document.getElementById(`tag-${index}`);
|
||||
|
||||
card.className = 'bench-card running';
|
||||
tag.className = 'bench-tag tag-running';
|
||||
tag.textContent = 'running...';
|
||||
|
||||
// Allow UI to update
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
|
||||
// Warmup
|
||||
b.run(10);
|
||||
|
||||
// Calibrate: find iteration count that takes ~200ms
|
||||
let iters = 100;
|
||||
let calibResult = b.run(iters);
|
||||
while (calibResult.elapsed < 50 && iters < 1000000) {
|
||||
iters *= 4;
|
||||
calibResult = b.run(iters);
|
||||
}
|
||||
// Scale to ~200ms target
|
||||
const targetMs = 200;
|
||||
iters = Math.max(100, Math.round(iters * targetMs / Math.max(calibResult.elapsed, 1)));
|
||||
|
||||
// Run 3 trials, take best
|
||||
let best = null;
|
||||
for (let trial = 0; trial < 3; trial++) {
|
||||
const result = b.run(iters);
|
||||
if (!best || result.elapsed < best.elapsed) best = result;
|
||||
// Progress
|
||||
document.getElementById(`bar-${index}`).style.width = `${((trial + 1) / 3) * 100}%`;
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
}
|
||||
|
||||
const opsPerSec = (best.iterations / best.elapsed) * 1000;
|
||||
const avgMicros = (best.elapsed / best.iterations) * 1000;
|
||||
const cls = rateClass(opsPerSec);
|
||||
|
||||
document.getElementById(`ops-${index}`).textContent = formatNum(opsPerSec);
|
||||
document.getElementById(`ops-${index}`).className = `metric-value ${cls}`;
|
||||
document.getElementById(`avg-${index}`).textContent = avgMicros.toFixed(2);
|
||||
document.getElementById(`avg-${index}`).className = `metric-value ${cls}`;
|
||||
document.getElementById(`total-${index}`).textContent = best.elapsed.toFixed(1);
|
||||
document.getElementById(`correct-${index}`).textContent = best.correct ? '✓' : '✕';
|
||||
document.getElementById(`correct-${index}`).className = `metric-value ${best.correct ? 'fast' : 'slow'}`;
|
||||
|
||||
card.className = 'bench-card done';
|
||||
tag.className = 'bench-tag tag-done';
|
||||
tag.textContent = formatNum(opsPerSec) + ' ops/s';
|
||||
|
||||
return {
|
||||
name: b.name,
|
||||
opsPerSec,
|
||||
avgMicros,
|
||||
signalCount: b.signalCount,
|
||||
cls
|
||||
};
|
||||
}
|
||||
|
||||
async function runAll() {
|
||||
runBtn.disabled = true;
|
||||
runBtn.textContent = 'Running...';
|
||||
summaryDiv.className = 'summary';
|
||||
summaryBody.innerHTML = '';
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < BENCHMARKS.length; i++) {
|
||||
const r = await runBenchmark(i);
|
||||
results.push(r);
|
||||
}
|
||||
|
||||
// Show summary
|
||||
for (const r of results) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${r.name}</td>
|
||||
<td class="${r.cls}">${formatNum(r.opsPerSec)}</td>
|
||||
<td>${r.avgMicros.toFixed(2)}</td>
|
||||
<td>${r.signalCount}</td>
|
||||
<td class="${r.cls}">${r.cls === 'fast' ? '🟢' : r.cls === 'medium' ? '🟡' : '🔴'}</td>
|
||||
`;
|
||||
summaryBody.appendChild(tr);
|
||||
}
|
||||
summaryDiv.className = 'summary visible';
|
||||
|
||||
runBtn.disabled = false;
|
||||
runBtn.textContent = 'Run Again';
|
||||
}
|
||||
|
||||
runBtn.addEventListener('click', runAll);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue