✨ Complete renderer benchmark command catalogue

- Improve readme
- Migrate scene cases to measured commands and remove obsolete descriptors.
- Preserve exact camera targets, including very small positive scales.

AI-assisted-by: gpt-6.1-sol
This commit is contained in:
Miguel de Benito Delgado 2026-11-05 10:07:07 +00:00
parent 6fa70dc47d
commit e4709287dd
19 changed files with 459 additions and 308 deletions

View File

@ -5,15 +5,6 @@ Chromium. Scenes describe shapes; cases describe the work to perform on them.
The suite records setup, rendering, pan and zoom durations and compares runs
with different renderer builds. [How to run](#how-to-run) gives the commands.
The suite has three parts:
1. A Node runner controls the experiment. It prepares or reuses a build, selects
cases, serves the browser code, launches Chromium, schedules attempts, saves
results, and prints reports. An attempt is one execution of a case.
2. The browser creates the renderer and graphics context, constructs the scene
snapshot, uploads its shapes, performs the work, and records durations.
3. The reporting code computes statistics from saved records in Node or the
browser without loading the renderer.
## Contents
@ -55,25 +46,28 @@ The suite has three parts:
- **Scene builder**: the construction API used inside a scene. It provides basic
shapes (`rect`, `path`, `circle`, `frame`, `group`, and `bool`) constructors,
and a (hopefully) self-explanatory API.
- **Reference**: a label that a scene attaches to a shape during construction.
A case operation uses references to find shapes it needs.
- **Shape reference**: a label that a scene attaches to a shape during
construction. A _case_ body uses references to find shapes it needs.
- **Case**: a scene plus its execution settings: parameters, base view, context
policy, completion rule, optional operation settings, and a run function. A
case is the unit that is measured.
- **Operation**: the function that performs a case's work during one attempt.
It receives a command context with the case data, scene snapshot, current
view, and hooks. Standard operations load, pan, and zoom.
policy, optional preparation callback, optional reset function, and an
executable body. A case is the unit that is measured.
- **Case body**: the function declared by `defcase` that performs a case's work
during one attempt. It receives a command context with the case data, scene
snapshot, current view, and hooks. It can call `render-sync`, `load`, `pan`,
`zoom`, or `restore` and their measured variants.
- **View**: the camera's zoom `:scale` and offsets `:x` and `:y`.
Optional `:viewport` settings give the canvas width, height, and device pixel
ratio, the number of drawing-buffer pixels per CSS pixel along each dimension.
- **Hook**: a function supplied through `:hooks` for clock readings,
scheduling, view updates, rendering, or checking whether work may continue.
- **Case descriptor**: What gets sent to the browser. It contains the case id,
scene, derived seed, parameters, view, context policy, completion rule, batch
size, preparation settings, and operation settings. Any functions are removed.
scene, scene description, derived seed, parameters, view, context policy, and
preparation settings. Functions stay in the local registry and are removed
from the descriptor; the browser resolves them by case id.
- **Catalogue**: the compiled list of case descriptors and scene declarations
for one build. Node reads the catalogue to select cases. The browser executor
contains the same declarations and looks up the run function by case id.
contains the same declarations and looks up the case body by case id. The
current catalogue declares 14 cases.
- **Fingerprint**: SHA-256 digest of the validated starting snapshot. A
comparison checks it to establish equal starting scene content.
@ -114,9 +108,9 @@ The suite has three parts:
- **Runner**: the Node command line interface that builds or reuses a build,
selects cases, serves the build, launches Chromium, runs attempts, saves
records, and prints reports.
- **Browser executor**: the compiled page and JavaScript that perform
preparation and case operations. Its bridge exposes the operations that Node
can request.
- **Browser executor**: the compiled page and JavaScript that execute
preparations and case bodies. Its bridge exposes the requests that Node can
send.
- **Measurement**: a numeric result for a quantity such as elapsed duration.
The suite measures module start, graphics start, upload, first render, and
camera movement separately.
@ -217,9 +211,12 @@ The suite has three parts:
## Architecture
### Components
The suite has three components: runner, browser, and reports. They are all driven via the same CLI.
The Node runner owns everything outside the page:
### Node runner
The Node runner owns everything outside the page and
controls the experiment.
- it prepares or loads a build;
- it reads the compiled catalogue and selects cases;
@ -230,16 +227,20 @@ The Node runner owns everything outside the page:
- it writes the run record as work progresses;
- it prints a final report.
The browser bridge owns everything inside the page:
### Browser bridge
The bridge owns everything inside the page that runs the benchmark:
- it validates a request before assigning the page to that work;
- it imports the renderer module and creates the canvas and WebGL2 context;
- it builds, validates, fingerprints, orders, and uploads the scene;
- it carries each phase's returned context into the next phase or attempt;
- it runs the case operation and records the requested measurements;
- it runs the case body and records the measurements its commands select;
- it returns plain data and disposes the context when asked.
The shared reporting code owns analysis:
### Reporting
This code performs the analysis offline:
- it normalizes run records into summaries;
- it computes statistics and intervals for each metric;
@ -387,7 +388,7 @@ Containers follow the same pattern:
### Declaring a case
For a scene named `:rects`, case ids start with `:rects/`. The `defcase` form
derives the scene from that keyword namespace and attaches the run function.
derives the scene from that keyword namespace and attaches the case body.
```clojure
(defcase :rects/pan
@ -395,7 +396,7 @@ derives the scene from that keyword namespace and attaches the run function.
:view {:scale 1 :x 0 :y 0}
:context :reuse
:reset (fn [rtx]
(camera/pan rtx {:steps 20 :dx -200 :dy 0 :settle-ms 100}))}
(camera/restore rtx (get-in rtx [:case :view])))}
[rtx]
(camera/pan* rtx {:steps 20 :dx 200 :dy 0 :settle-ms 100}))
```
@ -416,6 +417,10 @@ The case declares:
main execution and returns the next context or its Promise. Reset runs after
every successful main, including warmups and the final attempt.
The case body owns the command sequence, arguments, and any deliberate
repetition. The declaration supplies the starting settings; the body and the
commands it calls own the work.
Commands receive workload arguments from the case body. The callback, main body,
and reset share the command context and can each call measured commands.
@ -455,12 +460,22 @@ shape.
### Some example scenes
| Scene | Content | Cases |
| ------- | ----------------------------------------------------------------------- | --------------- |
| `rects` | 1000 rectangles with random size, translucent fills, strokes, and radii | load, pan, zoom |
| `paths` | 1000 curved paths with fills and strokes | load, pan, zoom |
| `plus` | 1000 plus signs made from two open line subpaths | load, pan, zoom |
| `masks` | 24 masked groups, each with a circle mask and 8 striped rectangles | load, pan, zoom |
| Scene | Content | Cases |
| ------- | ----------------------------------------------------------------------- | -------------------------------------- |
| `rects` | 1000 rectangles with random size, translucent fills, strokes, and radii | load, pan, zoom, blocking-full, mixed-pans |
| `paths` | 1000 curved paths with fills and strokes | load, pan, zoom |
| `plus` | 1000 plus signs made from two open line subpaths | load, pan, zoom |
| `masks` | 24 masked groups, each with a circle mask and 8 striped rectangles | load, pan, zoom |
The current catalogue has 14 cases: load, pan, and zoom for each scene, plus
`rects/blocking-full` and `rects/mixed-pans`. Every load case is cold, with
`:context :fresh` and `:preparation {:render :none}`, and its body calls
`camera/load*`. The pan and zoom cases for each scene reuse one prepared context
and keep their declared offsets, steps, factor, and settling time; their reset
calls the bare `camera/restore` to return to the case view without recording an
event. `rects/blocking-full` is another fresh cold case and calls
`camera/render-sync*`. `rects/mixed-pans` runs two measured pans around one bare
pan and resets with measured `camera/restore*`.
Scene recipes supply canonical geometry for rendering and camera workloads.
Camera sessions submit view updates and wait for settling before their final
@ -489,6 +504,34 @@ by throwing an error. The owner is the execution authorized to use the renderer.
Returned contexts retain the execution's `:check` guard; replacing it rejects
completion.
### Public commands
The runtime provides five public commands. Each has a bare function and a
measured variant with `*`. Both forms accept a context or its Promise and return
a Promise of the next context.
| Command | Work | Recorded metric types |
| --- | --- | --- |
| `render-sync` | Runs the blocking renderer path once. | `:command/elapsed` |
| `load` | Waits for an animation frame, then renders until `Full`. | `:command/elapsed`, `:renderer/call-elapsed`, `:renderer/prepared-scene-to-full` |
| `pan` | Moves by `:dx` and `:dy` through `:steps` cached previews, waits `:settle-ms`, ends the interaction, and renders until `Full`. | `:command/elapsed`, `:renderer/cache-call-elapsed`, `:renderer/call-elapsed`, `:camera/finalization-to-full` |
| `zoom` | Multiplies scale by `:factor` with the same camera session. | The same types as `pan`. |
| `restore` | Applies a view and renders immediately with `SyncTiles` until `Full`. | `:command/elapsed`, `:renderer/call-elapsed` |
Bare commands update the context but record no event. A measured command records
one event. `load`, `pan`, `zoom`, and `restore` carry the final renderer return
captured by the protocol into `:end-ms`; their command elapsed ends at that
endpoint, not at a later Promise callback. `:renderer/call-elapsed` holds one
duration per renderer call in the final drain, in call order. `pan` and `zoom`
also record one `:renderer/cache-call-elapsed` value per cached preview and a
single `:camera/finalization-to-full` value. `load` adds
`:renderer/prepared-scene-to-full`, the drain's `:full-ms`. `render-sync` is
synchronous and records only its command elapsed.
`restore` requires the target view's resolved viewport to equal the current
context's resolved viewport. A different width, height, or device pixel ratio
fails before the command changes the renderer state.
### Declaring a command
`defcommand` defines two functions from one body. The base function runs the
@ -555,25 +598,36 @@ The command supplies `:command/elapsed`, its body duration in milliseconds.
`:metrics` supplies additional quantities. Command events preserve zero,
negative, `NaN`, and infinite numeric values as raw measurements.
Camera completion illustrates the callback interface:
Camera completion illustrates the callback interface. The current helpers copy
the protocol's captured endpoint and renderer call durations into the completion
map:
```clojure
(defn- render-evidence
[evidence]
{:end-ms (:end-ms evidence)
:metrics {:renderer/call-elapsed (mapv :duration-ms (:slices evidence))}
:trace evidence})
(defn- camera-evidence
[evidence]
(-> (render-evidence evidence)
(assoc-in [:metrics :renderer/cache-call-elapsed]
(mapv :duration-ms (:cached-slices evidence)))
(assoc-in [:metrics :camera/finalization-to-full]
[(:time-to-full-ms evidence)])))
(defcommand pan
"Moves the view and completes when rendering reaches Full."
"Moves the view by `:dx` and `:dy` through `:steps` cached previews.
Waits `:settle-ms` milliseconds, ends the interaction and renders until Full."
{:async [complete fail]}
[rtx {:keys [dx dy steps settle-ms]}]
(let [target (-> (:view rtx)
(update :x + dx)
(update :y + dy))]
(-> (protocol/start-camera! rtx {:settle-ms settle-ms})
(protocol/animate-view! {:to target :steps steps})
(protocol/finish-camera!)
(.then (fn [trace]
[rtx {:keys [dx dy] :as options}]
{:pre [(valid-pan? options) (valid-view? (pan-view (:view rtx) dx dy))]}
(let [target (pan-view (:view rtx) dx dy)]
(-> (run-camera rtx target options)
(.then (fn [evidence]
(complete (assoc rtx :view target)
{:end-ms (:end-ms trace)
:metrics {:renderer/call-elapsed
(mapv :duration-ms (:slices trace))}
:trace trace})))
(camera-evidence evidence))))
(.catch fail))))
```
@ -581,22 +635,8 @@ The final renderer call can return before the Promise callback runs. The
`:end-ms` field carries that renderer endpoint through the scheduling delay.
For example, renderer work lasting 7 ms followed by a 500 ms callback delay
still supplies a renderer duration of 7 ms. A clock reading in the later
callback would measure 507 ms.
The provided `camera/pan` and `camera/pan*` commands accept explicit `:dx`, `:dy`,
`:steps`, and `:settle-ms`. They submit interpolated views on animation frames,
draw cached previews, wait for settling, and finish rendering to `Full`. They
resolve to the context with the final view. The measured function records:
| Metric | Values in one event, in milliseconds |
| --- | --- |
| `:command/elapsed` | One duration from the start of the body through the renderer endpoint. |
| `:renderer/call-elapsed` | One duration per renderer call in the final drain. |
| `:renderer/cache-call-elapsed` | One duration per cached render call. |
| `:camera/finalization-to-full` | One duration from before ending the interaction through the renderer endpoint. |
Its trace also contains individual render calls, view-update durations, waits,
and the final `:end-ms`. Each vector preserves the call order within its quantity.
callback would measure 507 ms. The public commands table lists the metric types
each measured command records.
### Composition and execution
@ -615,7 +655,9 @@ function and selects measurements through the calls in its body:
All three calls update the view and renderer. The first and third record
events. Command functions resolve incoming Promises, so threading passes each
updated context to the next command.
updated context to the next command. `rects/mixed-pans` declares this pattern:
two measured pans and one bare pan in main, then a measured `camera/restore*`
in reset.
A command execution runs a function from one command context to the next,
authorizes its work, and collects events in completion order. Given a prepared
@ -688,15 +730,15 @@ case's browser and starts the next case in a new browser when the run continues.
[`runtime/commands.clj`](runtime/commands.clj) declares commands;
[`runtime/commands.cljs`](runtime/commands.cljs) executes them.
[`runtime/camera.cljs`](runtime/camera.cljs) defines rendering and camera
operations. [`runtime/protocol.cljs`](runtime/protocol.cljs) defines render
commands. [`runtime/protocol.cljs`](runtime/protocol.cljs) defines render
drains, camera sessions, hook contracts, and trace fields.
Case declarations such as [`scenes/rects.cljs`](scenes/rects.cljs) attach operation
Case declarations such as [`scenes/rects.cljs`](scenes/rects.cljs) attach case
bodies through [`declarations.cljc`](declarations.cljc). Node loads these
declarations and functions during catalogue collection. Renderer operations run
declarations and functions during catalogue collection. Renderer commands run
through the supplied hooks when the functions execute in the browser.
[`browser/bridge.cljs`](browser/bridge.cljs) constructs the runtime context for
case operations and prepares graphics contexts, snapshots, and uploads. Reused
case bodies and prepares graphics contexts, snapshots, and uploads. Reused
cases carry their returned context into the next attempt; their reset function
controls restoration.
@ -724,7 +766,7 @@ Preparation diagnostics time:
- the automatic render drain to `Full`.
Measured commands record their own intervals during preparation, main, or reset.
For a camera command, this includes view updates, cached previews, settling,
For `pan` and `zoom`, this includes view updates, cached previews, settling,
ending the interaction, and the final drain.
### Lifecycle of a fresh load
@ -753,8 +795,8 @@ starts its clock immediately before its work.
### Warm interactions
A warm case owns one prepared page. Its main body starts from the context
returned by preparation or the preceding attempt's reset. A camera command runs
this session:
returned by preparation or the preceding attempt's reset. A `pan` or `zoom`
command runs this session:
1. start the camera interaction;
2. for each view update, wait for the next animation frame, apply the view, and
@ -764,10 +806,11 @@ this session:
5. render until `Full`.
A pure pan can reuse cached textures while it updates which tiles are visible.
A zoom rebuilds and invalidates tile textures. Standard cases request a 100 ms
settling wait and record the requested and observed durations separately from
renderer calls. Camera finalization includes the call that ends the interaction;
the run also records that call's duration.
A zoom rebuilds and invalidates tile textures. The standard pan and zoom cases
request a 100 ms settling wait; `rects/mixed-pans` uses 50 ms for its three
pans. They record the requested and observed durations separately from renderer
calls. Camera finalization includes the call that ends the interaction; the run
also records that call's duration.
### Slices and metrics
@ -782,7 +825,7 @@ A render drain retains these trace fields:
drain origin;
- `full-ms`: the `Full` completion from the drain origin.
A camera command's trace contains the camera session and final drain:
A `pan` or `zoom` command's trace contains the camera session and final drain:
- `cached-slices`: one record per gesture frame with `timestamp`,
`duration-ms` for the cached preview call, and `view-and-preview-ms` for the
@ -805,6 +848,11 @@ Both drains and camera sessions return `end-ms`, the clock reading immediately
after the final renderer call returns `Full`. Completion durations end at that
reading. Promise callbacks can run later as the browser schedules queued work.
A `load` or `restore` command uses the final drain trace directly. `load` also
records `:renderer/prepared-scene-to-full` from `:full-ms`. `restore` starts its
drain immediately with `SyncTiles` and records the same per-call durations as the
final drain.
Measured command events keep each metric's raw values in a vector. Repeated
commands produce separate events, including when they name the same metric.
Raw values preserve zero, negative, and non-finite measurements in the saved
@ -1008,8 +1056,7 @@ By default, a comparison requires equal:
- scene identity, scene seed, and scene parameters;
- starting-snapshot fingerprints;
- view, context policy, completion rule, batch size, preparation settings, and
operation settings;
- view, context policy, and preparation settings;
- effective graphics settings and runtime environment;
- build mode, effective features, instrumentation settings, and recorded build
environment;
@ -1037,8 +1084,7 @@ mismatch with the rule that allowed it.
- `--allow-build-differences` acknowledges recorded build settings, effective
features, instrumentation, and the recorded build environment. Runtime
environment, graphics, operation settings, and saved quantity units stay
strict.
environment, graphics, and saved quantity units stay strict.
- `--allow-scene-differences` acknowledges scene identity, scene seed, scene
parameters, and differences in fingerprint content. Execution settings stay
strict. Missing or varying fingerprint evidence still needs the
@ -1102,10 +1148,9 @@ and launches no browser.
Every configuration gets its own build, catalogue, browser executor, server,
browser, and run. Each configuration runs and cleans up before the next one
starts. The configurations share the selected cases and their seeds, parameters,
views, operations, budgets, preparation settings, and batch sizes. Collection
views, context policies, preparation settings, and case deadlines. Collection
checks each configuration's catalogue identity and requires the same selected
case
descriptors in the same order.
case descriptors in the same order.
```bash
pnpm run build:renderer-benchmarks:runner
@ -1167,10 +1212,9 @@ retains an unacknowledged difference keeps its saved report, is marked failed,
and makes the invocation exit 1.
Other build differences require `--allow-build-differences`. Scene differences
require `--allow-scene-differences`. Operation, view,
context, preparation, completion, batch size, graphics, runtime environment, and
saved quantity-unit differences stay strict unless `--allow-all-differences` is
selected. If two configurations report the same effective feature set, the
require `--allow-scene-differences`. View, context, preparation, graphics,
runtime environment, and saved quantity-unit differences stay strict unless
`--allow-all-differences` is selected. If two configurations report the same effective feature set, the
comparison includes their edge and labels it with that fact. Effective feature
sets describe the code settings that accompany the observed timings.
@ -1198,7 +1242,10 @@ pnpm run render-bench -- run --help
Command options select declared cases and control the build source, sampling,
output and hardware policy. Case declarations own workload bodies, scene
parameters, view and operation settings, and case lifecycle.
parameters, view, context policy, preparation, reset, and case lifecycle.
`--seed` is the explicit exception: it changes generated scene content
reproducibly, but it does not replace a case body, its command arguments, or its
lifecycle settings.
Build the runner and its command module:
@ -1213,6 +1260,18 @@ pnpm run render-bench -- run \
--filter paths/pan --seed 42 --warmups 0 --repetitions 1 --screenshots
```
Run the blocking renderer and mixed gesture proof cases:
```bash
pnpm run render-bench -- run \
--filter rects/blocking-full --seed 42 \
--warmups 0 --repetitions 1 --screenshots
pnpm run render-bench -- run \
--filter rects/mixed-pans --seed 42 \
--warmups 0 --repetitions 1 --screenshots
```
Prepare a build once and reuse it:
```bash
@ -1399,6 +1458,7 @@ Attempts share evolving caches, so bootstrap intervals describe the observations
within a run. Repeat whole runs to assess reproducibility, and judge whether the
observed change matters for the workload.
## Repository map
```text

View File

@ -5,12 +5,8 @@
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; Discovery loads each scene and its transitive requires, including any
;; colocated operation functions. Stripping :run! only makes output plain
;; data; it cannot isolate browser dependencies. Keep this entire import
;; graph safe for Node. Browser adapters will supply execution capabilities in
;; rtx; discovery must never invoke operation bodies. Collection with a
;; real body present is proven by `contracts-scene/case`, not by the
;; metadata-only rects cases.
;; colocated operation functions.
;; Keep this entire import subgraph safe for Node.
(ns benches.render-wasm.cases
"Case collection for the renderer benchmarks.
@ -32,9 +28,7 @@
:params {:count 1000 :width 1920 :height 1080 :min-size 20 :max-size 100}
:view {:scale 1 :x 0 :y 0}
:context :fresh
:completion :render-full
:batch-size 1
:preparation {}}
:preparation {:render :none}}
{:id :rects/pan ...}
{:id :rects/zoom ...}]"
(:require

View File

@ -153,9 +153,6 @@
[:params [:map]]
[:view schema:view]
[:context [:enum :fresh :reuse]]
[:completion {:optional true} [:enum :call :await :render-full]]
[:batch-size {:optional true} [:int {:min 1}]]
[:operation {:optional true} [:map]]
[:preparation {:optional true}
[:map
[:render {:optional true} [:enum :full :none]]
@ -172,10 +169,7 @@
[:params [:map]]
[:view schema:view]
[:context [:enum :fresh :reuse]]
[:completion [:enum :call :await :render-full]]
[:batch-size [:int {:min 1}]]
[:preparation [:map [:render {:optional true} [:enum :full :none]]]]
[:operation {:optional true} [:map]]])
[:preparation [:map [:render {:optional true} [:enum :full :none]]]]])
(defonce registry
(atom {:scenes {}
@ -356,14 +350,16 @@
(= encoded (-> encoded t/decode-str t/encode-str))))))
(defn project-case
"Returns the case's data descriptor with defaults for completion,
batch size and preparation."
"Returns a data descriptor with the declared preparation settings.
Args:
bench-case: registered case with local executable functions.
Returns:
Case data with an explicit preparation map."
[bench-case]
(-> bench-case
(select-keys [:id :scene :params :view :context :completion :batch-size
:operation :preparation])
(update :completion #(or % :render-full))
(update :batch-size #(or % 1))
(select-keys [:id :scene :params :view :context :preparation])
(update :preparation #(dissoc (or % {}) :callback))))
(defn check-collected-case

View File

@ -19,5 +19,7 @@
{:unit :ms :description "Elapsed time of each synchronous renderer call."}
:renderer/cache-call-elapsed
{:unit :ms :description "Elapsed time of each synchronous cached renderer call."}
:renderer/prepared-scene-to-full
{:unit :ms :description "Prepared scene render request through the renderer return at Full."}
:camera/finalization-to-full
{:unit :ms :description "Camera finalization through the renderer return at Full."}})

View File

@ -57,10 +57,9 @@
(select-keys definition [:scene :scene-seed :params]))
(defn- execution-settings
"Selects operation and measurement settings that scene permission cannot ignore."
"Selects view, context and preparation settings for execution compatibility."
[definition]
(select-keys definition [:view :context :completion
:batch-size :preparation :operation]))
(select-keys definition [:view :context :preparation]))
(defn- evidence-differences
"Checks cross-run equality plus missing or varying preparation evidence."

View File

@ -5,72 +5,35 @@
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns benches.render-wasm.runtime.camera
"Control rendering and camera movement in a scene case.
"Rendering and camera commands over a prepared snapshot.
Recap: A `scene` is a recipe for shapes, and a `snapshot` is just an instance
of the scene. A `case` is what we benchmark: a set of operations executed on
a snapshot. This namespace provides operations to control rendering and camera.
Commands accept a context or its Promise and return a Promise of the next
context. Pan, zoom and restore update its view. Starred variants record raw
elapsed time and renderer evidence in the current execution phase.
## Runtime data
Load waits for an animation frame and renders through Full. Pan and zoom
submit paced cached previews, settle, then render through Full. Restore
applies a view and renders immediately with SyncTiles. Render-sync uses the
blocking renderer path. Full denotes renderer submission completion.
A view contains zoom `:scale`, offsets `:x` and `:y`, and optional viewport
settings for canvas dimensions and device pixel ratio. Hooks are functions
that supply clocks, scheduling and renderer operations. A guard returns to
authorize work or throws an error to reject it. A command context contains:
- `:case`: the case descriptor, which contains its data settings.
- `:scene`: the prepared snapshot of shapes and references.
- `:view`: the current view.
- `:hooks`: the clock, scheduling, renderer and execution guard functions.
`pan!` and `zoom!` read movement settings from `[:case :operation]`.
## Rendering and camera sessions
A frame type describes the progress of a renderer call. `Full`
(`FRAME_TYPE_FULL`) marks the renderer's completion of the request. GPU
execution and display proceed asynchronously after renderer submission.
A cached preview applies a view and draws once from cached tiles. A camera
session starts the interaction, submits interpolated views, waits for the
requested settling time, ends the interaction and renders until Full.
Each view update waits for the next animation frame and draws one cached
preview. `:settle-ms` gives the requested settling wait in milliseconds.
`render-sync` renders synchronously. `pan` receives explicit offsets, step
count and settling time, and returns the context with its updated view.
These commands accept a command context or its Promise and return a Promise
of the next context. Their measured functions, `render-sync*` and `pan*`,
require a command execution, which runs a function over a command context and
collects its measured calls. Each call records an event with the command's
metric values and optional traces. A metric names a measured quantity, such
as elapsed time.
`load!` renders an uploaded snapshot until Full. `pan!` adds `:dx` and `:dy`
to the starting offsets; `zoom!` multiplies the starting scale by `:factor`.
Both submit `:steps` interpolated views and finish a camera session. These
operations resolve to the recorded render calls and measured durations.
## Timing
Each trace entry records a render call's timestamp, flags, frame type and
duration. Camera measurements also contain cached previews, requested and
observed waits, and completion durations. `:end-ms` is the clock reading
immediately after the final renderer call returns Full.
`pan!` and `zoom!` calculate their target view before camera timing starts.
The `:interact-ms` duration runs from camera start through the final renderer
return. `pan*` measures its command body, including the target calculation,
through that same endpoint. Command input resolution and argument validation
precede the measured interval. Output validation and event recording follow
the endpoint."
Measured async commands finish at the final renderer return captured in
`:end-ms`. Input resolution and argument validation precede timing; output
validation and event recording follow it. Traces retain renderer calls,
cached previews, waits and protocol endpoints."
(:require
[app.common.schema :as sm]
[benches.render-wasm.declarations :as decl]
[benches.render-wasm.runtime.commands :refer [defcommand]]
[benches.render-wasm.runtime.protocol :as protocol]))
(defcommand render-sync
"Renders the prepared scene synchronously and returns the command context."
"Renders the prepared scene synchronously.
Args:
rtx: context or Promise of it.
Returns:
A Promise of the context after the blocking renderer call."
[rtx]
((get-in rtx [:hooks :render-sync]))
rtx)
@ -83,6 +46,35 @@
[:steps [:int {:min 1}]]
[:settle-ms [:and ::sm/safe-number [:>= 0]]]]))
(def ^:private valid-zoom?
(sm/validator
[:map {:closed true}
[:factor [:and ::sm/safe-number [:> 0]]]
[:steps [:int {:min 1}]]
[:settle-ms [:and ::sm/safe-number [:>= 0]]]]))
(def ^:private valid-view? (sm/validator decl/schema:view))
(defn- pan-view
[view dx dy]
(-> view (update :x + dx) (update :y + dy)))
(defn- zoom-view
[view factor]
(update view :scale * factor))
(defn- render-evidence
[evidence]
{:end-ms (:end-ms evidence)
:metrics {:renderer/call-elapsed (mapv :duration-ms (:slices evidence))}
:trace evidence})
(defn- camera-evidence
[evidence]
(-> (render-evidence evidence)
(assoc-in [:metrics :renderer/cache-call-elapsed] (mapv :duration-ms (:cached-slices evidence)))
(assoc-in [:metrics :camera/finalization-to-full] [(:time-to-full-ms evidence)])))
(defn- run-camera
[rtx target {:keys [steps settle-ms]}]
(-> (protocol/start-camera! rtx {:settle-ms settle-ms})
@ -92,57 +84,72 @@
(defcommand pan
"Moves the view by `:dx` and `:dy` through `:steps` cached previews.
Waits `:settle-ms` milliseconds, ends the interaction and renders until Full.
Resolves to the command context with its updated view. Steps must be positive;
the settling wait must be nonnegative."
Args:
rtx: context or Promise of it.
options: finite offsets, positive step count and nonnegative settling wait.
Returns:
A Promise of the context with its updated view."
{:async [complete fail]}
[rtx {:keys [dx dy] :as options}]
{:pre [(valid-pan? options)]}
(let [target (-> (:view rtx)
(update :x + dx)
(update :y + dy))]
{:pre [(valid-pan? options) (valid-view? (pan-view (:view rtx) dx dy))]}
(let [target (pan-view (:view rtx) dx dy)]
(-> (run-camera rtx target options)
(.then (fn [evidence]
(complete (assoc rtx :view target)
{:end-ms (:end-ms evidence)
:metrics {:renderer/call-elapsed (mapv :duration-ms (:slices evidence))
:renderer/cache-call-elapsed (mapv :duration-ms (:cached-slices evidence))
:camera/finalization-to-full [(:time-to-full-ms evidence)]}
:trace evidence})))
(complete (assoc rtx :view target) (camera-evidence evidence))))
(.catch fail))))
(defn load!
(defcommand load
"Renders the uploaded snapshot until the renderer returns Full.
Waits for an animation frame before the first render call. Returns a Promise
with recorded render calls, completion durations and the final clock reading
`:end-ms`. The context supplies clock, scheduling and renderer hooks."
Waits for an animation frame before the first renderer call.
Args:
rtx: context or Promise of it.
Returns:
A Promise of the context after renderer completion."
{:async [complete fail]}
[rtx]
(let [hooks (:hooks rtx)]
(protocol/drain hooks {:flags 0
:origin ((:now hooks))
:immediate false})))
(-> (protocol/drain hooks {:flags 0 :origin ((:now hooks)) :immediate false})
(.then (fn [evidence]
(complete rtx (assoc-in (render-evidence evidence)
[:metrics :renderer/prepared-scene-to-full] [(:full-ms evidence)]))))
(.catch fail))))
(defn pan!
"Moves the starting view by the offsets in `[:case :operation]`.
`:dx` and `:dy` give the offsets; `:steps` gives the number of interpolated
views. Each update waits for an animation frame and draws a cached preview.
After the `:settle-ms` wait, ends the interaction and renders until Full.
Returns a Promise with recorded render calls, waits and camera durations."
[rtx]
(let [view (:view rtx)
{:keys [steps dx dy settle-ms]} (get-in rtx [:case :operation])
target (-> view
(update :x + dx)
(update :y + dy))]
(run-camera rtx target {:steps steps :settle-ms settle-ms})))
(defcommand zoom
"Multiplies view scale through paced cached previews, then renders through Full.
(defn zoom!
"Changes the starting scale using settings in `[:case :operation]`.
`:factor` multiplies the scale; `:steps` gives the number of interpolated
views. Each update waits for an animation frame and draws a cached preview.
After the `:settle-ms` wait, ends the interaction and renders until Full.
Returns a Promise with recorded render calls, waits and camera durations."
[rtx]
(let [view (:view rtx)
{:keys [steps factor settle-ms]} (get-in rtx [:case :operation])
target (update view :scale * factor)]
(run-camera rtx target {:steps steps :settle-ms settle-ms})))
Args:
rtx: context or Promise of it.
options: positive finite factor, positive steps and nonnegative settle-ms.
Returns:
A Promise of the context with its updated scale."
{:async [complete fail]}
[rtx {:keys [factor] :as options}]
{:pre [(valid-zoom? options) (valid-view? (zoom-view (:view rtx) factor))]}
(let [target (zoom-view (:view rtx) factor)]
(-> (run-camera rtx target options)
(.then (fn [evidence]
(complete (assoc rtx :view target) (camera-evidence evidence))))
(.catch fail))))
(defcommand restore
"Restores a view and renders immediately through Full with SyncTiles.
Args:
rtx: context or Promise of it.
view: scale and offsets to restore. Its resolved viewport must match rtx.
Returns:
A Promise of the context with the restored view."
{:async [complete fail]}
[rtx view]
{:pre [(valid-view? view)
(= (decl/resolve-viewport view) (decl/resolve-viewport (:view rtx)))]}
(-> (protocol/restore (:hooks rtx) view)
(.then (fn [evidence]
(complete (assoc rtx :view view) (render-evidence evidence))))
(.catch fail)))

