diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index bf442e4473..aeb8f9056c 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -28,7 +28,9 @@ clojure.core/let hooks.core/check-let cljs.core/let hooks.core/check-let benches.render-wasm.declarations/defscene hooks.benches/defscene - benches.render-wasm.declarations/defcase hooks.benches/defcase}} + benches.render-wasm.declarations/defcase hooks.benches/defcase + benches.render-wasm.runtime.protocol/resolve-or-reject hooks.benches/resolve-or-reject + benches.render-wasm.runtime.protocol/resolve-or-reject-with hooks.benches/resolve-or-reject-with}} :output {:exclude-files diff --git a/.clj-kondo/hooks/benches.clj b/.clj-kondo/hooks/benches.clj index 03c81b3582..1332af9a8a 100644 --- a/.clj-kondo/hooks/benches.clj +++ b/.clj-kondo/hooks/benches.clj @@ -24,3 +24,21 @@ {:node (api/list-node [(api/token-node (quote do)) opts (api/list-node (into [(api/token-node (quote fn)) argv] body))])} {:node node}))) + +(defn resolve-or-reject + "Analyzes the body of a Promise-returning form." + [{:keys [node]}] + (let [[_ & body] (:children node)] + {:node (api/list-node (into [(api/token-node (quote do))] body))})) + +(defn resolve-or-reject-with + "Analyzes the cause handler and body. Binds `cause` in the handler only." + [{:keys [node]}] + (let [[_ cause on-error & body] (:children node)] + {:node (api/list-node + (into [(api/token-node (quote do)) + (api/list-node + [(api/token-node (quote let)) + (api/vector-node [cause (api/token-node nil)]) + on-error])] + body))})) diff --git a/.cljfmt.edn b/.cljfmt.edn index ad56b8c70d..126cef0edb 100644 --- a/.cljfmt.edn +++ b/.cljfmt.edn @@ -10,5 +10,6 @@ :extra-indents {rumext.v2/fnc [[:inner 0]] cljs.test/async [[:inner 0]] promesa.exec/thread [[:inner 0]] - specify! [[:inner 0] [:inner 1]]} + specify! [[:inner 0] [:inner 1]] + benches.render-wasm.runtime.protocol/resolve-or-reject-with [[:inner 0]]} } diff --git a/frontend/performance/benches/render_wasm/runtime/protocol.clj b/frontend/performance/benches/render_wasm/runtime/protocol.clj new file mode 100644 index 0000000000..75b9ea3a43 --- /dev/null +++ b/frontend/performance/benches/render_wasm/runtime/protocol.clj @@ -0,0 +1,26 @@ +;; 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.runtime.protocol) + +(defmacro resolve-or-reject + "Evaluates `body` and returns a Promise. A synchronous throw rejects the + returned promise." + [& body] + `(try + (js/Promise.resolve (do ~@body)) + (catch :default cause# + (js/Promise.reject cause#)))) + +(defmacro resolve-or-reject-with + "Evaluates `body` and returns a Promise. A synchronous throw rejects with the + result of `on-error`, evaluated with `cause` bound to the thrown value." + [cause on-error & body] + `(try + (js/Promise.resolve (do ~@body)) + (catch :default cause# + (let [~cause cause#] + (js/Promise.reject ~on-error))))) diff --git a/frontend/performance/benches/render_wasm/runtime/protocol.cljs b/frontend/performance/benches/render_wasm/runtime/protocol.cljs index b66519f3c6..5a6215c303 100644 --- a/frontend/performance/benches/render_wasm/runtime/protocol.cljs +++ b/frontend/performance/benches/render_wasm/runtime/protocol.cljs @@ -80,6 +80,8 @@ boundaries, renamed at the interaction level. - `:last-input-to-full-ms` from the clock read before the last view update to the clock read after the final drain promise resolves." + (:require-macros + [benches.render-wasm.runtime.protocol :refer [resolve-or-reject resolve-or-reject-with]]) (:require [app.common.render-wasm.wasm :as wasm] [benches.render-wasm.errors :as errors])) @@ -200,75 +202,66 @@ retrying." [{:keys [check] :as hooks} {:keys [flags origin immediate]}] - (try - (check) - (letfn [(step [state immediate?] - (-> (next-render-timestamp hooks immediate?) - (.then ;; recurse or drain - (fn [timestamp] - (let [{:keys [frame-type ended-ms] :as rendered} - (render-slice hooks state timestamp) - next-state - (advance-drain-state (:state rendered) - frame-type - ended-ms - origin)] + (resolve-or-reject + (check) + (letfn [(step [state immediate?] + (-> (next-render-timestamp hooks immediate?) + (.then ;; recurse or drain + (fn [timestamp] + (let [{:keys [frame-type ended-ms] :as rendered} + (render-slice hooks state timestamp) + next-state + (advance-drain-state (:state rendered) + frame-type + ended-ms + origin)] - (if (drain-complete? frame-type) - (drain-result next-state ended-ms origin) - (step next-state false))))) - (.catch (fn [cause] - (throw (partial-error cause (select-keys state [:slices :viewport-ready-ms])))))))] - - (step {:flags flags - :slices [] - :viewport-ready-ms nil} - immediate)) - - (catch :default cause - (js/Promise.reject cause)))) + (if (drain-complete? frame-type) + (drain-result next-state ended-ms origin) + (step next-state false))))) + (.catch (fn [cause] + (throw (partial-error cause (select-keys state [:slices :viewport-ready-ms])))))))] + (step {:flags flags + :slices [] + :viewport-ready-ms nil} + immediate)))) (defn restore "Puts the camera back to `view` (`{:scale :x :y}`) and drains with the SyncTiles flag immediately. Resolves what `drain` resolves. Callers run this outside timing: the reset itself is never scored." [{:keys [now check set-view set-view-start set-view-end] :as hooks} view] - (try - (check) - (set-view-start) - (set-view view) - (set-view-end) - (drain hooks {:flags wasm/RENDER_FLAG_SYNC_TILES - :origin (now) - :immediate true}) - (catch :default cause - (js/Promise.reject cause)))) + (resolve-or-reject + (check) + (set-view-start) + (set-view view) + (set-view-end) + (drain hooks {:flags wasm/RENDER_FLAG_SYNC_TILES + :origin (now) + :immediate true}))) (defn start-camera! "Starts a measured camera session after the caller's untimed preparation. `rtx` supplies `:hooks` and the current full `:view`." [{:keys [hooks view]} {:keys [settle-ms]}] - (try - (when-not (and (number? settle-ms) - (js/Number.isFinite settle-ms) - (<= 0 settle-ms)) - (throw (ex-info "settle-ms must be a nonnegative finite number" - {:type ::invalid-settle-ms :settle-ms settle-ms}))) - (let [{:keys [now check set-view-start]} hooks] - (check) - (let [started-ms (now)] - (set-view-start) - (js/Promise.resolve - {:hooks hooks - :view view - :settle-ms settle-ms - :started-ms started-ms - :last-input-ms started-ms - :active-end-ms (now) - :cached-slices [] - :authored-sleeps []}))) - (catch :default cause - (js/Promise.reject cause)))) + (resolve-or-reject + (when-not (and (number? settle-ms) + (js/Number.isFinite settle-ms) + (<= 0 settle-ms)) + (throw (ex-info "settle-ms must be a nonnegative finite number" + {:type ::invalid-settle-ms :settle-ms settle-ms}))) + (let [{:keys [now check set-view-start]} hooks] + (check) + (let [started-ms (now)] + (set-view-start) + {:hooks hooks + :view view + :settle-ms settle-ms + :started-ms started-ms + :last-input-ms started-ms + :active-end-ms (now) + :cached-slices [] + :authored-sleeps []})))) (defn preview-view! "Awaits a session and submits one cached preview after exactly one rAF. @@ -279,7 +272,8 @@ (.then (fn [{:keys [hooks] :as session}] (let [{:keys [now frame check render-from-cache set-view]} hooks evidence (select-keys session [:cached-slices :authored-sleeps])] - (try + (resolve-or-reject-with cause + (partial-error cause evidence) (check) (-> (frame) (.then (fn [timestamp] @@ -300,9 +294,7 @@ (throw (partial-error cause (assoc evidence :cached-slices slices))))) (assoc session :view view :last-input-ms input-ms :active-end-ms (now) :cached-slices slices)))))) - (.catch (fn [cause] (throw (partial-error cause evidence))))) - (catch :default cause - (js/Promise.reject (partial-error cause evidence))))))))) + (.catch (fn [cause] (throw (partial-error cause evidence))))))))))) (defn animate-view! "Submits exactly `steps` serial, linearly interpolated full views. @@ -331,7 +323,8 @@ (.then (fn [{:keys [hooks] :as session}] (let [{:keys [now sleep check]} hooks evidence (select-keys session [:cached-slices :authored-sleeps])] - (try + (resolve-or-reject-with cause + (partial-error cause evidence) (when-not (and (number? requested-ms) (js/Number.isFinite requested-ms) (<= 0 requested-ms)) @@ -348,9 +341,7 @@ (update :authored-sleeps conj {:requested-ms requested-ms :actual-ms (- ended-ms started-ms)}))))) - (.catch (fn [cause] (throw (partial-error cause evidence)))))) - (catch :default cause - (js/Promise.reject (partial-error cause evidence))))))))) + (.catch (fn [cause] (throw (partial-error cause evidence)))))))))))) (defn- finish-interaction "Ends the camera and returns final metrics, retaining trace evidence on failure." @@ -392,10 +383,9 @@ (-> (js/Promise.resolve session-p) (.then (fn [{:keys [hooks settle-ms] :as session}] (let [evidence (select-keys session [:cached-slices :authored-sleeps])] - (try + (resolve-or-reject-with cause + (partial-error cause evidence) ((:check hooks)) (-> ((:sleep hooks) settle-ms) (.then (fn [_] (finish-interaction hooks session))) - (.catch (fn [cause] (throw (partial-error cause evidence))))) - (catch :default cause - (js/Promise.reject (partial-error cause evidence))))))))) + (.catch (fn [cause] (throw (partial-error cause evidence))))))))))) diff --git a/frontend/test/performance_tests/render_wasm/protocol_test.cljs b/frontend/test/performance_tests/render_wasm/protocol_test.cljs index 0252bb7070..c778d661a7 100644 --- a/frontend/test/performance_tests/render_wasm/protocol_test.cljs +++ b/frontend/test/performance_tests/render_wasm/protocol_test.cljs @@ -361,3 +361,30 @@ (.catch (fn [cause] (t/is (= "cache failed" (ex-message cause))) (done))))))) + +(t/deftest invalid-settle-ms-rejects + (t/async done + (let [hooks (drain-hooks {:clock (fake-clock 0) :script [2] :calls (atom [])})] + (-> (protocol/start-camera! {:hooks hooks :view {:scale 1 :x 0 :y 0}} + {:settle-ms -1}) + (.then (fn [_] + (t/is false "invalid settle-ms must reject") + (done))) + (.catch (fn [cause] + (t/is (= ::protocol/invalid-settle-ms (:type (ex-data cause)))) + (done))))))) + +(t/deftest invalid-sleep-rejects-with-camera-evidence + (t/async done + (let [hooks (drain-hooks {:clock (fake-clock 0) :script [2] :calls (atom [])})] + (-> (protocol/start-camera! {:hooks hooks :view {:scale 1 :x 0 :y 0}} + {:settle-ms 0}) + (protocol/sleep! -1) + (.then (fn [_] + (t/is false "invalid sleep must reject") + (done))) + (.catch (fn [cause] + (t/is (= ::protocol/invalid-sleep (:type (ex-data cause)))) + (t/is (= {:cached-slices [] :authored-sleeps []} + (:partial (ex-data cause)))) + (done)))))))