🚧 WIP 28, enable GPU support

This commit is contained in:
Miguel de Benito Delgado 2026-10-29 22:57:42 +01:00
parent 13af3fb323
commit ca6dbbe658
13 changed files with 377 additions and 113 deletions

View File

@ -21,6 +21,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-commands bench-render-wasm-runner",
"render-bench": "sh -c 'if [ \"${PENPOT_GPU:-0}\" = 1 ]; then exec xvfb-run -a node target/performance/render-wasm/runner.cjs \"$@\"; else exec node target/performance/render-wasm/runner.cjs \"$@\"; fi'",
"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

@ -141,17 +141,20 @@
:factory-ms (- (now) t0)})))))))))
@module-promise*)
(defn- renderer-string
"Reads the unmasked renderer (SwiftShader/llvmpipe detection for the
software-renderer warning). The init helper forces the debug extension."
[context]
(defn- graphics-string
[context parameter]
(try
(let [ext (.getExtension ^js context "WEBGL_debug_renderer_info")]
(if (some? ext)
(str (.getParameter ^js context (unchecked-get ext "UNMASKED_RENDERER_WEBGL")))
(str (.getParameter ^js context (unchecked-get ext parameter)))
"unknown"))
(catch :default _ "unknown")))
(defn- renderer-string
"Reads the unmasked WebGL renderer identity."
[context]
(graphics-string context "UNMASKED_RENDERER_WEBGL"))
(defn- software-renderer?
"Recognizes common software graphics implementations from their renderer text."
[s]
@ -216,11 +219,7 @@
attr (fn [k]
(when (some? attrs)
(unchecked-get attrs k)))
vendor (try
(if-let [ext (.getExtension ^js ctx "WEBGL_debug_renderer_info")]
(str (.getParameter ^js ctx (unchecked-get ext "UNMASKED_VENDOR_WEBGL")))
"unknown")
(catch :default _ "unknown"))]
vendor (graphics-string ctx "UNMASKED_VENDOR_WEBGL")]
{:css-width width
:css-height height
:drawing-buffer-width (.-drawingBufferWidth ^js ctx)

View File

@ -8,32 +8,40 @@
"Owns Chromium through process/run from spawn, then attaches Playwright by CDP.
Shutdown stops the owned process group before disconnecting the client."
(:require
["fs" :as fs]
["os" :as os]
["path" :as path]
["playwright" :as playwright]
[app.common.transit :as transit]
[benches.render-wasm.failures :as failures]
[benches.render-wasm.identity :as identity]
[benches.render-wasm.node.options :as options]
[benches.render-wasm.node.process :as process]))
["fs" :as fs]
["os" :as os]
["path" :as path]
["playwright" :as playwright]
[app.common.transit :as transit]
[benches.render-wasm.failures :as failures]
[benches.render-wasm.identity :as identity]
[benches.render-wasm.node.options :as options]
[benches.render-wasm.node.process :as process]))
(def launch-settings
"Static settings for the full Chromium executable; profile and port are owned."
(defn gpu-active?
"Returns whether the development container enables GPU access."
[]
(= "1" (.-PENPOT_GPU (.-env js/process))))
(defn launch-settings
"Returns the resolved Chromium settings for this environment."
[]
{:headless true
:transport :cdp
:args ["--headless"
"--no-sandbox"
"--disable-dev-shm-usage"
"--enable-gpu"
"--enable-unsafe-swiftshader"
"--no-first-run"
"--no-default-browser-check"
"--disable-background-networking"
"--disable-background-timer-throttling"
"--disable-renderer-backgrounding"
"--hide-scrollbars"
"--mute-audio"]})
:args (cond-> ["--headless"
"--no-sandbox"
"--disable-dev-shm-usage"
"--enable-unsafe-swiftshader"
"--no-first-run"
"--no-default-browser-check"
"--disable-background-networking"
"--disable-background-timer-throttling"
"--disable-renderer-backgrounding"
"--hide-scrollbars"
"--mute-audio"]
(gpu-active?) (into ["--enable-gpu"
"--use-gl=angle"
"--use-angle=vulkan"]))})
(defn within
"Calls work while its absolute deadline and optional AbortSignal allow it.
@ -225,7 +233,7 @@
controller (js/AbortController.)
endpoint (endpoint-waiter)
running (process/run {:command command
:args (into (:args launch-settings)
:args (into (:args (launch-settings))
["--remote-debugging-port=0"
"--remote-debugging-address=127.0.0.1"
(str "--user-data-dir=" user-data-dir)

View File

@ -9,8 +9,9 @@
The caller owns exclusive preparation for this checkout. The renderer's
lock protects Cargo and copying; CLJS compilation follows after it releases.
Preparation uses installed dependencies and offline Cargo. Every attempt
retains its manifest and compiler log, including failed attempts."
Preparation uses installed dependencies. Cargo may fetch missing crates and
the custom Skia binaries on a cold build. Every attempt retains its manifest
and compiler log, including failed attempts."
(:require
["fs" :as fs]
["path" :as path]
@ -89,7 +90,7 @@
(log/info "Renderer compiling")
(let [build @build-ref
facts-path (path/join (:renderer-dir build) "cargo-build.json")
args (cond-> ["frontend" "--offline"]
args (cond-> ["frontend"]
(not (:default-features? selection)) (conj "--no-default-features")
(seq (:features selection)) (into ["--features" (str/join "," (:features selection))]))]
(await (process/run (merge bounds
@ -97,7 +98,7 @@
:args args :cwd (path/resolve root ".." "render-wasm")
:capture-output? false :on-line (fn [line _] (log/debug line))
:env {"PENPOT_WASM_PREPARED" "1" "NODE_ENV" "production"
"BUILD_MODE" "release" "CARGO_NET_OFFLINE" "true"
"BUILD_MODE" "release"
"VERSION_TAG" (:id build)
"FRONTEND_DEST" (:renderer-dir build)
"FRONTEND_SHARED_DEST" (path/join root "src" "app" "render_wasm" "api" "shared.js")

View File

@ -87,6 +87,12 @@
(let [outcome
(try
(let [{:keys [command options]} (transit/decode-str request)
_ (when (and (:require-gpu options)
(or (= command "run")
(and (= command "ab") (not (:dry-run options))))
(not= "1" (.-PENPOT_GPU (.-env js/process))))
(throw (ex-info "--require-gpu needs PENPOT_GPU=1. Start the container with ./manage.sh run-devenv --gpu and use pnpm run render-bench."
{:type ::gpu-required})))
root (artifacts/frontend-root)]
(log/configure {:level (options/console-level options)
:color (options/color-mode options)})

View File

@ -439,6 +439,8 @@
exit-code (:exit-code run-result)
termination (get-in run-record [:termination :reason])
attempts (:attempts run-record)
gpu-failure (some #(when (= :gpu (:stage %)) %)
(get-in run-record [:metadata :diagnostics :failures]))
evidence-error
(or (:storage-error run-result)
(try
@ -460,6 +462,9 @@
(some? evidence-error)
(failure-evidence :run-evidence evidence-error)
(some? gpu-failure)
gpu-failure
(map? (get-in run-record [:termination :failure]))
(get-in run-record [:termination :failure])
@ -514,6 +519,14 @@
:reason (str "not attempted after run evidence failure in configuration " slug)
:error run-error}
(some? gpu-failure)
{:stop? true
:reason (str "not attempted after GPU failure in configuration " slug)
:error (ex-info (:message gpu-failure)
{:type ::gpu-required
:log-path (:log-path run-home)
:configuration slug})}
:else
(if (empty? attempts)
(let [cause (ex-info "configuration run made no attempts"

View File

@ -31,7 +31,8 @@
[benches.render-wasm.node.server :as server]
[benches.render-wasm.report.format :as format]
[benches.render-wasm.report.summarize :as summary]
[benches.render-wasm.result :as result]))
[benches.render-wasm.result :as result]
[clojure.string :as str]))
(defn- now
[]
@ -252,6 +253,31 @@
(write-log state log/debug (str "Browser " (:version owner)))
owner)))
(defn- usable-graphics-identity?
[value]
(and (string? value)
(not (contains? #{"" "unknown" "unavailable" "null" "undefined"}
(str/lower-case (str/trim value))))))
(defn- check-gpu
[state preparation]
(let [graphics (:effective-graphics preparation)
accepted (and (true? (:webgl2 graphics))
(false? (:software graphics))
(usable-graphics-identity? (:vendor graphics))
(usable-graphics-identity? (:renderer graphics)))]
(if (and (:require-gpu? (:opts state)) (not accepted))
(do
(record-failure state (:case-id preparation) :gpu
{:status "failed" :phase "gpu"
:message (str "GPU check failed; renderer: " (pr-str (:renderer graphics))
". Hardware WebGL2 with a usable vendor and renderer is required."
" Start the container with ./manage.sh run-devenv --gpu and use pnpm run render-bench.")
:partial {:graphics graphics}})
(reset! (:abort-run? state) true)
:abort)
:ok)))
(defn- ^:async open-page
[state case]
(if (interrupted? state)
@ -304,28 +330,30 @@
(let [preparation (:preparation response)
prep-id (:preparation-id preparation)]
(record-preparation state preparation)
(let [attempted (try
{:evidence (await (run-attempt state page prep-id deadline))}
(catch :default cause {:error cause}))]
(if-let [cause (:error attempted)]
(let [evidence (cause-evidence cause :attempt)]
(record-attempt state
(measurement/attempt case-id prep-id warmup? evidence))
(when final?
(record-screenshot-skip state case-id :no-completed-measured-attempt))
(if (interrupted? state) :interrupted :failed))
(let [attempt (measurement/attempt case-id prep-id warmup?
(:evidence attempted))]
(record-attempt state attempt)
(if (= :completed (:outcome attempt))
(do
(when final? (await (screenshot-case state case page)))
:ok)
(do
(when final?
(record-screenshot-skip state case-id
:no-completed-measured-attempt))
:failed))))))
(if (= :abort (check-gpu state preparation))
:abort
(let [attempted (try
{:evidence (await (run-attempt state page prep-id deadline))}
(catch :default cause {:error cause}))]
(if-let [cause (:error attempted)]
(let [evidence (cause-evidence cause :attempt)]
(record-attempt state
(measurement/attempt case-id prep-id warmup? evidence))
(when final?
(record-screenshot-skip state case-id :no-completed-measured-attempt))
(if (interrupted? state) :interrupted :failed))
(let [attempt (measurement/attempt case-id prep-id warmup?
(:evidence attempted))]
(record-attempt state attempt)
(if (= :completed (:outcome attempt))
(do
(when final? (await (screenshot-case state case page)))
:ok)
(do
(when final?
(record-screenshot-skip state case-id
:no-completed-measured-attempt))
:failed)))))))
(let [evidence (failure-evidence response :preparation)]
(record-attempt state (measurement/attempt case-id nil warmup? evidence))
(when final? (record-screenshot-skip state case-id :no-completed-measured-attempt))
@ -388,6 +416,11 @@
(ensure-screenshot-status state case :case-abandoned)
:interrupted)
(= :abort outcome)
(do
(ensure-screenshot-status state case :case-abandoned)
:abort)
(not closed?)
(do
(ensure-screenshot-status state case :case-abandoned)
@ -433,24 +466,26 @@
total (count slots)]
(record-preparation state preparation)
(let [outcome
(loop [index 0]
(if (>= index total)
:done
(let [warmup? (nth slots index)
result (await (run-reuse-slot state case page prep-id warmup?))]
(cond
(= :ok result)
(if (= index (dec total))
(do (await (screenshot-case state case page)) :done)
(recur (inc index)))
(if (= :abort (check-gpu state preparation))
:abort
(loop [index 0]
(if (>= index total)
:done
(let [warmup? (nth slots index)
result (await (run-reuse-slot state case page prep-id warmup?))]
(cond
(= :ok result)
(if (= index (dec total))
(do (await (screenshot-case state case page)) :done)
(recur (inc index)))
(= index (dec total))
(do
(record-screenshot-skip state (:id case)
:no-completed-measured-attempt)
result)
(= index (dec total))
(do
(record-screenshot-skip state (:id case)
:no-completed-measured-attempt)
result)
:else result))))
:else result)))))
closed? (await (close-current-page state))]
(ensure-screenshot-status state case :case-abandoned)
(cond
@ -458,6 +493,7 @@
(= outcome :interrupted) :interrupted
:else outcome))))))))
(defn- ^:async launch-initial
[state]
(let [launched (try
@ -625,7 +661,7 @@
:arch (os/arch)
:cpu {:models (vec (distinct (map #(.-model %) cpus)))
:logical-count (count cpus)}
:launch browser/launch-settings}))
:launch (browser/launch-settings)}))
(defn- metadata
[root selected-build opts]
@ -645,6 +681,8 @@
:build-timeout-ms (:build-timeout-ms opts)
:startup-timeout-ms (:startup-timeout-ms opts)
:screenshots? (boolean (:screenshots? opts))
:require-gpu? (boolean (:require-gpu? opts))
:penpot-gpu (or (.-PENPOT_GPU (.-env js/process)) nil)
:runner-git {:sha nil :dirty nil}
:runtime-helper-identity :unverified
:screenshots {}
@ -656,8 +694,10 @@
(assoc :screenshots? (boolean (or (:screenshots? options)
(:screenshots options)))
:remove? (boolean (or (:remove? options)
(:remove options))))
(dissoc :screenshots :remove)))
(:remove options)))
:require-gpu? (boolean (or (:require-gpu? options)
(:require-gpu options))))
(dissoc :screenshots :remove :require-gpu)))
(defn ^:async execute
"Executes a prepared selection in an allocated home and owns host and browser.

View File

@ -79,6 +79,7 @@
:validate nonempty]
[nil "--remove" "Remove the used build after shutdown"]
[nil "--screenshots" "Capture one screenshot per case"]
[nil "--require-gpu" "Fail when GPU rendering is unavailable"]
[nil "--only-builds" "Purge build attempts"]
[nil "--only-runs" "Purge managed runs"]
[nil "--only-comparisons" "Purge managed feature comparisons"]
@ -104,6 +105,9 @@
"-v/--verbose includes raw build output. --[no-]color overrides terminal\n"
"color detection. File logs receive all levels without logger colors.\n"
"Builds and managed runs live under target/performance/render-wasm.\n"
"run and ab accept --require-gpu. Start the container with\n"
"./manage.sh run-devenv --gpu and use pnpm run render-bench -- run ...\n"
"to require hardware WebGL2 in preparation metadata. ab --dry-run skips it.\n"
"compare reads saved schema:run records and starts no build, host or browser.\n"
"Build and scene permissions combine; operation and runtime checks stay strict.\n"
"All-differences permits every compatibility difference. Reports retain them.\n\n"
@ -165,7 +169,7 @@
(def ^:private run-flags
#{"--build" "--filter" "--seed" "--warmups" "--repetitions" "--timeout-ms"
"--startup-timeout-ms" "--output-dir" "--remove" "--screenshots"})
"--startup-timeout-ms" "--output-dir" "--remove" "--screenshots" "--require-gpu"})
(def ^:private purge-flags
#{"--only-builds" "--only-runs" "--only-comparisons"})

View File

@ -26,6 +26,23 @@
{:before #(reset! temporary-root (fs/mkdtempSync (path/join (os/tmpdir) "penpot-browser-test-")))
:after #(fs/rmSync @temporary-root #js {:recursive true :force true})})
(t/deftest gpu-container-selects-the-vulkan-launch-backend
(let [env (.-env js/process)
previous (.-PENPOT_GPU env)]
(try
(doseq [value [nil "0" "true" "1"]]
(if (nil? value)
(js-delete env "PENPOT_GPU")
(set! (.-PENPOT_GPU env) value))
(let [args (set (:args (browser/launch-settings)))]
(t/is (= (= value "1") (contains? args "--use-angle=vulkan")))
(t/is (= (= value "1") (contains? args "--use-gl=angle")))
(t/is (contains? args "--enable-unsafe-swiftshader"))))
(finally
(if (undefined? previous)
(js-delete env "PENPOT_GPU")
(set! (.-PENPOT_GPU env) previous))))))
(defn- ^:async open-host
[actual-id ready?]
(let [identity-text (.stringify js/JSON (transit/encode-str {:build-id actual-id}))]

View File

@ -812,6 +812,7 @@ test(
testOptions,
async (t) => {
const fixture = createFrontend(t);
fixture.env.PENPOT_GPU = "0";
installCargoMetadata(fixture, { default: [], a: [], b: ["a"] });
const retained = tree(fixture.managed);
@ -822,6 +823,7 @@ test(
"--filter",
"rects/load",
"--dry-run",
"--require-gpu",
]);
assert.equal(result.exitCode, 0);
@ -1110,6 +1112,34 @@ test(
},
);
test(
"required GPU access rejects run and ab before allocating output or checking builds",
testOptions,
async (t) => {
for (const command of [
["run"],
["ab", "--features", "a", "--filter", "rects/load"],
]) {
const fixture = createFrontend(t);
fixture.env.PENPOT_GPU = "";
const original = tree(fixture.managed);
const result = await runCli(fixture, [
...command,
"--require-gpu",
"--output-dir",
path.join(fixture.managed, "gpu-run"),
]);
assert.equal(result.exitCode, 1);
assert.equal(result.stdout, "");
assert.match(result.stderr, /PENPOT_GPU=1/);
assert.match(result.stderr, /\.\/manage\.sh run-devenv --gpu/);
assert.match(result.stderr, /pnpm run render-bench/);
assert.deepEqual(tree(fixture.managed), original);
}
},
);
test(
"run validates an explicit output dir before preparing a build",
testOptions,

View File

@ -220,26 +220,33 @@
recorded in the shared call log. The context reports a drawing-buffer size
distinct from any canvas size, so tests prove `read-graphics` reads it from
the context rather than the canvas attributes."
[id]
(let [lose-ext #js {:loseContext (fn [] (record! ["lose" id]) nil)}
context #js {:getExtension (fn [name]
(when (= name "WEBGL_lose_context")
lose-ext))
:getContextAttributes (fn []
#js {"antialias" false
"depth" true
"stencil" false
"alpha" false
"preserveDrawingBuffer" false})
:drawingBufferWidth 1234
:drawingBufferHeight 5678}]
#js {:id id
:width 0
:height 0
:style #js {}
:getContext (fn [_ _] context)
:addEventListener (fn [_ _] nil)
:remove (fn [] (record! ["remove" id]) nil)}))
([id]
(fake-canvas id nil))
([id {:keys [vendor renderer]}]
(let [debug-ext (when (and vendor renderer)
#js {:UNMASKED_VENDOR_WEBGL 1 :UNMASKED_RENDERER_WEBGL 2})
lose-ext #js {:loseContext (fn [] (record! ["lose" id]) nil)}
context #js {:getExtension (fn [name]
(case name
"WEBGL_lose_context" lose-ext
"WEBGL_debug_renderer_info" debug-ext
nil))
:getParameter (fn [parameter] (case parameter 1 vendor 2 renderer nil))
:getContextAttributes (fn []
#js {"antialias" false
"depth" true
"stencil" false
"alpha" false
"preserveDrawingBuffer" false})
:drawingBufferWidth 1234
:drawingBufferHeight 5678}]
#js {:id id
:width 0
:height 0
:style #js {}
:getContext (fn [_ _] context)
:addEventListener (fn [_ _] nil)
:remove (fn [] (record! ["remove" id]) nil)})))
(defn- install-fake-dom!
"Installs fake DOM, import, clock and scenario globals and returns a
@ -423,6 +430,25 @@
(cleanup)
(throw cause)))))
(t/deftest ^:async preparations-record-the-context-identity-and-software-status
(doseq [[identity software] [[{:vendor "NVIDIA" :renderer "ANGLE (NVIDIA, Vulkan)"} false]
[{:vendor "Google" :renderer "ANGLE (SwiftShader)"} true]
[{:vendor "Mesa" :renderer "llvmpipe (LLVM)"} true]
[nil false]]]
(await
(with-entry-env
(^:async fn [cleanup]
(try
(set! (.-createElement js/document) (fn [_] (fake-canvas 1 identity)))
(let [response (transit/decode-str (await (browser/prepare-scene-request (scene-args 7 {}))))
graphics (get-in response [:preparation :effective-graphics])]
(t/is (= "ok" (:status response)))
(t/is (= (or (:vendor identity) "unknown") (:vendor graphics)))
(t/is (= (or (:renderer identity) "unknown") (:renderer graphics)))
(t/is (= software (:software graphics))))
(finally
(cleanup))))))))
(t/deftest successful-load-reaches-full
(t/async done
(with-entry-env

View File

@ -79,10 +79,14 @@
(throw (ex-info "browser service requires the page object"
{:phase :browser-page-startup}))))
(def ^:private hardware-graphics
{:webgl2 true :vendor "NVIDIA" :renderer "ANGLE (NVIDIA, Vulkan, NVIDIA)" :software false})
(defn- fake-browser
[{:keys [launch-failure new-page-failure close-page-failure
prepare-failure run-failure screenshot-failure
interruption controller]}]
interruption controller effective-graphics]
:or {effective-graphics (constantly {})}}]
(let [state (atom {:launches 0 :pages 0 :close-pages 0 :closes 0
:host nil :runs {} :preparations {}})]
{:state state
@ -135,7 +139,7 @@
:case-id case-id
:fingerprint {:algorithm :sha-256
:digest (apply str (repeat 64 "a"))}
:effective-graphics {}
:effective-graphics (effective-graphics case-id (:id page))
:diagnostics {}}]
(swap! state assoc-in [:preparations preparation-id] case-id)
{:status "ok" :preparation preparation}))))
@ -384,6 +388,68 @@
(t/is (= :browser-launch (:stage (last (get-in run [:metadata :diagnostics :failures])))))
(t/is (= 1 (:exit-code result)))))
(t/deftest ^:async hardware-preparations-complete-fresh-and-reuse-cases
(let [fake (fake-browser {:effective-graphics (constantly hardware-graphics)})
opts (assoc (options fake (output-dir)) :require-gpu true :warmups 1 :repetitions 2)
result (await (execute fake [(collected :case/fresh :fresh)
(collected :case/reuse :reuse)] opts))
run (saved-record result)]
(t/is (= 0 (:exit-code result)))
(t/is (= :completed (get-in run [:termination :reason])))
(t/is (= 6 (count (:attempts run))))
(t/is (every? #(= hardware-graphics (:effective-graphics %)) (:preparations run)))
(t/is (true? (get-in run [:metadata :diagnostics :require-gpu?])))))
(t/deftest ^:async required-gpu-failures-preserve-evidence-and-all-attempt-slots
(doseq [context [:fresh :reuse]
graphics [(assoc hardware-graphics :renderer "ANGLE (SwiftShader)" :software true)
(assoc hardware-graphics :webgl2 false)
(assoc hardware-graphics :renderer nil)
(assoc hardware-graphics :renderer " UnKnOwN ")
(assoc hardware-graphics :vendor "")
{}]]
(let [fake (fake-browser {:effective-graphics (constantly graphics)})
opts (assoc (options fake (output-dir)) :require-gpu true :warmups 1)
result (await (execute fake [(collected :case/a context)
(collected :case/b :reuse)] opts))
run (saved-record result)
failure (get-in run [:termination :failure])
state @(:state fake)]
(t/is (= 1 (:exit-code result)))
(t/is (= :incomplete (get-in run [:termination :reason])))
(t/is (= [graphics] (mapv :effective-graphics (:preparations run))))
(t/is (= [] (:attempts run)))
(t/is (= {:case/a {:warmups 1 :repetitions 1}
:case/b {:warmups 1 :repetitions 1}} (:remaining run)))
(t/is (= :gpu (:stage failure)))
(t/is (= graphics (get-in failure [:partial :graphics])))
(t/is (str/includes? (:message failure) (pr-str (:renderer graphics))))
(t/is (str/includes? (:message failure) "./manage.sh run-devenv --gpu"))
(t/is (str/includes? (:message failure) "pnpm run render-bench"))
(t/is (= 1 (:close-pages state)))
(t/is (= 1 (:closes state))))))
(t/deftest ^:async graphics-rejection-preserves-earlier-attempts-and-stops-the-run
(doseq [context [:fresh :reuse]]
(let [software (assoc hardware-graphics :renderer "SwiftShader" :software true)
fake (fake-browser {:effective-graphics (fn [_ page-id]
(if (= page-id 1) hardware-graphics software))})
opts (assoc (options fake (output-dir)) :require-gpu true :repetitions 2)
result (await (execute fake [(collected :case/a context)
(collected :case/b context)] opts))
run (saved-record result)
failure (get-in run [:termination :failure])]
(t/is (= 1 (:exit-code result)))
(t/is (= :gpu (:stage failure)))
(t/is (= software (get-in failure [:partial :graphics])))
(t/is (= (if (= context :fresh) 1 2) (count (:attempts run))))
(t/is (every? #(= :completed (:outcome %)) (:attempts run)))
(t/is (= [hardware-graphics software] (mapv :effective-graphics (:preparations run))))
(t/is (= {:case/a {:warmups 0 :repetitions (if (= context :fresh) 1 0)}
:case/b {:warmups 0 :repetitions 2}} (:remaining run)))
(t/is (= 2 (:close-pages @(:state fake))))
(t/is (= 1 (:closes @(:state fake)))))))
(t/deftest ^:async sigint-and-sigterm-persist-partial-interrupted-records
(doseq [[signal code] [["SIGINT" 130] ["SIGTERM" 143]]]
(let [controller (js/AbortController.)
@ -573,6 +639,57 @@
(t/is (= "<!doctype html>"
(fs/readFileSync (path/join dir "index.html") "utf8")))))
(defn- prepare-comparison-build
[{:keys [selection on-created]}]
(let [dir (build-dir)
build (-> (selected-build dir (str (random-uuid)))
(assoc :manifest-path (path/join dir "build.transit"))
(assoc-in [:manifest :functional :default-features?] false)
(assoc-in [:manifest :functional :features] (:features selection))
(assoc-in [:manifest :provenance :feature-selection] selection))]
(on-created build)
(js/Promise.resolve build)))
(t/deftest ^:async gpu-rejection-stops-later-matrix-configurations
(doseq [[hardware-first close-fails] [[false false] [true false] [true true]]]
(let [root @temporary-root
out (output-dir)
cases [(collected :case/a :fresh)]
software (assoc hardware-graphics :renderer "SwiftShader" :software true)
prepared {:plan (feature-plan/plan ["a"] {:dry-run true})
:cases cases
:output-dir out
:options (assoc (dissoc (options nil out) :run-id :browser)
:require-gpu true :repetitions 2)}
response (await
(feature-comparison/execute
root prepared nil
{:services
{:prepare-build prepare-comparison-build
:collect-catalog (fn [_ _] cases)
:execute-run
(fn [root home selected cases opts signal]
(let [fake (fake-browser
{:close-page-failure (when close-fails #(= % 2))
:effective-graphics
(fn [_ page-id]
(if (and hardware-first (= page-id 1))
hardware-graphics
software))})]
(run/execute root home selected cases
(assoc opts :browser (:service fake)) signal)))}}))
saved (result/decode (fs/readFileSync (get-in response [:home :record-path]) "utf8"))
run (result/decode (fs/readFileSync (get-in saved [:configurations 0 :run :record-path]) "utf8"))
failure (some #(when (= :gpu (:stage %)) %)
(get-in run [:metadata :diagnostics :failures]))]
(t/is (= 1 (:exit-code response)))
(t/is (= (if hardware-first 1 0) (count (:attempts run))))
(t/is (= software (get-in failure [:partial :graphics])))
(t/is (= (if close-fails :browser-close :gpu) (get-in run [:termination :failure :stage])))
(t/is (= :failed (get-in saved [:configurations 0 :status])))
(t/is (= :gpu (get-in saved [:configurations 0 :failure :stage])))
(t/is (= :unattempted (get-in saved [:configurations 1 :status]))))))
(t/deftest ^:async comparison-stops-after-run-record-or-log-storage-fails
(doseq [filename ["run.transit" "run.log"]]
(let [executed (atom nil)
@ -587,16 +704,7 @@
(feature-comparison/execute
root prepared nil
{:services
{:prepare-build
(fn [{:keys [selection on-created]}]
(let [dir (build-dir)
build (-> (selected-build dir (str (random-uuid)))
(assoc :manifest-path (path/join dir "build.transit"))
(assoc-in [:manifest :functional :default-features?] false)
(assoc-in [:manifest :functional :features] (:features selection))
(assoc-in [:manifest :provenance :feature-selection] selection))]
(on-created build)
(js/Promise.resolve build)))
{:prepare-build prepare-comparison-build
:collect-catalog (fn [_ _] cases)
:execute-run
(^:async fn [root home selected cases opts signal]

View File

@ -218,6 +218,17 @@
["run" "--timeout-ms" " "]]]
(t/is (thrown? js/Error (runner/parse-args args)))))
(t/deftest gpu-policy-is-available-for-runs-and-feature-comparisons
(doseq [args [["run" "--require-gpu"]
["ab" "--features" "a" "--filter" "rects/load" "--require-gpu"]
["ab" "--features" "a" "--require-gpu" "--dry-run"]]]
(t/is (true? (get-in (runner/parse-args args) [:options :require-gpu]))))
(doseq [args [["build" "--require-gpu"]
["purge" "--require-gpu"]
["compare" "a" "b" "--require-gpu"]]]
(t/is (thrown-with-msg? js/Error #"run options require run" (runner/parse-args args))))
(t/is (re-find #"--require-gpu" (runner/usage ""))))
(t/deftest ab-accepts-run-budgets-and-comparison-permissions
(let [{:keys [command options]}
(runner/parse-args ["ab" "--features" " b,a,b " "--filter" "rects/load"