| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 10h ago | 1 | #!/usr/bin/env bash |
| 2 | # Does the web build actually render in parallel, and does it still produce |
| 3 | # the right pixels? |
| 4 | # |
| 5 | # Both halves matter. A worker pool that silently falls back to one thread |
| 6 | # looks exactly like a working one except for the clock, and a pool that |
| 7 | # assembles bands in the wrong order produces a plausible-looking but wrong |
| 8 | # frame. So this times 1 worker against many and compares both against a |
| 9 | # straight single-instance render. |
| 10 | # |
| 11 | # The page POSTs its results back rather than logging them: headless Chrome's |
| 12 | # console plumbing is unreliable, and --virtual-time-budget would make |
| 13 | # performance.now() meaningless for timing anyway. |
| 14 | set -euo pipefail |
| 15 | cd "$(dirname "$0")/.." |
| 16 | |
| 17 | chrome="${CHROME_EXECUTABLE:-$(command -v google-chrome-stable || command -v google-chrome || command -v chromium || true)}" |
| 18 | [ -n "$chrome" ] || { echo "no Chrome found; set CHROME_EXECUTABLE"; exit 1; } |
| 19 | [ -f example/web/nimflutter.js ] || { echo "run 'just wasm' first"; exit 1; } |
| 20 | |
| 21 | work="$(mktemp -d)" |
| 22 | port="${PORT:-8793}" |
| 23 | profile="$work/profile" |
| 24 | trap 'rm -rf "$work"' EXIT |
| 25 | |
| 26 | cp example/web/nimflutter.js example/web/nimflutter.wasm \ |
| 27 | example/web/nimflutter_worker.js example/web/nimflutter_pool.js "$work/" |
| 28 | |
| 29 | cat > "$work/index.html" <<'HTML' |
| 30 | <!DOCTYPE html><meta charset="utf-8"><title>pool</title> |
| 31 | <script src="nimflutter.js"></script> |
| 32 | <script src="nimflutter_pool.js"></script> |
| 33 | <script type="module"> |
| 34 | const W = 800, H = 600, ITER = 500; |
| 35 | const SIG = Array(10).fill('number'); |
| 36 | const report = (lines) => |
| 37 | fetch('/result', {method: 'POST', body: lines.join('\n')}); |
| 38 | |
| 39 | try { |
| 40 | // Ground truth: one instance, one band, on the main thread. |
| 41 | const M = await createNimFlutterModule(); |
| 42 | M.ccall('nf_init', null, [], []); |
| 43 | const n = W * H * 4; |
| 44 | const p = M._malloc(n); |
| 45 | const t0 = performance.now(); |
| 46 | M.ccall('nf_mandelbrot', null, SIG, [p, n, W, H, -0.5, 0.0, 3.0, ITER, 0, H]); |
| 47 | const serialMs = performance.now() - t0; |
| 48 | const truth = M.HEAPU8.slice(p, p + n); |
| 49 | M._free(p); |
| 50 | |
| 51 | const timed = async (tiles) => { |
| 52 | await nimflutterRenderParallel(W, H, -0.5, 0.0, 3.0, ITER, tiles); // warm |
| 53 | const t = performance.now(); |
| 54 | const out = await nimflutterRenderParallel(W, H, -0.5, 0.0, 3.0, ITER, tiles); |
| 55 | return {ms: performance.now() - t, out}; |
| 56 | }; |
| 57 | |
| 58 | const diff = (a, b) => { |
| 59 | if (a.length !== b.length) return -1; |
| 60 | let d = 0; |
| 61 | for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) d++; |
| 62 | return d; |
| 63 | }; |
| 64 | |
| 65 | const one = await timed(1); |
| 66 | const many = await timed(32); |
| 67 | |
| 68 | await report([ |
| 69 | `cores=${navigator.hardwareConcurrency} workers=${nimflutterPoolSize()}`, |
| 70 | `main-thread=${serialMs.toFixed(1)}ms`, |
| 71 | `1-worker=${one.ms.toFixed(1)}ms`, |
| 72 | `32-bands=${many.ms.toFixed(1)}ms`, |
| 73 | `speedup=${(one.ms / many.ms).toFixed(2)}x`, |
| 74 | `diff-1worker=${diff(truth, one.out)}`, |
| 75 | `diff-pool=${diff(truth, many.out)}`, |
| 76 | ]); |
| 77 | } catch (e) { |
| 78 | await report([`error=${e && e.message ? e.message : e}`]); |
| 79 | } |
| 80 | </script> |
| 81 | HTML |
| 82 | |
| 83 | # Serves the directory, and exits after the page POSTs its results to /result. |
| 84 | cat > "$work/server.py" <<'PY' |
| 85 | import sys, http.server |
| 86 | out = sys.argv[1] |
| 87 | class H(http.server.SimpleHTTPRequestHandler): |
| 88 | def do_POST(self): |
| 89 | body = self.rfile.read(int(self.headers['Content-Length'])).decode() |
| 90 | open(out, 'w').write(body) |
| 91 | self.send_response(204); self.end_headers() |
| 92 | raise SystemExit(0) |
| 93 | def log_message(self, *a): pass |
| 94 | http.server.HTTPServer(('127.0.0.1', int(sys.argv[2])), H).serve_forever() |
| 95 | PY |
| 96 | |
| 97 | (cd "$work" && exec python3 server.py results.txt "$port") >/dev/null 2>&1 & |
| 98 | sleep 2 |
| 99 | |
| 100 | "$chrome" --headless=new --no-sandbox --disable-dev-shm-usage \ |
| 101 | --user-data-dir="$profile" --disable-gpu \ |
| 102 | "http://127.0.0.1:$port/index.html" >/dev/null 2>&1 & |
| 103 | chrome_pid=$! |
| 104 | |
| 105 | for _ in $(seq 1 60); do |
| 106 | [ -s "$work/results.txt" ] && break |
| 107 | sleep 1 |
| 108 | done |
| 109 | kill "$chrome_pid" 2>/dev/null || true |
| 110 | kill %1 %2 2>/dev/null || true |
| 111 | |
| 112 | [ -s "$work/results.txt" ] || { echo "FAIL page produced no result"; exit 1; } |
| 113 | { cat "$work/results.txt"; echo; } |
| 114 | grep -q '^error=' "$work/results.txt" && { echo "FAIL page threw"; exit 1; } |
| 115 | grep -q 'diff-1worker=0' "$work/results.txt" || { echo "FAIL 1-worker frame differs"; exit 1; } |
| 116 | grep -q 'diff-pool=0' "$work/results.txt" || { echo "FAIL pooled frame differs"; exit 1; } |
| 117 | echo "ok pooled and single-instance frames are identical" |