View File

@ -303,6 +303,21 @@
:active-end-ms (now) :cached-slices slices))))))
(.catch (fn [cause] (throw (partial-error cause evidence)))))))))))
(defn- interpolate
"Interpolates $v(t) = (1-t)a + tb$ between finite endpoints.
Args:
a: starting value.
b: target value.
t: interpolation fraction in [0, 1].
Returns:
The interpolated value, preserving positive endpoints near zero."
[a b t]
(if (< t 0.5)
(+ a (* t (- b a)))
(+ b (* (- 1 t) (- a b)))))
(defn animate-view!
"Moves to `to` through `steps` linearly interpolated views in sequence.
Each update waits for an animation frame and draws one cached preview.
@ -316,11 +331,14 @@
{:type ::invalid-steps :steps steps
:partial (select-keys session [:cached-slices :authored-sleeps])})))
(reduce (fn [session-p i]
(let [t (/ i steps)]
(let [t (/ i steps)
view (if (= i steps)
to
{:scale (interpolate (:scale from) (:scale to) t)
:x (interpolate (:x from) (:x to) t)
:y (interpolate (:y from) (:y to) t)})]
(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))))})))
view)))
(js/Promise.resolve session)
(range 1 (inc steps)))))))

View File

@ -42,18 +42,18 @@
(def ^:const ^:private base-view {:scale 1 :x 0 :y 0})
(decl/defcase :masks/load
{:params default-params :view base-view :context :fresh}
{:params default-params :view base-view :context :fresh :preparation {:render :none}}
[rtx]
(camera/load! rtx))
(camera/load* rtx))
(decl/defcase :masks/pan
{:params default-params :view base-view :context :reuse
:operation {:steps 20 :dx 120 :dy 40 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/pan! rtx))
(camera/pan* rtx {:steps 20 :dx 120 :dy 40 :settle-ms 100}))
(decl/defcase :masks/zoom
{:params default-params :view base-view :context :reuse
:operation {:steps 20 :factor 1.6 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/zoom! rtx))
(camera/zoom* rtx {:steps 20 :factor 1.6 :settle-ms 100}))

