🚧 WIP Step 12, temporary pilot

- pilot runner for testing
- will be entirely removed later

AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3
This commit is contained in:
Miguel de Benito Delgado 2026-09-27 11:24:07 +02:00
parent 5afac4a330
commit 9e5620600f
4 changed files with 282 additions and 1 deletions

View File

@ -27,6 +27,7 @@
"build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook",
"build:wasm": "../render-wasm/build frontend",
"build:renderer-benchmarks:runner": "clojure -M:dev:renderer-bench release bench-render-wasm-runner",
"build:renderer-benchmarks:pilot": "clojure -M:dev:renderer-bench release bench-render-wasm-pilot",
"build:app:libs": "node ./scripts/build-libs.js",
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",

View File

@ -195,10 +195,31 @@
:elide-asserts true
:anon-fn-naming-policy :off}}}
:bench-render-wasm-pilot
{:target :node-script
:output-to "target/renderer-benchmarks/pilot.cjs"
:output-dir "target/renderer-benchmarks/pilot/"
:main benches.render-wasm.pilot/-main
:devtools {:autoload false}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:warnings {:fn-deprecated false}}
:release
{:compiler-options
{:fn-invoke-direct true
:elide-asserts true
:anon-fn-naming-policy :off}}}
:bench-render-wasm-browser
{:target :esm
:output-dir "target/renderer-benchmarks/browser/"
:js-options {:js-provider :import}
;; Bundle npm deps (date-fns via app.common): the pilot serves plain
;; static files with no import-map resolver. TODO(10): revisit when
;; ticket 10 owns staging; artifacts must stay self-contained.
:js-options {:js-provider :shadow}
:modules
{:browser

View File

@ -0,0 +1,242 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns benches.render-wasm.pilot
"Interim diagnostic: loads `:rects/load` in headless Chromium and prints the
bridge result JSON on stdout.
TODO(mem:render-wasm/performance/cljs-rewrite/10-process-build-and-server):
the inline static server and artifact layout here are pilot scaffolding.
Ticket 10 owns staging and server ownership; this only serves the release
browser bundle, the prepared renderer artifacts and `pilot.html` on an
ephemeral 127.0.0.1 port.
TODO(mem:render-wasm/performance/cljs-rewrite/11-runner-and-failure-accounting):
CLI surface, orchestration style, attempt loop and exit policy belong to
ticket 11. Raw `.then` chains below are diagnostic scaffolding, not the
runner's interop pattern.
Exit codes: 0 when result JSON is produced (including bridge failure maps,
which are valid pilot output); 1 on harness errors and timeouts. A timeout
closes the browser, so it stops even a synchronously blocked page.
Usage, from `frontend/` with Playwright's Chromium installed:
PENPOT_WASM_PREPARED=1 ../render-wasm/build frontend
pnpm run build:renderer-benchmarks:pilot
node target/renderer-benchmarks/pilot.cjs --seed 42 --screenshot /tmp/pilot-rects.png
Usage, from `frontend/` with Playwright's Chromium installed:
PENPOT_WASM_PREPARED=1 ../render-wasm/build frontend
clojure -M:dev:renderer-bench release bench-render-wasm-browser
pnpm run build:renderer-benchmarks:pilot
node target/renderer-benchmarks/pilot.cjs --seed 42 --screenshot /tmp/pilot-rects.png
The pilot serves the release browser bundle, so compile
`bench-render-wasm-browser` first (after the prepared renderer build
refreshes `shared.js`). Flags: `--seed N` (default 42),
`--timeout-ms N` (default 30000), `--screenshot PATH` (optional untimed
capture for visual inspection)."
(:require
["fs" :as fs]
["http" :as http]
["path" :as path]
["playwright" :as playwright]
[clojure.string :as str]))
(def ^:private default-seed 42)
(def ^:private default-timeout-ms 30000)
(defn- frontend-root
"Resolves the frontend checkout root from the compiled pilot location
(`target/renderer-benchmarks/pilot.cjs`, next to `runner.cjs`). Ticket 10
owns command cwd resolution; the pilot pins its own roots instead."
[]
(path/resolve js/__dirname ".." ".."))
(def ^:private mime
{".html" "text/html"
".js" "text/javascript"
".wasm" "application/wasm"
".json" "application/json"})
(defn- safe-join
"Joins `sub` under `root`, returning nil when it escapes (path traversal)."
[root sub]
(let [joined (path/join root sub)]
(when-not (str/includes? (path/relative root joined) "..")
joined)))
(defn- serve-file!
[^js res file]
(if (and (some? file) (fs/existsSync file))
(let [ext (path/extname file)]
(.writeHead res 200 #js {"Content-Type" (get mime ext "application/octet-stream")})
(.end res (fs/readFileSync file)))
(do (.writeHead res 404 #js {"Content-Type" "text/plain"})
(.end res "not found"))))
(defn- start-server!
"Serves the pilot page, the release browser bundle and the prepared
renderer artifacts. Returns a promise of `{server port}`."
[browser-dir js-dir html-file]
(js/Promise.
(fn [resolve _reject]
(let [^js server (http/createServer
(fn [^js req ^js res]
(let [url (.-url ^js req)]
(cond
(= url "/") (serve-file! res html-file)
(= url "/pilot.html") (serve-file! res html-file)
(str/starts-with? url "/bench/")
(serve-file! res (safe-join browser-dir (subs url (count "/bench/"))))
(str/starts-with? url "/js/")
(serve-file! res (safe-join js-dir (subs url (count "/js/"))))
:else (do (.writeHead res 404 #js {"Content-Type" "text/plain"})
(.end res "not found"))))))]
(.listen server 0 "127.0.0.1"
(fn [] (resolve #js {"server" server
"port" (.-port (.address server))})))))))
(defn- usage
[]
"pilot [--seed N] [--timeout-ms N] [--screenshot PATH]")
(defn- parse-args
[argv]
(let [opts (loop [args argv
opts {:seed default-seed :timeout-ms default-timeout-ms}]
(if (empty? args)
opts
(let [[flag value & rest-args] args]
(cond
(= flag "--seed") (recur rest-args (assoc opts :seed (js/parseInt value 10)))
(= flag "--timeout-ms") (recur rest-args (assoc opts :timeout-ms (js/parseInt value 10)))
(= flag "--screenshot") (recur rest-args (assoc opts :screenshot value))
:else (recur rest-args (assoc opts ::invalid flag))))))
valid-seed? (fn [n] (and (integer? n) (<= 0 n 4294967295)))
valid-timeout? (fn [n] (and (integer? n) (pos? n)))]
(if (and (nil? (::invalid opts))
(valid-seed? (:seed opts)) (valid-timeout? (:timeout-ms opts)))
opts
(do
(.error js/console (str "pilot: invalid args " (pr-str opts)))
(.error js/console (usage))
(js/process.exit 1)))))
(defn- trace
"Diagnostic line on stderr; stdout carries only the result JSON."
[message]
(.error js/console message))
(defn- print-result!
[result]
(println (.stringify js/JSON result nil 2)))
(defn -main
[& argv]
(trace "pilot: parsing args")
(let [opts (parse-args (or argv []))
root (frontend-root)
bundle (path/join root "target" "renderer-benchmarks" "browser")
js-dir (path/join root "resources" "public" "js")
html (path/join root "test" "benches" "render_wasm" "pilot.html")
seed (:seed opts)
deadline (:timeout-ms opts)]
(-> (start-server! bundle js-dir html)
(.then
(fn [info]
(trace "pilot: server up")
(let [server (unchecked-get info "server")
port (unchecked-get info "port")
url (str "http://127.0.0.1:" port "/pilot.html")
close! (fn [] (js/Promise.resolve (.close server)))
;; Holder so the outer timeout path can close a browser
;; the work chain never returned (blocked page, or a
;; launch/context/page failure after launch).
browser-ref (atom nil)
work (-> (-> (.-chromium playwright)
(.launch #js {"args" #js ["--enable-gpu"]}))
(.then (fn [^js pw-browser]
(reset! browser-ref pw-browser)
(let [version (.version pw-browser)]
(-> (.newContext
pw-browser
#js {"viewport"
#js {"width" 1920 "height" 1080}
"deviceScaleFactor" 2})
(.then (fn [^js context]
(-> (.newPage context)
(.then
(fn [^js page]
(trace "pilot: page open")
(-> (.goto page url)
(.then (fn [_]
(.waitForFunction
page
"window.__benchReady === true")))
(.then (fn [_]
(trace "pilot: bridge ready, evaluating")
;; NOTE: a string page-function evaluates
;; as an expression, so inline the args
;; into an IIFE; a passed arg would
;; never arrive.
(let [call (str "(async () => window.__benchBridge.loadRects("
(.stringify
js/JSON
#js {"seed" seed
"module-url" "/js/render-wasm.js"
"wasm-url" "/js/render-wasm.wasm"})
"))()")]
(.evaluate page call))))
(.then (fn [result]
(let [out #js {"seed" seed
"result" result}]
(aset out "browser"
#js {"chromium" version})
(if-let [shot (:screenshot opts)]
(-> (.screenshot
page
#js {"path" shot})
(.then (fn [_]
(aset out "screenshot" shot)
out)))
out))))
(.then (fn [out]
(-> (.close pw-browser)
(.then (fn [_] out)))))
(.catch (fn [cause]
(.close pw-browser)
(throw cause))))))))))))))
guard (js/Promise.
(fn [_resolve reject]
(js/setTimeout
(fn [] (reject (ex-info "pilot timeout" {:timeout-ms deadline})))
deadline)))]
(-> (js/Promise.race #js [work guard])
(.then (fn [out]
(-> (close!)
(.then (fn [_]
(print-result! out)
(js/process.exit 0))))))
(.catch (fn [cause]
;; The work chain may have left a browser open
;; (guard timeout, blocked page, post-launch
;; failure): close it before the server so no
;; Chromium is orphaned. Double close is safe.
(-> (if-let [b @browser-ref]
(-> (.close b)
(.catch (fn [_] nil)))
(js/Promise.resolve nil))
(.then (fn [_] (close!)))
(.then (fn [_]
(println "pilot harness failure:"
(or (ex-message cause) (str cause)))
(js/process.exit 1))))))))))
(.catch (fn [cause]
(println "pilot startup failure:" (or (ex-message cause) (str cause)))
(js/process.exit 1))))))

View File

@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>renderer benchmark pilot</title>
</head>
<body>
<!-- Same one-liner as frontend/resources/polyfills/dynamicImport.js:
the bench bundle is plain ESM and must not depend on app boot. -->
<script>globalThis.dynamicImport = function (u) { return import(u); };</script>
<script type="module">
import bridge from "/bench/browser.js";
window.__benchBridge = bridge;
window.__benchReady = true;
</script>
</body>
</html>