feat: impl benchmark & do some optimizations

This commit is contained in:
2025-07-13 00:24:29 +02:00
parent cb7971814d
commit 54e225ea6a
9 changed files with 246 additions and 11 deletions

View File

@@ -24,7 +24,6 @@
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4",
"@pow-captcha/solver-wasm": "workspace:*",
"@pow-captcha/pow-captcha": "workspace:*"
}
}

View File

@@ -1,5 +1,84 @@
import "./App.css";
// import * as wasm from "@pow-captcha/solver-wasm";
import * as powCaptcha from "@pow-captcha/pow-captcha";
import { useEffect, useRef, useState } from "react";
async function benchmark(cb: () => void | Promise<void>): Promise<number> {
const start = performance.now();
await cb();
const end = performance.now();
return end - start;
}
export default function App() {
return <div>My Benchmark</div>;
const [durationJs, setDurationJs] = useState<null | number>(null);
const [durationWasm, setDurationWasm] = useState<null | number>(null);
const [runBenchmark, setRunBenchmark] = useState(false);
const initStartedRef = useRef(false);
useEffect(() => {
if (!runBenchmark || initStartedRef.current) {
return;
}
initStartedRef.current = true;
async function init() {
const challengesRaw = await powCaptcha.server.createChallengesRaw({
difficulty: 2,
challengeCount: 64,
});
const challenges = challengesRaw.challenges.map(
(challenge) =>
[
powCaptcha.wire.deserializeArray(challenge[0]),
powCaptcha.wire.deserializeArray(challenge[1]),
] as const,
);
const durationWasm = await benchmark(async () => {
const solutions = await powCaptcha.solver.solveChallenges(
challenges,
"wasm",
);
console.log("wasm solutions", solutions);
});
setDurationWasm(durationWasm);
// const durationJs = await benchmark(async () => {
// const solutions = await powCaptcha.solver.solveChallenges(
// challenges,
// "js",
// );
// console.log("js solutions", solutions);
// });
// setDurationJs(durationJs);
}
init();
}, [runBenchmark]);
return (
<div>
<h1>pow-captcha Benchmark</h1>
<button onClick={() => setRunBenchmark(true)}>run</button>
Results:
<table>
<tr>
<th>type</th>
<th>duration</th>
</tr>
<tr>
<td>wasm</td>
<td>{durationWasm}ms</td>
</tr>
<tr>
<td>js</td>
<td>{durationJs}ms</td>
</tr>
</table>
</div>
);
}

View File

@@ -1,9 +1,15 @@
import { defineConfig } from "vite";
import { defineConfig, type PluginOption } from "vite";
import react from "@vitejs/plugin-react";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
const plugins: Array<PluginOption> = [react(), wasm(), topLevelAwait()];
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), wasm(), topLevelAwait()],
plugins,
worker: {
format: "es",
plugins: () => plugins,
},
});

View File