View File

@ -61,18 +61,18 @@
(decl/defcase :paths/load
{:params default-params :view base-view :context :fresh}
{:params default-params :view base-view :context :fresh :preparation {:render :none}}
[rtx]
(camera/load! rtx))
(camera/load* rtx))
(decl/defcase :paths/pan
{:params default-params :view base-view :context :reuse
:operation {:steps 20 :dx 160 :dy 40 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/pan! rtx))
(camera/pan* rtx {:steps 20 :dx 160 :dy 40 :settle-ms 100}))
(decl/defcase :paths/zoom
{:params default-params :view base-view :context :reuse
:operation {:steps 20 :factor 1.5 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/zoom! rtx))
(camera/zoom* rtx {:steps 20 :factor 1.5 :settle-ms 100}))

View File

@ -51,18 +51,18 @@
(def ^:private base-view {:scale 1 :x 0 :y 0})
(decl/defcase :plus/load
{:params default-params :view base-view :context :fresh}
{:params default-params :view base-view :context :fresh :preparation {:render :none}}
[rtx]
(camera/load! rtx))
(camera/load* rtx))
(decl/defcase :plus/pan
{:params default-params :view base-view :context :reuse
:operation {:steps 20 :dx 140 :dy 60 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/pan! rtx))
(camera/pan* rtx {:steps 20 :dx 140 :dy 60 :settle-ms 100}))
(decl/defcase :plus/zoom
{:params default-params :view base-view :context :reuse
:operation {:steps 20 :factor 1.4 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/zoom! rtx))
(camera/zoom* rtx {:steps 20 :factor 1.4 :settle-ms 100}))

