Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: [2, 3, 4]

# Widget

Cap's client-side widget handles requesting, solving and displaying challenges using a native web component and rust-flavoured WASM. It also includes the [programmatic mode](./programmatic).
Cap's client-side widget handles requesting, solving and displaying challenges using a native web component and a C-based WASM solver. It also includes the [programmatic mode](./programmatic).

## Installation

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/workings.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ By the way, this is a more technical explanation of how Cap works. If you're loo

#### Computing the solution

5. The widget uses Rust-flavoured WASM and Web Workers to solve the challenges in parallel:
5. The widget uses a C-based WASM solver and Web Workers to solve the challenges in parallel:
- Each worker attempts to find a valid nonce by repeatedly:
- Combining the salt with different nonce values
- Computing the SHA-256 hash of this combination
Expand Down
42 changes: 42 additions & 0 deletions wasm/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Benchmark

This benchmark compares the historical Rust solver from commit `da725dba93f61099d264bc597d22ff388d09d2ad` against the current C solver at `HEAD`.

## What it does

- Rebuilds the Rust implementation from `benchmark/rust`
- Rebuilds the current C implementation from `src/c`
- Loads both Node targets
- Verifies the same challenge cases before timing them
- Reports average time per solve and the relative speedup

## Run it

```bash
bun --cwd benchmark run bench
```

Optional environment variables:

- `BENCH_ITERATIONS` controls the timed loops, default `10`
- `BENCH_WARMUP` controls warmup rounds, default `2`

## Browser version

This repo also includes a browser-hosted version that loads the wasm modules in a real browser
context.

```bash
bun --cwd benchmark run bench:browser
```

Then open the printed local URL. You can override the browser benchmark loop counts with query
parameters like `?iterations=20&warmup=3`.

## Requirements

- `make`
- `clang`
- `git`
- `rustup`
- the `wasm32-unknown-unknown` target installed for the rustup-managed toolchain
62 changes: 62 additions & 0 deletions wasm/benchmark/bench.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import path from "node:path";
import { createRequire } from "node:module";
import { buildAll, headDir, rustDir } from "./build-artifacts.js";
import { benchmarkSolver, rustBaselineCommit, verifySolver } from "./shared.js";

const iterations = Number.parseInt(process.env.BENCH_ITERATIONS ?? "10", 10);
const warmupRounds = Number.parseInt(process.env.BENCH_WARMUP ?? "2", 10);

if (!Number.isFinite(iterations) || iterations < 1) {
throw new Error("BENCH_ITERATIONS must be a positive integer");
}

if (!Number.isFinite(warmupRounds) || warmupRounds < 0) {
throw new Error("BENCH_WARMUP must be a non-negative integer");
}

function loadSolvePow(modulePath) {
const require = createRequire(import.meta.url);
const mod = require(modulePath);

if (typeof mod.solve_pow !== "function") {
throw new Error(`missing solve_pow in ${modulePath}`);
}

return mod.solve_pow;
}

buildAll();

const headSolvePow = loadSolvePow(path.join(headDir, "cap_wasm.js"));
const rustSolvePow = loadSolvePow(path.join(rustDir, "cap_wasm.js"));

verifySolver("HEAD (C)", headSolvePow);
verifySolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow);

const headResult = benchmarkSolver("HEAD (C)", headSolvePow, {
iterations,
warmupRounds,
});
const rustResult = benchmarkSolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow, {
iterations,
warmupRounds,
});

if (headResult.checksum !== rustResult.checksum) {
throw new Error(
`checksum mismatch: ${headResult.checksum.toString()} != ${rustResult.checksum.toString()}`,
);
}

console.log(`\nBenchmark results`);
console.log(
`${headResult.label.padEnd(14)} ${headResult.elapsedMs.toFixed(2)} ms total ` +
`(${headResult.perSolveMs.toFixed(4)} ms/solve)`,
);
console.log(
`${rustResult.label.padEnd(14)} ${rustResult.elapsedMs.toFixed(2)} ms total ` +
`(${rustResult.perSolveMs.toFixed(4)} ms/solve)`,
);
console.log(
`Speedup: ${(rustResult.perSolveMs / headResult.perSolveMs).toFixed(2)}x faster on HEAD`,
);
103 changes: 103 additions & 0 deletions wasm/benchmark/browser/bench.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { benchmarkSolver, challengeCases, rustBaselineCommit, verifySolver } from "../shared.js";

const statusEl = document.getElementById("status");
const metaEl = document.getElementById("meta");
const outputEl = document.getElementById("output");

function log(line) {
outputEl.textContent += `${line}\n`;
console.log(line);
}

function fail(error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
statusEl.textContent = "Failed.";
outputEl.textContent += `${message}\n`;
console.error(error);
}

window.addEventListener("error", (event) => {
fail(event.error ?? event.message);
});

window.addEventListener("unhandledrejection", (event) => {
fail(event.reason);
});

