mirror of
https://github.com/penpot/penpot.git
synced 2026-11-09 11:30:54 +00:00
✨ defcommand macro and expose render_sync
AI-assisted-by: gpt-6.1-sol, deepseek-flash
This commit is contained in:
parent
67e04f1c4a
commit
98fa12dec6
@ -11,5 +11,6 @@
|
|||||||
cljs.test/async [[:inner 0]]
|
cljs.test/async [[:inner 0]]
|
||||||
promesa.exec/thread [[:inner 0]]
|
promesa.exec/thread [[:inner 0]]
|
||||||
specify! [[:inner 0] [:inner 1]]
|
specify! [[:inner 0] [:inner 1]]
|
||||||
|
benches.render-wasm.runtime.commands/defcommand [[:inner 0]]
|
||||||
benches.render-wasm.runtime.protocol/resolve-or-reject-with [[:inner 0]]}
|
benches.render-wasm.runtime.protocol/resolve-or-reject-with [[:inner 0]]}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,6 +88,7 @@
|
|||||||
;; WASM calls
|
;; WASM calls
|
||||||
:render (fn [timestamp flags]
|
:render (fn [timestamp flags]
|
||||||
(h/call wasm/internal-module "_render" timestamp flags))
|
(h/call wasm/internal-module "_render" timestamp flags))
|
||||||
|
:render-sync #(h/call wasm/internal-module "_render_sync")
|
||||||
:render-from-cache #(h/call wasm/internal-module "_render_from_cache" 0)
|
:render-from-cache #(h/call wasm/internal-module "_render_from_cache" 0)
|
||||||
:set-view #(h/call wasm/internal-module "_set_view"
|
:set-view #(h/call wasm/internal-module "_set_view"
|
||||||
(:scale %) (- (:x %)) (- (:y %)))
|
(:scale %) (- (:x %)) (- (:y %)))
|
||||||
|
|||||||
@ -53,8 +53,16 @@
|
|||||||
their bodies. This namespace loads there because renderer effects come
|
their bodies. This namespace loads there because renderer effects come
|
||||||
through the browser-supplied `:hooks`."
|
through the browser-supplied `:hooks`."
|
||||||
(:require
|
(:require
|
||||||
|
[benches.render-wasm.runtime.commands :refer [defcommand]]
|
||||||
[benches.render-wasm.runtime.protocol :as protocol]))
|
[benches.render-wasm.runtime.protocol :as protocol]))
|
||||||
|
|
||||||
|
(defcommand render-sync
|
||||||
|
"Renders through the blocking renderer path."
|
||||||
|
{:requires [:render-sync]}
|
||||||
|
[rtx]
|
||||||
|
((get-in rtx [:hooks :render-sync]))
|
||||||
|
rtx)
|
||||||
|
|
||||||
(defn load!
|
(defn load!
|
||||||
"Drains the uploaded scene until the renderer reports Full completion.
|
"Drains the uploaded scene until the renderer reports Full completion.
|
||||||
`rtx` supplies the clock and renderer hooks. Returns a promise of first-render
|
`rtx` supplies the clock and renderer hooks. Returns a promise of first-render
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
;; 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.commands)
|
||||||
|
|
||||||
|
(defmacro defcommand
|
||||||
|
"Defines a command for scenes.
|
||||||
|
|
||||||
|
`(defcommand foo ...)` creates commands `foo` and `foo*`. The latter is
|
||||||
|
measured (records raw observations).
|
||||||
|
|
||||||
|
Both variants accept a context or its Promise and return a Promise of the
|
||||||
|
updated context. `:requires` declares callable hook names.
|
||||||
|
`:measured-variant false` defines a composite that awaits its returned context.
|
||||||
|
Argument destructuring and `:pre` checks precede the measured interval in
|
||||||
|
every build. The condition map accepts only `:pre`."
|
||||||
|
[command & forms]
|
||||||
|
(let [[doc forms] (if (string? (first forms))
|
||||||
|
[(first forms) (next forms)]
|
||||||
|
[nil forms])
|
||||||
|
[options forms] (if (map? (first forms))
|
||||||
|
[(first forms) (next forms)]
|
||||||
|
[{} forms])
|
||||||
|
[argv & body] forms
|
||||||
|
[conditions body] (if (and (map? (first body)) (next body))
|
||||||
|
[(first body) (next body)]
|
||||||
|
[nil body])
|
||||||
|
measured (get options :measured-variant true)
|
||||||
|
command-id (keyword (str (get-in &env [:ns :name])) (name command))
|
||||||
|
starred (symbol (str command "*"))
|
||||||
|
prepare (gensym "prepare")]
|
||||||
|
(when-not (and (simple-symbol? command)
|
||||||
|
(vector? argv)
|
||||||
|
(seq argv)
|
||||||
|
(boolean? measured)
|
||||||
|
(every? #{:requires :measured-variant} (keys options))
|
||||||
|
(every? keyword? (:requires options))
|
||||||
|
(every? #{:pre} (keys conditions))
|
||||||
|
(or (nil? (:pre conditions)) (vector? (:pre conditions))))
|
||||||
|
(throw (ex-info "invalid command declaration" {:command command})))
|
||||||
|
`(let [~prepare (fn ~argv
|
||||||
|
~@(for [condition (:pre conditions)]
|
||||||
|
`(when-not ~condition
|
||||||
|
(throw (ex-info "command precondition failed"
|
||||||
|
{:type ::invalid-arguments
|
||||||
|
:command ~command-id
|
||||||
|
:condition '~condition}))))
|
||||||
|
(fn [] ~@body))]
|
||||||
|
(defn ~command
|
||||||
|
~@(when doc [doc])
|
||||||
|
[rtx# & args#]
|
||||||
|
(invoke {:command ~command-id :measured false :composite ~(not measured)
|
||||||
|
:requires ~(:requires options)}
|
||||||
|
rtx# ~prepare args#))
|
||||||
|
~@(when measured
|
||||||
|
[`(defn ~starred
|
||||||
|
~(str "Measured variant of `" command "`." (when doc (str " " doc)))
|
||||||
|
[rtx# & args#]
|
||||||
|
(invoke {:command ~command-id :measured true :composite false
|
||||||
|
:requires ~(:requires options)}
|
||||||
|
rtx# ~prepare args#))]))))
|
||||||
127
frontend/performance/benches/render_wasm/runtime/commands.cljs
Normal file
127
frontend/performance/benches/render_wasm/runtime/commands.cljs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
;; 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.commands
|
||||||
|
"Renderer commands and collection of raw command observations."
|
||||||
|
(:require-macros [benches.render-wasm.runtime.commands])
|
||||||
|
(:require
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[benches.render-wasm.declarations :as decl]
|
||||||
|
[benches.render-wasm.errors :as errors :refer [attempt failure?]]))
|
||||||
|
|
||||||
|
(def ^:private schema:context
|
||||||
|
[:map {:closed true}
|
||||||
|
[:case [:map [:id qualified-keyword?]]]
|
||||||
|
[:scene [:map [:objects map?] [:refs map?]]]
|
||||||
|
[:view decl/schema:view]
|
||||||
|
[:hooks [:and
|
||||||
|
[:map-of :keyword fn?]
|
||||||
|
[:map [:now fn?] [:check fn?]]]]])
|
||||||
|
|
||||||
|
(def ^:private valid-context? (sm/validator schema:context))
|
||||||
|
(def ^:private owners (js/WeakMap.))
|
||||||
|
|
||||||
|
(defn- check-context
|
||||||
|
[rtx]
|
||||||
|
(when-not (valid-context? rtx)
|
||||||
|
(throw (ex-info "invalid command context"
|
||||||
|
{:type ::invalid-context
|
||||||
|
::sm/explain (sm/explain schema:context rtx)}))))
|
||||||
|
|
||||||
|
(defn- check-output
|
||||||
|
[rtx output]
|
||||||
|
(check-context output)
|
||||||
|
(when-not (identical? (get-in rtx [:hooks :check])
|
||||||
|
(get-in output [:hooks :check]))
|
||||||
|
(throw (ex-info "command changed execution ownership"
|
||||||
|
{:type ::ownership-lost})))
|
||||||
|
((get-in rtx [:hooks :check]))
|
||||||
|
output)
|
||||||
|
|
||||||
|
(defn- scope-context
|
||||||
|
[rtx events active]
|
||||||
|
(check-context rtx)
|
||||||
|
(let [previous-check (get-in rtx [:hooks :check])
|
||||||
|
previous-owner (.get owners previous-check)
|
||||||
|
base-check (or (:base-check previous-owner) previous-check)
|
||||||
|
check (fn []
|
||||||
|
(when-not @active
|
||||||
|
(throw (ex-info "command execution has ended"
|
||||||
|
{:type ::inactive-execution})))
|
||||||
|
(base-check))
|
||||||
|
scoped (assoc-in rtx [:hooks :check] check)]
|
||||||
|
(.set owners check {:base-check base-check
|
||||||
|
:record #(do (check) (vswap! events conj %))})
|
||||||
|
(check)
|
||||||
|
scoped))
|
||||||
|
|
||||||
|
(defn ^:async ^:no-doc execute
|
||||||
|
"Executes a context function and returns its outcome and ordered raw events.
|
||||||
|
A completed execution includes its updated context. A failed execution
|
||||||
|
includes its cause and any events recorded before failure."
|
||||||
|
[rtx run]
|
||||||
|
(let [events (volatile! [])
|
||||||
|
active (volatile! true)
|
||||||
|
result (try
|
||||||
|
(attempt :catch-fatal
|
||||||
|
(let [scoped (scope-context (await rtx) events active)]
|
||||||
|
(check-output scoped (await (run scoped)))))
|
||||||
|
(finally
|
||||||
|
(vreset! active false)))]
|
||||||
|
(if (failure? result)
|
||||||
|
{:outcome :failed :error result :events @events}
|
||||||
|
{:outcome :completed :context result :events @events})))
|
||||||
|
|
||||||
|
(defn- prepare-invocation
|
||||||
|
[{:keys [command measured requires] :as invocation} rtx prepare args]
|
||||||
|
(check-context rtx)
|
||||||
|
(let [{:keys [check now] :as hooks} (:hooks rtx)
|
||||||
|
owner (.get owners check)]
|
||||||
|
(when-not (sm/validate (into [:map] (map #(vector % fn?) requires)) hooks)
|
||||||
|
(throw (ex-info "command requires callable hooks"
|
||||||
|
{:type ::invalid-capabilities :command command
|
||||||
|
:required requires})))
|
||||||
|
(check)
|
||||||
|
(when (and measured (nil? owner))
|
||||||
|
(throw (ex-info "measured command requires an execution"
|
||||||
|
{:type ::missing-execution :command command})))
|
||||||
|
(assoc invocation
|
||||||
|
:input rtx
|
||||||
|
:core (apply prepare rtx args)
|
||||||
|
:now now
|
||||||
|
:record (:record owner))))
|
||||||
|
|
||||||
|
(defn- run-core
|
||||||
|
[{:keys [command measured composite core now] :as invocation}]
|
||||||
|
(if composite
|
||||||
|
(assoc invocation :output (core))
|
||||||
|
(let [start (when measured (now))
|
||||||
|
output (core)
|
||||||
|
end (when measured (now))]
|
||||||
|
(when (and (some? output) (fn? (.-then output)))
|
||||||
|
(.catch (js/Promise.resolve output) errors/as-failure)
|
||||||
|
(throw (ex-info "synchronous command returned asynchronous work"
|
||||||
|
{:type ::async-result :command command})))
|
||||||
|
(cond-> (assoc invocation :output output)
|
||||||
|
measured (assoc :elapsed (- end start))))))
|
||||||
|
|
||||||
|
(defn- finish-invocation
|
||||||
|
[{:keys [command measured input output elapsed record]}]
|
||||||
|
(check-output input output)
|
||||||
|
(when measured
|
||||||
|
(record {:command command :metrics {:command/elapsed [elapsed]}}))
|
||||||
|
output)
|
||||||
|
|
||||||
|
(defn ^:async invoke
|
||||||
|
"Resolves a command input and executes its declared core.
|
||||||
|
Measured invocations append one raw elapsed observation to their execution."
|
||||||
|
[command rtx prepare args]
|
||||||
|
(let [invocation (prepare-invocation command (await rtx) prepare args)
|
||||||
|
result (run-core invocation)]
|
||||||
|
(finish-invocation
|
||||||
|
(if (:composite command)
|
||||||
|
(assoc result :output (await (:output result)))
|
||||||
|
result))))
|
||||||
196
frontend/test/performance_tests/render_wasm/commands_test.cljs
Normal file
196
frontend/test/performance_tests/render_wasm/commands_test.cljs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
;; 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)))))
|
||||||
Loading…
x
Reference in New Issue
Block a user