@@ -11,7 +11,8 @@
},
"dependencies": {
"base64-js": "^1.5.1",
"zod": "^4"
"zod": "^4",
"@pow-captcha/solver-wasm": "workspace:*"
},
"devDependencies": {
"@types/node": "^24.0.4",

View File

@@ -0,0 +1,72 @@
import * as solver from "./solver";
// import * as wasm from "@pow-captcha/solver-wasm";
export type Challenge = readonly [Uint8Array, Uint8Array];
export type WorkerRequest = {
engine?: undefined | "js" | "wasm";
challenges: ReadonlyArray<Challenge>;
};
export type WorkerResponse = {
solutions: ReadonlyArray<Uint8Array>;
};
async function solveWasm(
nonce: Uint8Array,
target: Uint8Array,
): Promise<Uint8Array> {
const wasm = await import("@pow-captcha/solver-wasm");
return wasm.solve(nonce, target);
}
async function solve(
nonce: Uint8Array,
target: Uint8Array,
engine: undefined | "js" | "wasm",
): Promise<Uint8Array> {
switch (engine) {
case "js":
return await solver.solveJs(nonce, target);
case "wasm":
return await solveWasm(nonce, target);
case undefined:
try {
return await solveWasm(nonce, target);
} catch (err) {
console.warn(
"pow-captcha: Falling back to js solver. Error: ",
err,
);
return await solver.solveJs(nonce, target);
}
}
}
async function processMessage(m: MessageEvent<WorkerRequest>): Promise<void> {
const { challenges, engine } = m.data;
// const solutions = await Promise.all(
// challenges.map(([nonce, target]) => solve(nonce, target, engine)),
// );
const solutions: Array<Uint8Array> = [];
for (const [nonce, target] of challenges) {
const solution = await solve(nonce, target, engine);
solutions.push(solution);
}
const res: WorkerResponse = {
solutions,
};
postMessage(res);
}
onmessage = (m: MessageEvent<WorkerRequest>) => {
console.log("onmessage", m);
processMessage(m).catch((err) => {
console.error("pow-captcha: Failure in worker: ", err);
});
};

View File

@@ -1,4 +1,5 @@
import { arrayStartsWith } from "./utils";
import type { WorkerRequest, WorkerResponse } from "./solver-worker";
import { arrayStartsWith, chunkArray } from "./utils";
export async function solveJs(
nonce: Uint8Array,
@@ -30,6 +31,48 @@ export async function solveJs(
}
}
export async function solveChallenges(
challenges: ReadonlyArray<readonly [Uint8Array, Uint8Array]>,
engine?: "wasm" | "js",
): Promise<Array<Uint8Array>> {
const workerChallenges = chunkArray(
Math.floor(challenges.length / navigator.hardwareConcurrency),
challenges,
);
console.log("workerChallenges", workerChallenges);
const workers = workerChallenges.map(async (challenges) => {
const worker = new Worker(
new URL("./solver-worker.js", import.meta.url),
{
type: "module",
},
);
try {
const resultPromise = new Promise<ReadonlyArray<Uint8Array>>(
(onOk, onErr) => {
worker.onerror = onErr;
worker.onmessage = (m: MessageEvent<WorkerResponse>) => {
console.log("worker msg", m);
onOk(m.data.solutions);
};
},
);
const req: WorkerRequest = { challenges, engine };
worker.postMessage(req);
return await resultPromise;
} finally {
worker.terminate();
}
});
return (await Promise.all(workers)).flat();
}
export async function verify(
nonce: Uint8Array,
target: Uint8Array,

View File

@@ -18,7 +18,10 @@ export function byteArraysEqual(arr1: Uint8Array, arr2: Uint8Array): boolean {
return true;
}
export function arrayStartsWith(array: Uint8Array, search: Uint8Array): boolean {
export function arrayStartsWith(
array: Uint8Array,
search: Uint8Array,
): boolean {
const searchLen = search.length;
if (searchLen > array.length) {
return false;
@@ -30,3 +33,35 @@ export function arrayStartsWith(array: Uint8Array, search: Uint8Array): boolean
}
return true;
}
export function chunkArray<T>(
chunkSize: number,
input: ReadonlyArray<T>,
): Array<Array<T>> {
const chunks: Array<Array<T>> = [];
for (let i = 0; i < input.length; i += chunkSize) {
const chunk = input.slice(i, i + chunkSize);
chunks.push(chunk);
}
return chunks;
}
// export function distributeArray<T>(
// numBuckets: number,
// input: ReadonlyArray<T>,
// ): Array<Array<T>> {
// // input.length / numBuckets
// const chunkSize = Math.ceil();
// return createArray(numBuckets, (bucket): Array<T> => {
// const start = bucket * chunkSize;
// return input.slice(start, start + chunkSize);
// });
// // for (let i = 0; i < input.length; i += 1) {
// // const bucketIndex = Math.floor(i / (input.length / numBuckets));
// // distributed[bucketIndex]!.push(input[i]!);
// // }
// // return distributed;
// }