dreamstack/examples/benchmarks.html
enzotar e3da3b2d8b 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.
2026-02-25 01:06:07 -08:00

672 lines
No EOL
23 KiB
HTML

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