Miguel de Benito Delgado dc22e2d77f 🚧 WIP Step 21 Refactor namespaces
2026-10-28 19:17:39 +01:00

388 lines
16 KiB
Clojure
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;; 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
"Shared render-completion protocol for the renderer benchmarks.
The WASM renderer finishes progressively in tile budgets: one `_render`
call returns a frame type and the caller keeps calling until `FRAME_TYPE_FULL`.
This namespace implements a driver loop and camera sessions over an injected
`hooks` map. Callers use the same timing and continuation rules for
rendering and interaction:
- `drain` drives `_render` to `FRAME_TYPE_FULL`, collecting one slice per
call.
- `restore` puts the camera back to the case's initial view, then drains.
Callers run it outside timing.
- `start-camera!`, `preview-view!`, `animate-view!`, `sleep!` and
`finish-camera!` form one Promise-aware camera session. `interact` is a
thin adapter for callers with a predeclared frame vector.
`hooks` supplies clock, scheduling and renderer operations. The browser
supplies real capabilities; tests supply fakes.
- `:now` zero-arg clock, ms.
- `:frame` zero-arg promise of the next rAF timestamp.
- `:sleep` `(fn [ms] ...)` promise.
- `:check` zero-arg guard. Throws on cancellation, wall deadline,
context loss or superseded ownership. The protocol checks before work,
after awaited frames and sleeps, and around render calls. It never
retries; synchronous stalls cannot be
interrupted in-page, so Node stays the outer backstop.
- `:render` `(fn [timestamp flags] ...)` returning a frame type.
- `:render-from-cache` zero-arg cached-preview call. The protocol times
the call itself and discards its return value.
- `:set-view` `(fn [view] ...)` with `view` a `{:scale :x :y}` map.
- `:set-view-start` / `:set-view-end` zero-arg camera calls.
Guard and renderer errors reject the returned promises. The browser's
`:check` guard throws when work is cancelled, exceeds its deadline,
loses its rendering context or is no longer the active task.
Frame types come from `app.common.render-wasm.wasm`: FRAME_TYPE_PARTIAL and
FRAME_TYPE_VIEWPORT_READY continue, FRAME_TYPE_FULL completes, FRAME_TYPE_NONE
or anything else fails the attempt. The previous returned frame type becomes the
next call's flags. `restore` and the final drain of `interact` start with
the SyncTiles flag immediately (no rAF); a fresh drain can start with
different flags and wait for a frame.
## Metrics reported
- `:slices` one map per `_render` call:
- `:timestamp`: the rAF stamp the call ran under, or the clock value for
an immediate call without a frame
- `:flags`: flags passed in
- `:frame-type`: frame type returned
- `:duration-ms`: wall time of the call itself
Raw slices are retained as-is, including zero durations.
- `:cached-slices` one map per gesture frame:
- `:timestamp` rAF stamp of the input
- `:duration-ms`: the timed `_render-from-cache` call.
- `:view-and-preview-ms`: the view update and cached-preview call span.
Cached previews have their own shape: no flags or frame type.
- `:viewport-ready-ms` first Full-or-ViewportReady completion measured
from the drain origin. An immediate Full supplies both boundaries, so
they are equal there.
- `:full-ms` Full completion measured from the drain origin.
- `:interact-ms` from before `_set_view_start` through final Full.
- `:active-ms` from before `_set_view_start` to after the last cached
preview and its guard check, or an authored sleep after that preview.
- `:authored-sleeps` requested and actual durations of optional pauses.
- `:settling-requested-ms` the declared settle wait passed in.
- `:settling-actual-ms` from the end of the active span to finalization
start, recorded separately from the declared value.
- `:set-view-end-ms` wall time of the `_set_view_end` call itself.
Finalization starts before `_set_view_end`, so the final drain times
include it; the call time is still reported on its own.
- `:time-to-viewport-ready-ms` / `:time-to-full-ms` the drain's
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
[app.common.render-wasm.wasm :as wasm]))
(defn- next-render-timestamp
[{:keys [now frame]} immediate?]
(if immediate?
(js/Promise.resolve (now))
(frame)))
(defn- render-slice
"Times one render call and adds its slice to the drain state."
[{:keys [now check render]}
{:keys [flags] :as state}
timestamp]
(check)
(let [started-ms (now)
frame-type (render timestamp flags)
ended-ms (now)
slice {:timestamp timestamp
:flags flags
:frame-type frame-type
:duration-ms (- ended-ms started-ms)}]
(check)
{:state (update state :slices conj slice)
:frame-type frame-type
:ended-ms ended-ms}))
(defn- valid-frame-type?
[frame-type]
(or (= frame-type wasm/FRAME_TYPE_PARTIAL)
(= frame-type wasm/FRAME_TYPE_VIEWPORT_READY)
(= frame-type wasm/FRAME_TYPE_FULL)))
(defn- viewport-ready-time
[current-ready-ms frame-type ended-ms origin-ms]
(or current-ready-ms
(when (or (= frame-type wasm/FRAME_TYPE_VIEWPORT_READY)
(= frame-type wasm/FRAME_TYPE_FULL))
(- ended-ms origin-ms))))
(defn- advance-drain-state
[{:keys [viewport-ready-ms] :as state}
frame-type
ended-ms
origin-ms]
(when-not (valid-frame-type? frame-type)
(throw (ex-info (str "unexpected frame type: " (pr-str frame-type))
{:type ::unexpected-frame-type
:frame-type frame-type})))
(assoc state
:flags frame-type
:viewport-ready-ms
(viewport-ready-time viewport-ready-ms
frame-type
ended-ms
origin-ms)))
(defn- drain-complete?
[frame-type]
(= frame-type wasm/FRAME_TYPE_FULL))
(defn- drain-result
[{:keys [slices viewport-ready-ms]}
ended-ms
origin-ms]
{:slices slices
:viewport-ready-ms viewport-ready-ms
:full-ms (- ended-ms origin-ms)})
(defn drain
"Drives `_render` to `FRAME_TYPE_FULL` and resolves to
`{:slices [...] :viewport-ready-ms ... :full-ms ...}`.
The flow is:
drain
└─ step
├─ next-render-timestamp
├─ render-slice
├─ advance-drain-state
└─ either
├─ drain-result
└─ step
`opts` contains:
- `:flags` (first call's flags)
- `:origin` (required starting moment in ms)
- `:immediate` (run the first `_render` in the same task instead of waiting
for a frame; later frames always wait).
Partial and ViewportReady continue with the returned frame type as the next
flags. FRAME_TYPE_FULL resolves. Anything else rejects with
`::unexpected-frame-type`. A failure of the hook `:check` rejects without
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)]
(if (drain-complete? frame-type)
(drain-result next-state ended-ms origin)
(step next-state false)))))))]
(step {:flags flags
:slices []
:viewport-ready-ms nil}
immediate))
(catch :default cause
(js/Promise.reject cause))))
(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))))
(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))))
(defn preview-view!
"Awaits a session or session promise, then submits one cached preview after
exactly one rAF. The call span includes `_set_view` and cache rendering."
[session-p view]
(-> (js/Promise.resolve session-p)
(.then (fn [{:keys [hooks] :as session}]
(let [{:keys [now frame check render-from-cache set-view]} hooks]
(check)
(-> (frame)
(.then (fn [timestamp]
(check)
(let [input-ms (now)
call-start (now)]
(set-view view)
(let [cache-start (now)]
(render-from-cache)
(let [cache-end (now)]
(check)
(assoc session
:view view
:last-input-ms input-ms
:active-end-ms (now)
:cached-slices
(conj (:cached-slices session)
{:timestamp timestamp
:duration-ms (- cache-end cache-start)
:view-and-preview-ms (- cache-end call-start)})))))))))))))
(defn animate-view!
"Submits exactly `steps` serial, linearly interpolated full views."
[session-p {:keys [to steps]}]
(-> (js/Promise.resolve session-p)
(.then (fn [{from :view :as session}]
(when-not (and (integer? steps) (pos? steps))
(throw (ex-info "camera steps must be a positive integer"
{:type ::invalid-steps :steps steps})))
(reduce (fn [session-p i]
(let [t (/ i steps)]
(preview-view! session-p
{:scale (+ (:scale from) (* t (- (:scale to) (:scale from))))
:x (+ (:x from) (* t (- (:x to) (:x from))))
:y (+ (:y from) (* t (- (:y to) (:y from))))})))
(js/Promise.resolve session)
(range 1 (inc steps)))))))
(defn sleep!
"Awaits an authored pause inside the active span and records its actual time."
[session-p requested-ms]
(-> (js/Promise.resolve session-p)
(.then (fn [{:keys [hooks] :as session}]
(let [{:keys [now sleep check]} hooks]
(when-not (and (number? requested-ms)
(js/Number.isFinite requested-ms)
(<= 0 requested-ms))
(throw (ex-info "sleep duration must be a nonnegative finite number"
{:type ::invalid-sleep :requested-ms requested-ms})))
(check)
(let [started-ms (now)]
(-> (sleep requested-ms)
(.then (fn [_]
(check)
(let [ended-ms (now)]
(-> session
(assoc :active-end-ms ended-ms)
(update :authored-sleeps conj
{:requested-ms requested-ms
:actual-ms (- ended-ms started-ms)}))))))))))))
(defn- finish-interaction
[{:keys [now check set-view-end] :as hooks}
{:keys [settle-ms started-ms cached-slices last-input-ms active-end-ms authored-sleeps]}]
(check)
(let [finalization-start (now)
t0 (now)]
(set-view-end)
(let [set-view-end-ms (- (now) t0)]
(-> (drain hooks
{:flags wasm/RENDER_FLAG_SYNC_TILES
:origin finalization-start
:immediate true})
(.then
(fn [{:keys [slices viewport-ready-ms full-ms]}]
{:cached-slices cached-slices
:slices slices
:interact-ms (- (now) started-ms)
:active-ms (- active-end-ms started-ms)
:authored-sleeps authored-sleeps
:settling-requested-ms settle-ms
:settling-actual-ms (- finalization-start active-end-ms)
:set-view-end-ms set-view-end-ms
:time-to-viewport-ready-ms viewport-ready-ms
:time-to-full-ms full-ms
:last-input-to-full-ms (- (now) last-input-ms)}))))))
(defn finish-camera!
"Awaits a session, settles, ends the camera and drains through Full."
[session-p]
(-> (js/Promise.resolve session-p)
(.then (fn [{:keys [hooks settle-ms] :as session}]
((:check hooks))
(-> ((:sleep hooks) settle-ms)
(.then (fn [_]
(finish-interaction hooks session))))))))
(defn interact
"Replays `frames` (a vector of `{:scale :x :y}` views) as one gesture
and resolves the interaction metrics (see the namespace docstring).
The flow is:
interact
├─ start-camera!
├─ preview-view! × N
└─ finish-camera!
├─ settle
├─ set-view-end
└─ drain
Each frame waits for rAF, records the input time, sets the view and
times one cached preview into `:cached-slices`. Afterwards the declared
`settle-ms` elapses, `_set_view_end` is timed on its own, and a SyncTiles
immediate drain runs with its origin at finalization start, so the final
times include `_set_view_end` while reporting it separately."
[hooks
{:keys [frames settle-ms]}]
(-> (reduce preview-view!
(start-camera! {:hooks hooks :view (first frames)}
{:settle-ms settle-ms})
frames)
(finish-camera!)))