2026-11-06 16:20:42 +01:00

349 lines
19 KiB
Clojure

;; 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.node.pilot
"The diagnostic pilot executes one unscored case in headless Chromium.
An unscored attempt checks integration without a performance conclusion.
Stdout contains one raw RunRecord schema3 in Transit. The record
stores the saved plan, provenance, preparation, attempt and remaining slots.
THIS IS A SMOKE TEST AND THROWAWAY CODE. DO NOT USE THIS DESIGN AS GUIDANCE.
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 the case completes; 1 on case failures, harness errors and
timeouts. Failures retain raw records. A timeout closes the browser, including
a synchronously blocked page.
Usage, from `frontend/` with Playwright's Chromium installed:
Follow README.md's prepared renderer release commands first.
clojure -M:dev:renderer-bench release bench-render-wasm-browser
pnpm run build:renderer-benchmarks:pilot
node target/performance/render-wasm/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
["child_process" :as child-process]
["fs" :as fs]
["http" :as http]
["os" :as os]
["path" :as path]
["playwright" :as playwright]
["playwright/package.json" :as playwright-package]
[app.common.transit :as t]
[benches.render-wasm.cases :as cases]
[benches.render-wasm.failures :as failures]
[benches.render-wasm.measurement :as measurement]
[benches.render-wasm.random :as random]
[benches.render-wasm.report.format :as format]
[benches.render-wasm.report.summarize :as summary]
[benches.render-wasm.result :as result]
[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/performance/render-wasm/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!
"Serves an existing artifact with its MIME type, or returns a missing status."
[^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
"Returns the diagnostic pilot's command syntax."
[]
"pilot [--case SCENE/NAME] [--seed N] [--timeout-ms N] [--screenshot PATH]")
(defn- parse-args
"Resolves one case, seed, deadline and optional screenshot path."
[argv]
(let [opts (loop [args argv
opts {:seed default-seed :timeout-ms default-timeout-ms
:case-id :rects/load}]
(if (empty? args)
opts
(let [[flag value & rest-args] args]
(cond
(= flag "--seed") (recur rest-args (assoc opts :seed (js/parseInt value 10)))
(= flag "--case") (recur rest-args (assoc opts :case-id (when value (keyword value))))
(= 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-timeout? (fn [n] (and (integer? n) (pos? n)))]
(if (and (nil? (::invalid opts))
(random/seed? (:seed opts)) (valid-timeout? (:timeout-ms opts))
(some #{(:case-id opts)} (cases/case-ids)))
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 (transit)"
[message]
(.error js/console message))
(defn- print-result!
"Prints the raw schema3 record (transit)"
[record]
(println (result/encode record)))
(defn- git-provenance
"Reads the revision and dirty flag without saving status text or diffs."
[root]
(try
(let [opts #js {:cwd root :encoding "utf8"}]
{:sha (str/trim (child-process/execFileSync "git" #js ["rev-parse" "HEAD"] opts))
:dirty (not (str/blank? (child-process/execFileSync "git" #js ["status" "--porcelain"] opts)))})
(catch :default _ {:sha nil :dirty nil})))
(defn- pilot-metadata
"Captures available environment and declared build facts for an unscored pilot.
Prepared artifacts lack ticket10's stamp, so their functional identity stays
unverified. Environment capture uses a fixed allowlist."
[root opts]
(let [env (into {} (keep (fn [k]
(when-some [v (unchecked-get (.-env js/process) k)] [k v])))
["BUILD_MODE" "NODE_ENV" "PENPOT_WASM_PREPARED" "PENPOT_WASM_FUNCTION_NAMES"
"CARGO_BUILD_TARGET" "CARGO_NET_OFFLINE" "COREPACK_ENABLE_NETWORK"])
cpus (array-seq (os/cpus))]
{:git (git-provenance root)
:build {:functional {:prepared-artifact-settings :unverified :cljs-profile-marks false}
:provenance {:artifact-source :prepared-public-assets :declared-env env}}
:environment {:node (.-version js/process) :playwright (.-version playwright-package)
:browser nil :platform (os/platform) :release (os/release) :arch (os/arch)
:cpu {:models (vec (distinct (map #(.-model %) cpus))) :logical-count (count cpus)}
:launch {:headless true :args ["--enable-gpu"]}}
:scored? false
:diagnostics (select-keys opts [:seed :screenshot])}))
(defn- complete-record
"Adds available preparation and attempt evidence, then records termination.
Warm setup errors leave the slot unattempted. Fresh setup belongs to the
attempted cold load and consumes its slot even when setup fails."
[run preparation evidence attempted?]
(let [id (get-in run [:plan :cases 0 :id])
run (cond-> run preparation (result/record-preparation preparation))
run (if attempted?
(result/record-attempt run (measurement/attempt id (:preparation-id preparation) false evidence))
run)
ok? (= "ok" (:status evidence))]
(result/finish-run run (.toISOString (js/Date.))
(cond-> {:reason (if ok? :completed (if attempted? :failed :preparation-failed))}
(not ok?) (assoc :failure (select-keys evidence [:status :phase :message :cause])
:partial (:partial evidence))))))
(defn- evaluate-bridge
"Calls a fixed bridge method with one Transit string and returns its Promise.
JSON quoting embeds the string in JavaScript; records use Transit only."
[^js page method text]
(.evaluate page (str "window.__benchBridge." method "(" (.stringify js/JSON text) ")")))
(defn -main
"Runs one unscored attempt and verifies Node/browser summaries of its raw record."
[& argv]
(trace "pilot: parsing args")
(let [opts (parse-args (or argv []))
root (frontend-root)
bundle (path/join root "target" "performance" "render-wasm" "browser")
js-dir (path/join root "resources" "public" "js")
html (path/join root "performance" "bench" "render_wasm" "browser" "pilot.html")
seed (:seed opts)
selected (some #(when (= (:id %) (:case-id opts)) %)
(cases/collect-cases {:master-seed seed}))
deadline (:timeout-ms opts)
started-at (.toISOString (js/Date.))
initial-run (result/create-run {:run-id (str "pilot-" started-at) :started-at started-at
:plan {:cases [selected] :warmups 0 :repetitions 1
:timeout-policy {:attempt-ms deadline :pilot-total-ms deadline}}
:metadata (pilot-metadata root opts)})
preparation-ref (atom nil)
attempted-ref (atom false)
run-ref (atom initial-run)]
(-> (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)))
wait-for-bench-ready
(fn [^js page] (fn [] (.waitForFunction page "window.__benchReady === true")))
evaluate-load
(fn [^js page] (fn [_]
(trace "pilot: bridge ready, evaluating")
(when (= :fresh (:context selected))
(reset! attempted-ref true))
(let [request (t/encode-str {:case selected :timeout-ms deadline
:module-url "/js/render-wasm.js"
:wasm-url "/js/render-wasm.wasm"})]
(-> (evaluate-bridge page "prepareScene" request)
(.then (fn [text]
(let [prepared (t/decode-str text)]
(if (= "ok" (:status prepared))
(do
(reset! preparation-ref (:preparation prepared))
(reset! attempted-ref true)
(evaluate-bridge page "runPrepared"
(t/encode-str {:preparation-id (get-in prepared [:preparation :preparation-id])
:timeout-ms deadline})))
text))))))))
attach-screenshot-path
(fn [^js page version] (fn [text]
(let [out (complete-record
(assoc-in @run-ref [:metadata :environment :browser] version)
@preparation-ref (t/decode-str text) @attempted-ref)
report (summary/summarize out)
check-report (fn [_]
(-> (evaluate-bridge page "summarize" (result/encode out))
(.then (fn [browser-summary]
(when-not (= report (t/decode-str browser-summary))
(throw (ex-info "Node/browser summaries differ" {})))
(trace "pilot: Node/browser summaries agree")
(trace (format/format-run report))
out))))]
(reset! run-ref out)
(-> (if-let [shot (:screenshot opts)]
(.screenshot page #js {"path" shot})
(js/Promise.resolve nil))
(.then check-report)))))
viewport #js {"viewport"
#js {"width" 1920 "height" 1080}
"deviceScaleFactor" 2}
;; 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)
chromium-launch (-> (.-chromium playwright)
(.launch #js {"args" #js ["--enable-gpu"]}))
work
(-> chromium-launch
(.then
(fn [^js pw-browser]
(reset! browser-ref pw-browser)
(let [version (.version pw-browser)]
(swap! run-ref assoc-in [:metadata :environment :browser] version)
(-> (.newContext pw-browser viewport)
(.then
(fn [^js context]
(-> (.newPage context)
(.then
(fn [^js page]
(trace "pilot: page open")
(-> (.goto page url)
(.then (wait-for-bench-ready page))
(.then (evaluate-load page))
(.then (attach-screenshot-path page version))
(.then (fn [out]
(-> (.evaluate page "window.__benchBridge.dispose()")
(.then (fn [_] (.close page)))
(.then (fn [_] (.close context)))
(.then (fn [_] (.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
(if (= :completed (get-in out [:termination :reason])) 0 1)))))))
(.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 [_]
(if (= :running (get-in @run-ref [:termination :reason]))
(print-result! (complete-record @run-ref @preparation-ref
{:status "failed" :phase "pilot-harness"
:message (or (ex-message cause) (str cause))
:cause (failures/describe-cause cause)}
@attempted-ref))
(print-result! (assoc @run-ref :termination
{:reason :diagnostic-failed :cause (failures/describe-cause cause)})))
(js/process.exit 1))))))))))
(.catch (fn [cause]
(print-result! (complete-record initial-run nil
{:status "failed" :phase "pilot-startup"
:message (or (ex-message cause) (str cause))
:cause (failures/describe-cause cause)} false))
(js/process.exit 1))))))