Miguel de Benito Delgado 69980c5100 ✨ defcommand macro and expose render_sync
AI-assisted-by: gpt-6.1-sol, deepseek-flash
2026-11-02 20:00:36 +01:00

197 lines
7.9 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 performance-tests.render-wasm.commands-test
(:require
[benches.render-wasm.builder :as builder]
[benches.render-wasm.errors :as errors]
[benches.render-wasm.runtime.camera :as camera]
[benches.render-wasm.runtime.commands :as cmd :refer [defcommand]]
[cljs.test :as t]))
(defcommand move
"Moves the camera by the supplied offset."
[rtx {:keys [dx dy]}]
(-> rtx
(update-in [:view :x] + dx)
(update-in [:view :y] + dy)))
(defcommand selected-renders
{:measured-variant false}
[rtx]
(-> rtx
(camera/render-sync*)
(move {:dx 10 :dy 5})
(camera/render-sync)
(camera/render-sync*)))
(defcommand unexpected-promise
[rtx cause]
(if cause
(js/Promise.reject cause)
(js/Promise.resolve rtx)))
(defcommand reject-offset
[rtx offset]
{:pre [(pos? offset)]}
((get-in rtx [:hooks :render-sync]))
(update-in rtx [:view :x] + offset))
(defcommand replace-owner
[rtx]
(assoc-in rtx [:hooks :check] (fn [])))
(defn- context
[clock]
{:case {:id :commands/example}
:scene (builder/scene {:seed 1})
:view {:scale 1 :x 0 :y 0}
:hooks {:now #(deref clock)
:check (fn [])
:render-sync #(vswap! clock + 7)}})
(t/deftest ^:async measurement-uses-core-time
(let [clock (volatile! 0)
rtx (assoc-in (context clock) [:hooks :render-sync]
#(do
(vswap! clock + 7)
(js/queueMicrotask (fn [] (vswap! clock + 500)))))
{:keys [outcome events context]}
(await
(cmd/execute
rtx
(fn [rtx]
(let [input (-> (js/Promise.resolve rtx)
(.then (fn [rtx]
(vswap! clock + 1000)
rtx)))]
(-> (camera/render-sync* input)
(.then (fn [rtx]
(vswap! clock + 2000)
rtx)))))))]
(t/is (= :completed outcome))
(t/is (= (:view rtx) (:view context)))
(t/is (= [{:command ::camera/render-sync
:metrics {:command/elapsed [7]}}]
events))
(t/is (= 3507 @clock))))
(t/deftest ^:async composite-preserves-view-and-selected-observations
(doseq [promised [false true]]
(let [clock (volatile! 0)
cost (volatile! 0)
rtx (assoc-in (context clock) [:hooks :render-sync]
#(vswap! clock + (vswap! cost + 7)))
{:keys [outcome context events]}
(await (cmd/execute rtx #(selected-renders
(if promised (js/Promise.resolve %) %))))]
(t/is (= :completed outcome))
(t/is (= {:scale 1 :x 10 :y 5} (:view context)))
(t/is (= [{:command ::camera/render-sync
:metrics {:command/elapsed [7]}}
{:command ::camera/render-sync
:metrics {:command/elapsed [21]}}]
events))
(t/is (= 42 @clock)))))
(t/deftest measured-command-documentation
(t/is (.startsWith (:doc (meta #'camera/render-sync*))
"Measured variant of `render-sync`."))
(t/is (= "Measured variant of `unexpected-promise`."
(:doc (meta #'unexpected-promise*)))))
(t/deftest ^:async zero-duration-is-a-raw-observation
(let [rtx (context (volatile! 0))
result (await (cmd/execute rtx #(move* % {:dx 2 :dy 3})))]
(t/is (= :completed (:outcome result)))
(t/is (= {:scale 1 :x 2 :y 3} (get-in result [:context :view])))
(t/is (= [{:command ::move :metrics {:command/elapsed [0]}}]
(:events result)))))
(t/deftest ^:async failed-work-retains-earlier-observations
(let [unhandled (atom [])
observe #(swap! unhandled conj %)]
(.on js/process "unhandledRejection" observe)
(try
(doseq [cause [nil (js/Error. "rejected core")]]
(let [rtx (context (volatile! 0))
result (await (cmd/execute rtx #(-> %
(camera/render-sync*)
(unexpected-promise* cause))))]
(t/is (= :failed (:outcome result)))
(t/is (= ::cmd/async-result (:type (ex-data (:error result)))))
(t/is (= [{:command ::camera/render-sync
:metrics {:command/elapsed [7]}}]
(:events result)))))
(await (js/Promise. #(js/setImmediate %)))
(t/is (= [] @unhandled))
(finally
(.removeListener js/process "unhandledRejection" observe)))))
(t/deftest ^:async execution-failures-preserve-causes-and-earlier-observations
(doseq [cause [(js/Error. "renderer failed")
"renderer disconnected"
{:reason :renderer-disconnected}
(errors/fatal-error "recording failed" {} (js/Error. "storage"))]]
(let [clock (volatile! 0)
rtx (assoc-in (context clock) [:hooks :render-sync]
#(if (zero? @clock)
(vswap! clock + 7)
(throw cause)))
result (await (cmd/execute rtx #(-> %
(camera/render-sync*)
(camera/render-sync*))))
error (:error result)]
(t/is (= :failed (:outcome result)))
(t/is (errors/failure? error))
(if (errors/failure? cause)
(t/is (identical? cause error))
(t/is (= cause (:thrown (ex-data error)))))
(t/is (= [{:command ::camera/render-sync
:metrics {:command/elapsed [7]}}]
(:events result))))))
(t/deftest ^:async invalid-boundaries-fail-the-execution
(doseq [[input command expected]
[[#(assoc-in % [:view :scale] 0) camera/render-sync* ::cmd/invalid-context]
[#(assoc % :module #js {}) camera/render-sync* ::cmd/invalid-context]
[#(update % :hooks dissoc :render-sync) camera/render-sync* ::cmd/invalid-capabilities]
[identity #(reject-offset* % -1) ::cmd/invalid-arguments]]]
(let [clock (volatile! 0)
now-reads (volatile! 0)
rtx (assoc-in (context clock) [:hooks :now]
#(do (vswap! now-reads inc) @clock))
result (await (cmd/execute rtx #(command (input %))))]
(t/is (= :failed (:outcome result)))
(t/is (= expected (:type (ex-data (:error result)))))
(t/is (= [] (:events result)))
(t/is (zero? @now-reads))
(t/is (zero? @clock)))))
(t/deftest ^:async commands-enforce-execution-ownership
(let [rtx (context (volatile! 0))
result (await (cmd/execute rtx replace-owner*))
cause (await (.then (camera/render-sync* rtx) (fn [_] nil) identity))]
(t/is (= :failed (:outcome result)))
(t/is (= ::cmd/ownership-lost (:type (ex-data (:error result)))))
(t/is (= [] (:events result)))
(t/is (= ::cmd/missing-execution (:type (ex-data cause))))))
(t/deftest ^:async completed-context-starts-a-new-execution
(let [first-result (await (cmd/execute (context (volatile! 0))
#(move* % {:dx 10 :dy 5})))
rtx (:context first-result)
late-error (await (.then (camera/render-sync* rtx)
(fn [_] nil)
identity))
next-result (await (cmd/execute rtx camera/render-sync*))]
(t/is (= ::cmd/inactive-execution (:type (ex-data late-error))))
(t/is (= :completed (:outcome next-result)))
(t/is (= {:scale 1 :x 10 :y 5} (get-in next-result [:context :view])))
(t/is (= [{:command ::camera/render-sync
:metrics {:command/elapsed [7]}}]
(:events next-result)))))