function readIntParam(name, fallback) {
const value = new URLSearchParams(location.search).get(name);

if (value === null) {
return fallback;
}

const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error(`${name} must be a non-negative integer`);
}

return parsed;
}

const iterations = readIntParam("iterations", 10);
const warmupRounds = readIntParam("warmup", 2);

metaEl.textContent = `iterations=${iterations}, warmup=${warmupRounds}, cases=${challengeCases.length}`;

try {
const cacheToken = Date.now();
const headModule = await import(`/out/head/browser/cap_wasm.js?cache=${cacheToken}`);
const rustModule = await import(`/out/rust/browser/cap_wasm.js?cache=${cacheToken}`);

const initHead = headModule.default;
const headSolvePow = headModule.solve_pow;
const initRust = rustModule.default;
const rustSolvePow = rustModule.solve_pow;

statusEl.textContent = "Loading wasm modules...";
log("Loading wasm modules...");
await Promise.all([initHead(), initRust()]);
log("Wasm modules loaded.");

statusEl.textContent = "Verifying solver outputs...";
log("Verifying solver outputs...");
await new Promise((resolve) => requestAnimationFrame(resolve));

verifySolver("HEAD (C)", headSolvePow);
verifySolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow);
log("Verification complete.");

statusEl.textContent = "Running benchmark...";
log("Running benchmark...");
await new Promise((resolve) => requestAnimationFrame(resolve));

const headResult = benchmarkSolver("HEAD (C)", headSolvePow, {
iterations,
warmupRounds,
});
const rustResult = benchmarkSolver(`Rust (${rustBaselineCommit.slice(0, 7)})`, rustSolvePow, {
iterations,
warmupRounds,
});

if (headResult.checksum !== rustResult.checksum) {
throw new Error(
`checksum mismatch: ${headResult.checksum.toString()} != ${rustResult.checksum.toString()}`,
);
}

log("Benchmark results");
log(
`${headResult.label.padEnd(14)} ${headResult.elapsedMs.toFixed(2)} ms total ` +
`(${headResult.perSolveMs.toFixed(4)} ms/solve)`,
);
log(
`${rustResult.label.padEnd(14)} ${rustResult.elapsedMs.toFixed(2)} ms total ` +
`(${rustResult.perSolveMs.toFixed(4)} ms/solve)`,
);
log(`Speedup: ${(rustResult.perSolveMs / headResult.perSolveMs).toFixed(2)}x faster on HEAD`);

statusEl.textContent = "Done.";
} catch (error) {
fail(error);
}
109 changes: 109 additions & 0 deletions wasm/benchmark/browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wasm Browser Benchmark</title>
<link rel="icon" href="data:," />
<style>
:root {
color-scheme: dark;
--bg: #0b1020;
--panel: rgba(13, 21, 42, 0.88);
--panel-border: rgba(255, 255, 255, 0.12);
--text: #e8edf7;
--muted: #94a3b8;
--accent: #7dd3fc;
--accent-2: #a78bfa;
}

html,
body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at top left, rgba(125, 211, 252, 0.16), transparent 28%),
radial-gradient(circle at top right, rgba(167, 139, 250, 0.2), transparent 26%),
linear-gradient(180deg, #070b15 0%, var(--bg) 100%);
color: var(--text);
font: 16px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}

body {
display: grid;
place-items: center;
padding: 24px;
box-sizing: border-box;
}

main {
width: min(900px, 100%);
border: 1px solid var(--panel-border);
background: var(--panel);
backdrop-filter: blur(18px);
border-radius: 20px;
padding: 28px;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}

h1 {
margin: 0 0 8px;
font-size: clamp(1.8rem, 4vw, 3rem);
line-height: 1.05;
}

p {
margin: 0;
color: var(--muted);
}

pre {
margin: 24px 0 0;
padding: 18px;
overflow: auto;
border-radius: 16px;
background: rgba(2, 6, 23, 0.6);
border: 1px solid rgba(148, 163, 184, 0.18);
color: var(--text);
white-space: pre-wrap;
}

.status {
margin-top: 20px;
color: var(--accent);
font-weight: 600;
letter-spacing: 0.01em;
}

.meta {
margin-top: 8px;
color: var(--muted);
font-size: 0.95rem;
}
</style>
</head>
<body>
<main>
<h1>Wasm Browser Benchmark</h1>
<p>
Runs the C and Rust solvers in an actual browser context and compares their average solve
time.
</p>
<div class="status" id="status">Waiting to load wasm modules...</div>
<div class="meta" id="meta"></div>
<pre id="output"></pre>
</main>
<script type="module">
const status = document.getElementById("status");
status.textContent = "Booting benchmark...";

import(`/browser/bench.js?cache=${Date.now()}`).catch((error) => {
status.textContent = "Failed to boot.";
console.error(error);
const output = document.getElementById("output");
output.textContent = `${error?.stack ?? error?.message ?? String(error)}\n`;
});
</script>
</body>
</html>
Loading