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.
672 lines
No EOL
23 KiB
HTML
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> |