mirror of
https://github.com/penpot/penpot.git
synced 2026-11-06 10:00:38 +00:00
✨ 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:
parent
6fa70dc47d
commit
e4709287dd
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."}})
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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)))))))
|
||||
|
||||
|
||||
@ -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}))
|
||||
|
||||
@ -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}))
|
||||
|
||||
@ -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}))
|
||||
|
||||
@ -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})))
|
||||
|
||||
@ -525,8 +525,6 @@
|
||||
:params {}
|
||||
:view {:scale 1 :x 0 :y 0}
|
||||
:context :fresh
|
||||
:completion :render-full
|
||||
:batch-size 1
|
||||
:preparation {}})
|
||||
|
||||
(defn- write-catalog
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 %)]
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -68,8 +68,6 @@
|
||||
:params {}
|
||||
:view {:scale 1 :x 0 :y 0}
|
||||
:context context
|
||||
:completion :render-full
|
||||
:batch-size 1
|
||||
:preparation {}})
|
||||
|
||||
(defn- next-count
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user