From 54e225ea6aaf105330f7e624b4c6fc3e0df47d1a Mon Sep 17 00:00:00 2001 From: Thomas Heck Date: Sun, 13 Jul 2025 00:24:29 +0200 Subject: [PATCH] feat: impl benchmark & do some optimizations --- pkgs/benchmark/package.json | 1 - pkgs/benchmark/src/App.css | 0 pkgs/benchmark/src/App.tsx | 83 ++++++++++++++++++++++++++- pkgs/benchmark/vite.config.ts | 10 +++- pkgs/pow-captcha/package.json | 3 +- pkgs/pow-captcha/src/solver-worker.ts | 72 +++++++++++++++++++++++ pkgs/pow-captcha/src/solver.ts | 45 ++++++++++++++- pkgs/pow-captcha/src/utils.ts | 37 +++++++++++- pnpm-lock.yaml | 6 +- 9 files changed, 246 insertions(+), 11 deletions(-) delete mode 100644 pkgs/benchmark/src/App.css create mode 100644 pkgs/pow-captcha/src/solver-worker.ts diff --git a/pkgs/benchmark/package.json b/pkgs/benchmark/package.json index ce18925..2800e3a 100644 --- a/pkgs/benchmark/package.json +++ b/pkgs/benchmark/package.json @@ -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:*" } } diff --git a/pkgs/benchmark/src/App.css b/pkgs/benchmark/src/App.css deleted file mode 100644 index e69de29..0000000 diff --git a/pkgs/benchmark/src/App.tsx b/pkgs/benchmark/src/App.tsx index 8c610a3..89589e3 100644 --- a/pkgs/benchmark/src/App.tsx +++ b/pkgs/benchmark/src/App.tsx @@ -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): Promise { + const start = performance.now(); + await cb(); + const end = performance.now(); + return end - start; +} export default function App() { - return
My Benchmark
; + const [durationJs, setDurationJs] = useState(null); + const [durationWasm, setDurationWasm] = useState(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 ( +
+

pow-captcha Benchmark

+ + Results: + + + + + + + + + + + + + +
typeduration
wasm{durationWasm}ms
js{durationJs}ms
+
+ ); } diff --git a/pkgs/benchmark/vite.config.ts b/pkgs/benchmark/vite.config.ts index 0f3204c..e4e26dc 100644 --- a/pkgs/benchmark/vite.config.ts +++ b/pkgs/benchmark/vite.config.ts @@ -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 = [react(), wasm(), topLevelAwait()]; + // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), wasm(), topLevelAwait()], + plugins, + worker: { + format: "es", + plugins: () => plugins, + }, }); diff --git a/pkgs/pow-captcha/package.json b/pkgs/pow-captcha/package.json index 2551b38..4ff9eb2 100644 --- a/pkgs/pow-captcha/package.json +++ b/pkgs/pow-captcha/package.json @@ -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", diff --git a/pkgs/pow-captcha/src/solver-worker.ts b/pkgs/pow-captcha/src/solver-worker.ts new file mode 100644 index 0000000..0173635 --- /dev/null +++ b/pkgs/pow-captcha/src/solver-worker.ts @@ -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; +}; + +export type WorkerResponse = { + solutions: ReadonlyArray; +}; + +async function solveWasm( + nonce: Uint8Array, + target: Uint8Array, +): Promise { + 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 { + 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): Promise { + const { challenges, engine } = m.data; + + // const solutions = await Promise.all( + // challenges.map(([nonce, target]) => solve(nonce, target, engine)), + // ); + + const solutions: Array = []; + 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) => { + console.log("onmessage", m); + processMessage(m).catch((err) => { + console.error("pow-captcha: Failure in worker: ", err); + }); +}; diff --git a/pkgs/pow-captcha/src/solver.ts b/pkgs/pow-captcha/src/solver.ts index 02fae95..f93deec 100644 --- a/pkgs/pow-captcha/src/solver.ts +++ b/pkgs/pow-captcha/src/solver.ts @@ -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, + engine?: "wasm" | "js", +): Promise> { + 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>( + (onOk, onErr) => { + worker.onerror = onErr; + worker.onmessage = (m: MessageEvent) => { + 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, diff --git a/pkgs/pow-captcha/src/utils.ts b/pkgs/pow-captcha/src/utils.ts index b6861b8..ad2de2a 100644 --- a/pkgs/pow-captcha/src/utils.ts +++ b/pkgs/pow-captcha/src/utils.ts @@ -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( + chunkSize: number, + input: ReadonlyArray, +): Array> { + const chunks: Array> = []; + for (let i = 0; i < input.length; i += chunkSize) { + const chunk = input.slice(i, i + chunkSize); + chunks.push(chunk); + } + return chunks; +} + +// export function distributeArray( +// numBuckets: number, +// input: ReadonlyArray, +// ): Array> { +// // input.length / numBuckets +// const chunkSize = Math.ceil(); + + + +// return createArray(numBuckets, (bucket): Array => { +// 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; +// } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d173057..8102812 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: '@pow-captcha/pow-captcha': specifier: workspace:* version: link:../pow-captcha - '@pow-captcha/solver-wasm': - specifier: workspace:* - version: link:../solver-wasm/dist '@types/react': specifier: ^19.1.8 version: 19.1.8 @@ -83,6 +80,9 @@ importers: pkgs/pow-captcha: dependencies: + '@pow-captcha/solver-wasm': + specifier: workspace:* + version: link:../solver-wasm/dist base64-js: specifier: ^1.5.1 version: 1.5.1