Files
mfahampshire 43a1bd38e8 Max/smolmix wasm (#6784)
* Mod gitignore + license trimming + comment trimming

* Big rewrite

* SURB inputs + DNS button in internal-dev

* Make ipr addr optional

* Accidentatly omitted files from rewrite commit

* Makefile + readme

* Comment rewrite

* Optimisation comment

* Replace manual waker map with
      smoltcp built-ins + adaptive poll

* Comments

* Extract socket creation helpers into stream.rs

* Cleanup comments

* Comment

* Comment notes and restrict ciphersuites wrt rustls-rustcrypto

* Dep. hack fix for demo + add clearnet fetch() for contrast

* Stripped down devtester

* Fix Clippy arg (fatfingered deletion)

* CodeRabbit catches

* Cargofmt

* Review nits: bridge logs, fetch early-return, static port counter, copyright years, README + Cargo + headless.js tidying

* PHONY + taskset override, switch internal-dev/tests to pnpm, fix wasm-pack out-dir

* Gate codec tests behind the codec feature for no-default-features builds

* IPv6 addr/route on smoltcp iface + configurable DNS resolvers via TunnelOpts

* DNS GUI inputs, close stale WS on reconnect, worker init guards + ws-send warning, Playwright listener cleanup, pnpm-lock in internal-dev

* Fix lp -> lp-data after rebase

* Revert nym-lp/nym-lp-data feature-gating left over from rebase

* Lift getrandom wasm_js cfg to workspace .cargo/config.toml so cargo check -p smolmix-wasm works from any CWD

* temp will amend git message

* Auto-discover IPR when none specified + 'Use random IPR' checkbox in internal-dev

* smolmix_tracker + State machine + ready_tunnel gate + getTunnelState JS surface

* Mirror red display() entries to console.error

* Add left out package-lock

* Reactor clock + yield_now + atomic seq + gateway-storage errors

* setupMixTunnel gate + MTU 1980 + http::Uri cleanup

* Review pass + fix test + clippy

* restore axum 0.8 bump from borked earlier merge

* Feature gating (dns/fetch/socket) + TunnelOptsBuilder + pnpm bypass

* Cont. with review comments

* tokio Nofity reactor wakes + cancellation + setup polishing

* Notify wakes + inner pattern + close_notify + util

* Tunable tunnelopts

* Fix tired commit

* CI prep

* Lint + Clippy

* coderabbit u32 fix

* nits + runtime debugging + expose in internal-dev

* remove redudant default-features

* Remove more redundant default-features
2026-05-28 15:57:10 +00:00

317 lines
8.6 KiB
JavaScript

// smolmix-wasm headless test runner
//
// Auto-runs a battery of tests on page load. Config via URL params:
//
// ?ipr=<address> IPR address (uses default if omitted)
// ?cover=true Enable cover traffic (default: disabled)
// ?poisson=true Enable Poisson traffic (default: disabled)
// ?count=10 Stress test request count
//
// Two runs needed for the full matrix (OnceLock prevents re-init):
// http://localhost:9000/headless.html
// http://localhost:9000/headless.html?cover=true&poisson=true
import * as Comlink from "comlink";
// Config
const params = new URLSearchParams(location.search);
const IPR_ADDRESS =
params.get("ipr") ||
"6B6iuWX4bQP4GVA4Yq7XmZencaaGw6BaPY6xJWYSwsbF.6g6LRx1fgU2Q2A4ZPKonYHtfBARh1GPMe1LtXk6vpRR8@q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1";
const ENABLE_COVER = params.get("cover") === "true";
const ENABLE_POISSON = params.get("poisson") === "true";
// Clamp to [1, 500]: parseInt of garbage returns NaN, negative values would
// underflow the test loop, very large values would exhaust browser memory.
const STRESS_COUNT = Math.min(500, Math.max(1, parseInt(params.get("count") || "10", 10) || 10));
const CONFIG_LABEL = `cover=${ENABLE_COVER ? "ON" : "OFF"}, poisson=${
ENABLE_POISSON ? "ON" : "OFF"
}`;
// Output
const outputEl = document.getElementById("output");
function log(msg) {
const ts = new Date().toISOString().slice(11, 23);
const line = `[${ts}] ${msg}`;
outputEl.textContent += line + "\n";
outputEl.scrollTop = outputEl.scrollHeight;
console.log(line);
}
// Worker setup (shared worker.js)
function createWorker() {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL("./worker.js", import.meta.url));
worker.addEventListener("error", reject);
worker.addEventListener(
"message",
(msg) => {
if (msg.data?.kind === "Loaded") {
worker.removeEventListener("error", reject);
resolve(Comlink.wrap(worker));
}
},
{ once: true }
);
});
}
function toResponse(raw) {
return new Response(raw.body, {
status: raw.status,
statusText: raw.statusText,
headers: new Headers(raw.headers),
});
}
// Helpers
async function timedFetch(api, url, init = {}) {
const t0 = performance.now();
const raw = await api.mixFetch(url, init);
const ms = performance.now() - t0;
const resp = toResponse(raw);
return { ok: resp.ok, status: resp.status, statusText: resp.statusText, ms };
}
function fmtMs(ms) {
return (ms / 1000).toFixed(2) + "s";
}
// Test: Smoke (cold HTTPS GET)
async function testSmoke(api) {
log("");
log("=== Smoke Test (cold HTTPS GET) ===");
log("GET https://httpbin.org/get");
try {
const r = await timedFetch(api, "https://httpbin.org/get");
log(` ${r.status} ${r.statusText} in ${fmtMs(r.ms)}`);
return { name: "Smoke (cold HTTPS)", ok: true, ms: r.ms };
} catch (e) {
log(` FAIL: ${e}`);
return { name: "Smoke (cold HTTPS)", ok: false, ms: 0, error: String(e) };
}
}
// Test: HTTPS GET (warm, pooled connection)
async function testHttpsGetWarm(api) {
log("");
log("=== HTTPS GET (warm) ===");
log("GET https://httpbin.org/get (pooled connection)");
try {
const r = await timedFetch(api, "https://httpbin.org/get");
log(` ${r.status} ${r.statusText} in ${fmtMs(r.ms)}`);
return { name: "HTTPS GET (warm)", ok: true, ms: r.ms };
} catch (e) {
log(` FAIL: ${e}`);
return { name: "HTTPS GET (warm)", ok: false, ms: 0, error: String(e) };
}
}
// Test: Stress (httpbin mixed sizes)
const SIZE_PROFILES = [
{ label: "tiny", bytes: 128 },
{ label: "small", bytes: 1024 },
{ label: "medium", bytes: 10240 },
{ label: "large", bytes: 102400 },
];
async function testStressHttpbin(api) {
log("");
log(`=== Stress Test: httpbin (${STRESS_COUNT} requests, mixed sizes) ===`);
const requests = [];
for (let i = 0; i < STRESS_COUNT; i++) {
const p = SIZE_PROFILES[Math.floor(Math.random() * SIZE_PROFILES.length)];
requests.push({
id: i + 1,
url: `https://httpbin.org/bytes/${p.bytes}`,
label: p.label,
});
}
// Log profile distribution
const dist = {};
for (const r of requests) dist[r.label] = (dist[r.label] || 0) + 1;
log(` Profiles: ${JSON.stringify(dist)}`);
const t0 = performance.now();
const perReq = [];
// Fire all concurrently (origin lock serialises per-host)
const settled = await Promise.allSettled(
requests.map(async (req) => {
const start = performance.now();
try {
const r = await timedFetch(api, req.url);
const elapsed = performance.now() - start;
log(` #${req.id} ${req.label}: ${r.status} OK ${fmtMs(elapsed)}`);
perReq.push({ id: req.id, label: req.label, ok: true, ms: elapsed });
} catch (e) {
const elapsed = performance.now() - start;
log(` #${req.id} ${req.label}: FAIL ${fmtMs(elapsed)}${e}`);
perReq.push({ id: req.id, label: req.label, ok: false, ms: elapsed });
}
})
);
const totalMs = performance.now() - t0;
const okCount = perReq.filter((r) => r.ok).length;
const avgMs =
perReq.filter((r) => r.ok).reduce((s, r) => s + r.ms, 0) / (okCount || 1);
log(
` Result: ${okCount}/${STRESS_COUNT} OK, total ${fmtMs(
totalMs
)}, avg ${fmtMs(avgMs)}/req`
);
return {
name: `Stress httpbin (${STRESS_COUNT})`,
ok: okCount === STRESS_COUNT,
ms: totalMs,
okCount,
total: STRESS_COUNT,
avgMs,
perReq,
};
}
// Summary
function printSummary(results) {
log("");
log("================================================================");
log(` smolmix-wasm test results`);
log(` Config: ${CONFIG_LABEL}`);
log(` Date: ${new Date().toISOString()}`);
log("================================================================");
log("");
const nameWidth = 28;
const resultWidth = 10;
log(` ${"Test".padEnd(nameWidth)}${"Result".padEnd(resultWidth)}Time`);
log(
` ${"".padEnd(nameWidth, "-")}${"".padEnd(resultWidth, "-")}${"".padEnd(
20,
"-"
)}`
);
for (const r of results) {
let resultStr;
if (r.total !== undefined) {
resultStr = `${r.okCount}/${r.total}`;
} else {
resultStr = r.ok ? "PASS" : "FAIL";
}
let timeStr = r.ms ? fmtMs(r.ms) : "N/A";
if (r.avgMs !== undefined) {
timeStr += ` (avg ${fmtMs(r.avgMs)}/req)`;
}
log(
` ${r.name.padEnd(nameWidth)}${resultStr.padEnd(resultWidth)}${timeStr}`
);
}
log("");
log("================================================================");
// Also output as JSON for programmatic consumption
const json = {
config: { cover: ENABLE_COVER, poisson: ENABLE_POISSON },
date: new Date().toISOString(),
results: results.map((r) => ({
name: r.name,
ok: r.ok,
ms: Math.round(r.ms),
...(r.okCount !== undefined && { okCount: r.okCount, total: r.total }),
...(r.avgMs !== undefined && { avgMs: Math.round(r.avgMs) }),
...(r.error && { error: r.error }),
})),
};
// Machine-readable output for Playwright (bypasses log() timestamp prefix)
console.log("RESULTS_JSON:" + JSON.stringify(json));
}
// Main
async function runSuite() {
log("smolmix-wasm headless test runner");
log(`Config: ${CONFIG_LABEL}`);
log(`Stress count: ${STRESS_COUNT}`);
log(`IPR: ${IPR_ADDRESS.slice(0, 40)}...`);
log("");
// 1. Start worker
log("Starting worker...");
let api;
try {
api = await createWorker();
log("Worker started");
} catch (e) {
log(`FATAL: Worker creation failed: ${e}`);
printSummary([{ name: "Worker init", ok: false, ms: 0, error: String(e) }]);
return;
}
// 2. Setup tunnel
log("Setting up tunnel...");
const setupT0 = performance.now();
try {
await api.setupMixTunnel({
preferredIpr: IPR_ADDRESS,
clientId: "headless-" + Math.random().toString(36).slice(2, 8),
forceTls: true,
disablePoissonTraffic: !ENABLE_POISSON,
disableCoverTraffic: !ENABLE_COVER,
});
const setupMs = performance.now() - setupT0;
log(`Tunnel ready in ${fmtMs(setupMs)}`);
} catch (e) {
log(`FATAL: Tunnel setup failed: ${e}`);
printSummary([
{ name: "Tunnel setup", ok: false, ms: 0, error: String(e) },
]);
return;
}
// 3. Run tests sequentially
const results = [];
results.push(await testSmoke(api));
results.push(await testHttpsGetWarm(api));
results.push(await testStressHttpbin(api));
// 4. Summary
printSummary(results);
// 5. Disconnect
log("");
log("Disconnecting...");
try {
await api.disconnectMixTunnel();
log("Disconnected");
} catch (e) {
log(`Disconnect error: ${e}`);
}
log("Done.");
}
runSuite();