From 9e5620600f0b6413bf1899e1b04b20ecb214a3fe Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Sun, 27 Sep 2026 11:24:07 +0200 Subject: [PATCH] :construction: 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 --- frontend/package.json | 1 + frontend/shadow-cljs.edn | 23 +- frontend/test/benches/render_wasm/pilot.cljs | 242 +++++++++++++++++++ frontend/test/benches/render_wasm/pilot.html | 17 ++ 4 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 frontend/test/benches/render_wasm/pilot.cljs create mode 100644 frontend/test/benches/render_wasm/pilot.html diff --git a/frontend/package.json b/frontend/package.json index 9ecc807111..6e7ba80f58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 45b69d8f2f..4215d3b7a3 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -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 diff --git a/frontend/test/benches/render_wasm/pilot.cljs b/frontend/test/benches/render_wasm/pilot.cljs new file mode 100644 index 0000000000..8b95669429 --- /dev/null +++ b/frontend/test/benches/render_wasm/pilot.cljs @@ -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)))))) diff --git a/frontend/test/benches/render_wasm/pilot.html b/frontend/test/benches/render_wasm/pilot.html new file mode 100644 index 0000000000..87acda4cd0 --- /dev/null +++ b/frontend/test/benches/render_wasm/pilot.html @@ -0,0 +1,17 @@ + + + + + renderer benchmark pilot + + + + + + +