Files
wickra/examples/wasm/parallel_assets.html
T
kingchenc 6e3190a44a examples(wasm): add a browser parallel-assets demo via Web Workers
Close the final "parallel assets" cell of the cross-language matrix for
WASM.

* examples/wasm/parallel_assets.html — generates a synthetic
  `(assets, bars)` panel deterministically (the LCG matches the Node
  and Rust siblings so timings are directly comparable), runs the
  serial baseline on the main thread, then dispatches the same workload
  to a pool of module Workers and reports the speedup. The render is
  three cards (serial / parallel / speedup) plus a sanity-check line
  asserting per-asset agreement between the two paths.
* examples/wasm/parallel_worker.js — companion module worker that
  loads its own copy of the WebAssembly module via `init()` and
  processes whichever slice of the panel its parent dispatches.

Modern browsers ship module-worker support (`new Worker(url, {
type: "module" })`) which lets every worker do `import init, { SMA, RSI
} from "../../bindings/wasm/pkg/wickra_wasm.js"` without bundler
glue. The inline page module and the worker module both syntax-check
cleanly under `node --check`.
2026-05-23 00:45:01 +02:00

191 lines
7.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Wickra WASM — parallel assets</title>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; max-width: 980px; margin: 2rem auto; padding: 0 1rem; color: #1d1d1d; }
h1 { margin-bottom: .25rem; }
.meta { color: #666; margin-top: 0; }
code { background: #f4f4f4; padding: .1rem .25rem; border-radius: .2rem; font-size: .9em; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; margin-top: 1rem; }
.card { border: 1px solid #ddd; padding: .75rem; border-radius: .5rem; background: #fafafa; }
.card h3 { margin: 0 0 .25rem 0; font-size: .9rem; color: #666; font-weight: 500; }
.card span { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 1.1rem; }
label { display: inline-block; margin-right: .5rem; }
input, select { padding: .35rem; }
button { padding: .5rem 1rem; }
#status { margin-top: 1rem; color: #666; }
#log { margin-top: 1rem; font-family: ui-monospace, monospace; font-size: .85rem; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>Wickra WASM — parallel assets</h1>
<p class="meta">
Builds a synthetic <code>(assets, bars)</code> panel, runs a serial
baseline on the main thread, then dispatches the same workload to a
pool of <code>Worker</code>s — each loading its own copy of the
WebAssembly module — and reports the speedup. The browser counterpart
of <code>examples/node/parallel_assets.js</code> and
<code>examples/rust/src/bin/parallel_assets.rs</code>.
</p>
<p>
<label>Assets: <input id="assets" type="number" value="200" min="1" max="5000" /></label>
<label>Bars: <input id="bars" type="number" value="5000" min="100" max="200000" /></label>
<label>Indicator:
<select id="indicator"><option>sma</option><option>rsi</option></select>
</label>
<label>Workers: <input id="workers" type="number" value="4" min="1" max="32" /></label>
<button id="go" disabled>Run</button>
</p>
<div class="grid">
<div class="card"><h3>Serial</h3><span id="t-serial"></span></div>
<div class="card"><h3>Parallel</h3><span id="t-parallel"></span></div>
<div class="card"><h3>Speedup</h3><span id="speedup"></span></div>
</div>
<p id="status">Loading WASM module…</p>
<div id="log"></div>
<script type="module">
import init, { version, installPanicHook, SMA, RSI } from "../../bindings/wasm/pkg/wickra_wasm.js";
const $ = (id) => document.getElementById(id);
// Deterministic LCG matching the Node and Rust siblings, so a
// side-by-side run on three runtimes produces comparable timings.
function makeRng(seed) {
let state = seed >>> 0;
return () => {
state = (Math.imul(state, 1103515245) + 12345) & 0x7fffffff;
return state / 0x7fffffff;
};
}
function synthesizePanel(nAssets, nBars) {
const panel = new Array(nAssets);
for (let a = 0; a < nAssets; a++) {
const rng = makeRng((1234567 + Math.imul(a, 2654435761)) >>> 0);
const series = new Float64Array(nBars);
let price = 100;
for (let i = 0; i < nBars; i++) {
price += (rng() - 0.5) * 0.4;
series[i] = price;
}
panel[a] = series;
}
return panel;
}
function lastValue(prices, indicator) {
const ind = indicator === "sma" ? new SMA(14) : new RSI(14);
let last = null;
for (let i = 0; i < prices.length; i++) {
const v = ind.update(prices[i]);
if (v !== null) last = v;
}
return last;
}
async function runWorker(panelSlice, indicator) {
return new Promise((resolve, reject) => {
const worker = new Worker(
new URL("./parallel_worker.js", import.meta.url),
{ type: "module" },
);
worker.addEventListener("message", (e) => {
worker.terminate();
resolve(e.data);
});
worker.addEventListener("error", (e) => {
worker.terminate();
reject(new Error(e.message || "worker error"));
});
worker.postMessage({ panelSlice, indicator });
});
}
async function run() {
const assets = Number($("assets").value);
const bars = Number($("bars").value);
const indicator = $("indicator").value;
const workerCount = Math.max(1, Math.min(Number($("workers").value), assets));
if (!Number.isInteger(assets) || assets <= 0 ||
!Number.isInteger(bars) || bars <= 0) {
$("status").textContent = "error: assets and bars must be positive integers";
return;
}
$("go").disabled = true;
$("status").textContent = `Generating ${assets}×${bars} synthetic panel…`;
await new Promise((r) => setTimeout(r, 0));
const panel = synthesizePanel(assets, bars);
// Serial baseline (main thread).
$("status").textContent = `Running serial baseline…`;
await new Promise((r) => setTimeout(r, 0));
let t0 = performance.now();
const serial = new Array(assets);
for (let a = 0; a < assets; a++) {
serial[a] = lastValue(panel[a], indicator);
}
const tSerial = (performance.now() - t0) / 1000;
$("t-serial").textContent = `${tSerial.toFixed(3)} s`;
// Parallel via Worker pool.
$("status").textContent = `Dispatching to ${workerCount} workers…`;
await new Promise((r) => setTimeout(r, 0));
const sliceSize = Math.ceil(assets / workerCount);
t0 = performance.now();
const promises = [];
for (let w = 0; w < workerCount; w++) {
const start = w * sliceSize;
const end = Math.min(start + sliceSize, assets);
if (start >= end) continue;
const panelSlice = panel.slice(start, end);
promises.push(
runWorker(panelSlice, indicator).then((results) => ({ start, results })),
);
}
try {
const chunks = await Promise.all(promises);
const tParallel = (performance.now() - t0) / 1000;
const parallel = new Array(assets);
for (const { start, results } of chunks) {
for (let j = 0; j < results.length; j++) parallel[start + j] = results[j];
}
$("t-parallel").textContent = `${tParallel.toFixed(3)} s`;
$("speedup").textContent = `${(tSerial / Math.max(tParallel, 1e-9)).toFixed(2)}×`;
// Sanity check: serial and parallel must agree per asset.
let mismatches = 0;
for (let i = 0; i < assets; i++) {
if (serial[i] !== parallel[i] && !(serial[i] === null && parallel[i] === null)) {
mismatches += 1;
}
}
$("log").textContent = mismatches === 0
? `Parallel results match serial results — OK (assets=${assets}, bars=${bars}).`
: `WARNING: ${mismatches} asset(s) disagree between serial and parallel.`;
$("status").textContent = `Done.`;
} catch (err) {
$("status").textContent = `error: ${err.message || err}`;
} finally {
$("go").disabled = false;
}
}
$("go").onclick = run;
init().then(() => {
installPanicHook();
$("go").disabled = false;
$("status").textContent = `Ready — wickra ${version()}. Click "Run".`;
});
</script>
</body>
</html>