View File

@ -77,24 +77,42 @@
(decl/defcase :rects/load
{:params default-params
:view base-view
:context :fresh}
:context :fresh
:preparation {:render :none}}
[rtx]
(camera/load! rtx))
(camera/load* rtx))
(decl/defcase :rects/pan
{:params default-params
:view base-view
:context :reuse
:completion :render-full
:operation {:steps 20 :dx 200 :dy 0 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/pan! rtx))
(camera/pan* rtx {:steps 20 :dx 200 :dy 0 :settle-ms 100}))
(decl/defcase :rects/zoom
{:params default-params
:view base-view
:context :reuse
:completion :render-full
:operation {:steps 20 :factor 1.5 :settle-ms 100}}
:reset #(camera/restore % (get-in % [:case :view]))}
[rtx]
(camera/zoom! rtx))
(camera/zoom* rtx {:steps 20 :factor 1.5 :settle-ms 100}))
(decl/defcase :rects/blocking-full
{:params default-params
:view base-view
:context :fresh
:preparation {:render :none}}
[rtx]
(camera/render-sync* rtx))
(decl/defcase :rects/mixed-pans
{:params default-params
:view base-view
:context :reuse
:reset #(camera/restore* % (get-in % [:case :view]))}
[rtx]
(-> rtx
(camera/pan* {:dx 120 :dy 0 :steps 12 :settle-ms 50})
(camera/pan {:dx 0 :dy 80 :steps 8 :settle-ms 50})
(camera/pan* {:dx -120 :dy 0 :steps 12 :settle-ms 50})))

View File

@ -525,8 +525,6 @@
:params {}
:view {:scale 1 :x 0 :y 0}
:context :fresh
:completion :render-full
:batch-size 1
:preparation {}})
(defn- write-catalog

View File

@ -391,10 +391,6 @@ function installPreparedCompiler(fixture, breakLog) {
m([k("scale"), 1, k("x"), 0, k("y"), 0]),
k("context"),
k("fresh"),
k("completion"),
k("render-full"),
k("batch-size"),
1,
k("preparation"),
m([]),
]);
@ -450,8 +446,8 @@ function runFixtureText({
sceneSeed = 7,
count = 10,
fingerprint = "a".repeat(64),
operation = "load",
operationArgs = { distance: 10 },
view = { scale: 1, x: 0, y: 0 },
context = "fresh",
warmups = 0,
repetitions = 1,
gitSha = runId,
@ -531,18 +527,12 @@ function runFixtureText({
sceneSeed,
k("params"),
map([k("count"), count]),
k("operation"),
map([
k("id"),
k(operation),
k("args"),
map(
Object.entries(operationArgs).flatMap(([key, entry]) => [
k(key),
entry,
]),
),
]),
k("view"),
map(Object.entries(view).flatMap(([key, entry]) => [k(key), entry])),
k("context"),
k(context),
k("preparation"),
map([]),
]),
],
k("warmups"),
@ -1646,7 +1636,7 @@ test(
);
test(
"scoped compare permissions keep operation arguments and runtime environment strict",
"scoped compare permissions keep execution settings and runtime environment strict",
testOptions,
async (t) => {
const fixture = createFrontend(t);
@ -1657,8 +1647,8 @@ test(
runFixtureText({ runId: "baseline", caseId: "case/load", value: 10 }),
);
for (const [field, changes] of [
["execution", { operation: "pan" }],
["execution", { operationArgs: { distance: 20 } }],
["execution", { context: "reuse" }],
["execution", { view: { scale: 2, x: 0, y: 0 } }],
["environment", { environment: { node: "v25" } }],
]) {
fs.writeFileSync(

View File

@ -7,7 +7,10 @@
(ns performance-tests.render-wasm.commands-test
(:require
[app.common.render-wasm.wasm :as wasm]
[app.common.schema :as sm]
[benches.render-wasm.builder :as builder]
[benches.render-wasm.cases :as cases]
[benches.render-wasm.declarations :as decl]
[benches.render-wasm.errors :as errors]
[benches.render-wasm.runtime.camera :as camera]
[benches.render-wasm.runtime.commands :as cmd :refer [defcommand]]
@ -299,8 +302,12 @@
(t/is (= [{:command ::camera/render-sync :metrics {:command/elapsed [7]}}]
(:events next-run)))))
(t/deftest ^:async pan-commands-return-the-view-and-renderer-evidence
(doseq [command [camera/pan camera/pan*]]
(t/deftest ^:async camera-gestures-return-the-view-and-renderer-evidence
(doseq [[command options target command-id measured]
[[camera/pan {:dx 10 :dy 5 :steps 2 :settle-ms 5} {:scale 1 :x 10 :y 5} ::camera/pan false]
[camera/pan* {:dx 10 :dy 5 :steps 2 :settle-ms 5} {:scale 1 :x 10 :y 5} ::camera/pan true]
[camera/zoom {:factor 2 :steps 2 :settle-ms 5} {:scale 2 :x 0 :y 0} ::camera/zoom false]
[camera/zoom* {:factor 2 :steps 2 :settle-ms 5} {:scale 2 :x 0 :y 0} ::camera/zoom true]]]
(let [clock (volatile! 0)
view (volatile! nil)
remaining (volatile! [wasm/FRAME_TYPE_PARTIAL wasm/FRAME_TYPE_FULL])
@ -313,14 +320,13 @@
(when (= wasm/FRAME_TYPE_FULL frame-type)
(js/queueMicrotask #(vswap! clock + 500)))
frame-type))})
options {:dx 10 :dy 5 :steps 2 :settle-ms 5}
result (await (cmd/execute rtx #(command % options)))]
(t/is (= :completed (:outcome result)))
(t/is (= {:scale 1 :x 10 :y 5} @view (get-in result [:context :view])))
(t/is (= target @view (get-in result [:context :view])))
(t/is (= 574 @clock))
(if (= command camera/pan*)
(if measured
(do
(t/is (= ::camera/pan (get-in result [:events 0 :command])))
(t/is (= command-id (get-in result [:events 0 :command])))
(t/is (= {:command/elapsed [74]
:renderer/call-elapsed [7 7]
:renderer/cache-call-elapsed [2 2]
@ -335,6 +341,88 @@
(t/is (= [] (:events invalid)))
(t/is (zero? @clock))))))
(t/deftest ^:async load-and-restore-finish-at-the-renderer-endpoint
(doseq [[command restore measured] [[camera/load false false] [camera/load* false true]
[camera/restore true false] [camera/restore* true true]]]
(let [clock (volatile! 0)
view (volatile! nil)
target {:scale 2 :x 30 :y -10}
rtx (update (context clock) :hooks merge
{:set-view #(vreset! view %)
:render (fn [_ _]
(vswap! clock + 7)
(js/queueMicrotask #(vswap! clock + 500))
wasm/FRAME_TYPE_FULL)})
result (await (cmd/execute rtx #(if restore
(command (js/Promise.resolve %) target)
(command (js/Promise.resolve %)))))]
(t/is (= :completed (:outcome result)))
(t/is (= (if restore target (:view rtx)) (get-in result [:context :view])))
(when restore (t/is (= target @view)))
(t/is (= (if restore 510 523) @clock))
(when measured
(let [event (first (:events result))]
(t/is (= (if restore ::camera/restore ::camera/load) (:command event)))
(t/is (= [(if restore 10 23)] (get-in event [:metrics :command/elapsed])))
(t/is (= [7] (get-in event [:metrics :renderer/call-elapsed])))
(t/is (= (if restore 10 23) (get-in event [:trace :end-ms])))
(when-not restore
(t/is (= [23] (get-in event [:metrics :renderer/prepared-scene-to-full])))))))))
(t/deftest ^:async invalid-camera-targets-fail-before-changing-renderer-state
(doseq [[command options view]
[[camera/pan* {:dx sm/max-safe-int :dy 0 :steps 1 :settle-ms 0} {:scale 1 :x 1 :y 0}]
[camera/zoom* {:factor sm/max-safe-int :steps 1 :settle-ms 0} {:scale 2 :x 0 :y 0}]
[camera/restore* {:scale 1 :x 10 :y 0 :viewport {:width 800}} {:scale 1 :x 0 :y 0}]]]
(let [clock (volatile! 0)
applied (volatile! view)
rtx (-> (context clock)
(assoc :view view)
(assoc-in [:hooks :set-view] #(vreset! applied %)))
result (await (cmd/execute rtx #(command % options)))]
(t/is (= :failed (:outcome result)))
(t/is (= ::cmd/invalid-arguments (:type (ex-data (:error result)))))
(t/is (= view @applied))
(t/is (zero? @clock)))))
(t/deftest ^:async declared-reference-cases-execute-selected-work-and-reset-the-view
(let [clock (volatile! 0)
descriptor (first (cases/collect-cases {:master-seed 42 :filter "rects/blocking-full"}))
result (await (cmd/execute (assoc (context clock) :case descriptor)
(:run! (decl/registered-case (:id descriptor)))))]
(t/is (= :completed (:outcome result)))
(t/is (= [{:command ::camera/render-sync :metrics {:command/elapsed [7]}}] (:events result))))
(let [clock (volatile! 0)
descriptor (first (cases/collect-cases {:master-seed 42 :filter "rects/mixed-pans"}))
registered (decl/registered-case (:id descriptor))
applied (volatile! (:view descriptor))
initial (-> (context clock) (assoc :case descriptor)
(assoc-in [:hooks :set-view] #(vreset! applied %)))]
(loop [rtx initial remaining 2]
(when (pos? remaining)
(let [main (await (cmd/execute rtx (:run! registered)))
_ (t/is (= {:scale 1 :x 0 :y 80} @applied (get-in main [:context :view])))
reset (await (cmd/execute (:context main) (:reset registered)))]
(t/is (= :completed (:outcome main) (:outcome reset)))
(t/is (= [::camera/pan ::camera/pan] (mapv :command (:events main))))
(t/is (= [::camera/restore] (mapv :command (:events reset))))
(t/is (= (:view descriptor) @applied (get-in reset [:context :view])))
(recur (:context reset) (dec remaining)))))))
(t/deftest ^:async small-positive-zoom-scales-stay-positive-and-reach-the-target
(doseq [[scale factor] [[1 1e-20] [js/Number.MIN_VALUE 1]]]
(let [applied (volatile! {:scale scale :x 0 :y 0})
minimum (volatile! scale)
rtx (-> (context (volatile! 0))
(assoc :view @applied)
(assoc-in [:hooks :set-view]
#(do (vreset! applied %)
(vswap! minimum min (:scale %)))))
result (await (cmd/execute rtx #(camera/zoom* % {:factor factor :steps 2 :settle-ms 0})))]
(t/is (= :completed (:outcome result)))
(t/is (pos? @minimum))
(t/is (= (* scale factor) (:scale @applied) (get-in result [:context :view :scale]))))))
(t/deftest ^:async failed-work-retains-earlier-observations
(let [unhandled (atom [])
observe #(swap! unhandled conj %)]

View File

@ -39,8 +39,6 @@
:params {:count 10}
:view {:scale 1 :x 0 :y 0}
:context :fresh
:completion :render-full
:batch-size 1
:preparation {}})
(defn- make-run
@ -214,11 +212,11 @@
(finally
(log/configure {})))))
(t/deftest scoped-permissions-keep-operation-and-runtime-checks-strict
(t/deftest scoped-permissions-keep-view-and-runtime-checks-strict
(let [baseline (write-run (path/join @temporary-root "baseline.transit")
(make-run "baseline" :case/load 10))]
(doseq [[field settings]
[[:execution {:case-settings {:operation {:id :pan :args {:distance 20}}}}]
[[:execution {:case-settings {:view {:scale 2 :x 0 :y 0}}}]
[:environment {:environment {:node "v25" :browser "Fake"}}]]]
(let [candidate (write-run (path/join @temporary-root "candidate.transit")
(apply make-run "candidate" :case/load 12 (mapcat identity settings)))

View File

@ -80,29 +80,19 @@
(t/deftest rects-cases-are-collected-in-declaration-order
(let [collected (rect-cases)]
(t/is (= [:rects/load :rects/pan :rects/zoom] (mapv :id collected)))))
(t/deftest real-rectangle-bodies-stay-out-of-node-descriptors
(let [ids [:rects/load :rects/pan :rects/zoom]]
(t/is (every? #(fn? (:run! (decl/registered-case %))) ids))
(t/is (= ids (mapv :id (rect-cases)))
"collection succeeds with the executable bodies present")
(t/is (every? #(not (contains? % :run!)) (rect-cases)))))
(t/is (= [:rects/load :rects/pan :rects/zoom :rects/blocking-full :rects/mixed-pans] (mapv :id collected)))))
(t/deftest collected-cases-survive-the-wire
(doseq [scene-id [:rects :paths :plus :masks]]
(let [collected (cases/collect-cases {:master-seed 42
:filter (str (name scene-id) "/")})
decoded (transit/decode-str (transit/encode-str collected))]
(t/is (= (mapv #(keyword (name scene-id) %) ["load" "pan" "zoom"])
(mapv :id collected)))
(t/is (= collected decoded))
(doseq [case-desc decoded]
(t/is (= case-desc (decl/check-collected-case case-desc)))
(t/is (not-any? #(contains? case-desc %)
[:run! :ns])
(str (:id case-desc)))
(t/is (= {} (:preparation case-desc)) (str (:id case-desc)))))))
(str (:id case-desc)))))))
(t/deftest declared-preparation-survives-collection-and-wire
(with-synthetic-scene :contracts-prepared {:count 3}
@ -122,18 +112,18 @@
(t/is (= ::cases/invalid-case
(:type (failure-data #(cases/collect-cases {:master-seed 42 :filter "contracts-prepared/"})))))))))
(t/deftest rects-cases-carry-declared-view-context-and-completion
(t/deftest rects-cases-carry-declared-view-context-and-preparation
(let [collected (rect-cases)]
(t/is (= [[:rects/load :fresh] [:rects/pan :reuse] [:rects/zoom :reuse]]
(mapv (juxt :id :context) collected)))
(t/is (= {:rects/load :fresh :rects/pan :reuse :rects/zoom :reuse
:rects/blocking-full :fresh :rects/mixed-pans :reuse}
(into {} (map (juxt :id :context)) collected)))
(doseq [case-desc collected]
(t/is (= {:count 1000 :width 1920 :height 1080 :min-size 20 :max-size 100}
(:params case-desc))
(str (:id case-desc)))
(t/is (= {:scale 1 :x 0 :y 0} (:view case-desc)) (str (:id case-desc)))
(t/is (= :render-full (:completion case-desc)) (str (:id case-desc)))
(t/is (= 1 (:batch-size case-desc)) (str (:id case-desc)))
(t/is (= {} (:preparation case-desc)) (str (:id case-desc))))))
(t/is (= (if (#{:rects/load :rects/blocking-full} (:id case-desc)) {:render :none} {})
(:preparation case-desc)) (str (:id case-desc))))))
(t/deftest rects-cases-carry-scene-metadata-and-seed
(let [collected (rect-cases)]
@ -218,7 +208,7 @@
(t/deftest transit-wire-preserves-non-finite-numbers
(let [value {:nan js/NaN :positive js/Infinity :negative js/-Infinity}
decoded (-> value transit/encode-str transit/decode-str)
case-desc (assoc (first (rect-cases)) :operation value)]
case-desc (assoc (first (rect-cases)) :params value)]
(t/is (number? (:nan decoded)))
(t/is (js/Number.isNaN (:nan decoded)))
(t/is (= js/Infinity (:positive decoded)))
@ -228,8 +218,8 @@
(t/deftest unencodable-collected-values-are-rejected
(let [good (first (rect-cases))]
(doseq [bad [(assoc good :operation {:run (fn [] 1)})
(assoc good :operation {:point (->Point 1 2)})]]
(doseq [bad [(assoc good :params {:run (fn [] 1)})
(assoc good :params {:point (->Point 1 2)})]]
(t/is (= ::decl/non-serializable-case
(:type (failure-data #(decl/check-collected-case bad))))
(pr-str bad)))))
@ -419,7 +409,7 @@
(t/deftest filter-matches-case-ids
(t/is (= [:rects/pan]
(mapv :id (cases/collect-cases {:master-seed 42 :filter "rects/pan"}))))
(t/is (= 3 (count (cases/collect-cases {:master-seed 42 :filter "rects/"})))))
(t/is (= 5 (count (cases/collect-cases {:master-seed 42 :filter "rects/"})))))
(t/deftest empty-selection-is-rejected
(t/is (= ::cases/no-cases

View File

@ -190,8 +190,6 @@
(doseq [id [:feature-plan-fixture/second :feature-plan-fixture/first]]
(decl/register-case! {:id id :scene :feature-plan-fixture :ns "feature-comparison-test"
:params {:count 17} :view {:scale 2 :x 10 :y -5} :context :reuse
:completion :await :batch-size 7
:operation {:id :custom/work :args {:amount 3}}
:preparation {:reset :outside-timing :data [1 2]}
:run! (fn [_] (throw (js/Error. "planning must not run an operation")))}))
(try
@ -201,9 +199,8 @@
(t/is (= [:feature-plan-fixture/second :feature-plan-fixture/first] (mapv :id data)))
(t/is (= (cases/collect-cases {:master-seed 42 :filter "feature-plan-fixture/"}) data))
(t/is (= {:params {:count 17} :view {:scale 2 :x 10 :y -5} :context :reuse
:completion :await :batch-size 7 :operation {:id :custom/work :args {:amount 3}}
:preparation {:reset :outside-timing :data [1 2]}}
(select-keys (first data) [:params :view :context :completion :batch-size :operation :preparation]))))
(select-keys (first data) [:params :view :context :preparation]))))
(finally
(decl/unregister-case! :feature-plan-fixture/second)
(decl/unregister-case! :feature-plan-fixture/first)

View File

@ -23,8 +23,6 @@
:params {:count 10}
:view {:scale 1 :x 0 :y 0}
:context :fresh
:completion :render-full
:batch-size 1
:preparation {}})
(defn- preparation

View File

@ -68,8 +68,6 @@
:params {}
:view {:scale 1 :x 0 :y 0}
:context context
:completion :render-full
:batch-size 1
:preparation {}})
(defn- next-count