From e4709287dd22a462fa5b4c5e6ce032d47e32ff23 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Thu, 5 Nov 2026 10:07:07 +0000 Subject: [PATCH] :sparkles: 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 --- .../performance/benches/render_wasm/README.md | 240 +++++++++++------- .../benches/render_wasm/cases.cljs | 12 +- .../benches/render_wasm/declarations.cljc | 22 +- .../benches/render_wasm/quantities.cljs | 2 + .../benches/render_wasm/report/compare.cljs | 5 +- .../benches/render_wasm/runtime/camera.cljs | 207 +++++++-------- .../benches/render_wasm/runtime/protocol.cljs | 26 +- .../benches/render_wasm/scenes/masks.cljs | 12 +- .../benches/render_wasm/scenes/paths.cljs | 12 +- .../benches/render_wasm/scenes/plus.cljs | 12 +- .../benches/render_wasm/scenes/rects.cljs | 34 ++- .../render_wasm/artifacts_test.cljs | 2 - .../render_wasm/cli_integration.cjs | 32 +-- .../render_wasm/commands_test.cljs | 100 +++++++- .../render_wasm/compare_test.cljs | 6 +- .../render_wasm/contracts_test.cljs | 34 +-- .../render_wasm/feature_comparison_test.cljs | 5 +- .../render_wasm/merge_test.cljs | 2 - .../render_wasm/run_test.cljs | 2 - 19 files changed, 459 insertions(+), 308 deletions(-) diff --git a/frontend/performance/benches/render_wasm/README.md b/frontend/performance/benches/render_wasm/README.md index 844fef3e01..aa9f373d8c 100644 --- a/frontend/performance/benches/render_wasm/README.md +++ b/frontend/performance/benches/render_wasm/README.md @@ -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 diff --git a/frontend/performance/benches/render_wasm/cases.cljs b/frontend/performance/benches/render_wasm/cases.cljs index fd77b8918d..e995554bae 100644 --- a/frontend/performance/benches/render_wasm/cases.cljs +++ b/frontend/performance/benches/render_wasm/cases.cljs @@ -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 diff --git a/frontend/performance/benches/render_wasm/declarations.cljc b/frontend/performance/benches/render_wasm/declarations.cljc index c0134531e6..1c59d30f6e 100644 --- a/frontend/performance/benches/render_wasm/declarations.cljc +++ b/frontend/performance/benches/render_wasm/declarations.cljc @@ -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 diff --git a/frontend/performance/benches/render_wasm/quantities.cljs b/frontend/performance/benches/render_wasm/quantities.cljs index 79fe16c1f4..ea114106ed 100644 --- a/frontend/performance/benches/render_wasm/quantities.cljs +++ b/frontend/performance/benches/render_wasm/quantities.cljs @@ -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."}}) diff --git a/frontend/performance/benches/render_wasm/report/compare.cljs b/frontend/performance/benches/render_wasm/report/compare.cljs index f6696ba5a7..904ebb48b3 100644 --- a/frontend/performance/benches/render_wasm/report/compare.cljs +++ b/frontend/performance/benches/render_wasm/report/compare.cljs @@ -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." diff --git a/frontend/performance/benches/render_wasm/runtime/camera.cljs b/frontend/performance/benches/render_wasm/runtime/camera.cljs index 24974b22ba..dec26b01ae 100644 --- a/frontend/performance/benches/render_wasm/runtime/camera.cljs +++ b/frontend/performance/benches/render_wasm/runtime/camera.cljs @@ -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))) diff --git a/frontend/performance/benches/render_wasm/runtime/protocol.cljs b/frontend/performance/benches/render_wasm/runtime/protocol.cljs index 5539ee36a5..84ed08f9ba 100644 --- a/frontend/performance/benches/render_wasm/runtime/protocol.cljs +++ b/frontend/performance/benches/render_wasm/runtime/protocol.cljs @@ -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))))))) diff --git a/frontend/performance/benches/render_wasm/scenes/masks.cljs b/frontend/performance/benches/render_wasm/scenes/masks.cljs index 5eaa23e75d..8deb1c4cbb 100644 --- a/frontend/performance/benches/render_wasm/scenes/masks.cljs +++ b/frontend/performance/benches/render_wasm/scenes/masks.cljs @@ -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})) diff --git a/frontend/performance/benches/render_wasm/scenes/paths.cljs b/frontend/performance/benches/render_wasm/scenes/paths.cljs index 0cd50f6a5f..d96382ad30 100644 --- a/frontend/performance/benches/render_wasm/scenes/paths.cljs +++ b/frontend/performance/benches/render_wasm/scenes/paths.cljs @@ -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})) diff --git a/frontend/performance/benches/render_wasm/scenes/plus.cljs b/frontend/performance/benches/render_wasm/scenes/plus.cljs index 1752be892e..d4021bcd85 100644 --- a/frontend/performance/benches/render_wasm/scenes/plus.cljs +++ b/frontend/performance/benches/render_wasm/scenes/plus.cljs @@ -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})) diff --git a/frontend/performance/benches/render_wasm/scenes/rects.cljs b/frontend/performance/benches/render_wasm/scenes/rects.cljs index b7a3480e25..6e8ec5b83c 100644 --- a/frontend/performance/benches/render_wasm/scenes/rects.cljs +++ b/frontend/performance/benches/render_wasm/scenes/rects.cljs @@ -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}))) diff --git a/frontend/test/performance_tests/render_wasm/artifacts_test.cljs b/frontend/test/performance_tests/render_wasm/artifacts_test.cljs index c5e73ada4e..7dce7361c6 100644 --- a/frontend/test/performance_tests/render_wasm/artifacts_test.cljs +++ b/frontend/test/performance_tests/render_wasm/artifacts_test.cljs @@ -525,8 +525,6 @@ :params {} :view {:scale 1 :x 0 :y 0} :context :fresh - :completion :render-full - :batch-size 1 :preparation {}}) (defn- write-catalog diff --git a/frontend/test/performance_tests/render_wasm/cli_integration.cjs b/frontend/test/performance_tests/render_wasm/cli_integration.cjs index a411e58f0c..0ed796f27c 100644 --- a/frontend/test/performance_tests/render_wasm/cli_integration.cjs +++ b/frontend/test/performance_tests/render_wasm/cli_integration.cjs @@ -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( diff --git a/frontend/test/performance_tests/render_wasm/commands_test.cljs b/frontend/test/performance_tests/render_wasm/commands_test.cljs index af07feaed0..b236a684ef 100644 --- a/frontend/test/performance_tests/render_wasm/commands_test.cljs +++ b/frontend/test/performance_tests/render_wasm/commands_test.cljs @@ -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 %)] diff --git a/frontend/test/performance_tests/render_wasm/compare_test.cljs b/frontend/test/performance_tests/render_wasm/compare_test.cljs index 4281bad296..7809f03713 100644 --- a/frontend/test/performance_tests/render_wasm/compare_test.cljs +++ b/frontend/test/performance_tests/render_wasm/compare_test.cljs @@ -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))) diff --git a/frontend/test/performance_tests/render_wasm/contracts_test.cljs b/frontend/test/performance_tests/render_wasm/contracts_test.cljs index 133ce83dcc..0804f0e388 100644 --- a/frontend/test/performance_tests/render_wasm/contracts_test.cljs +++ b/frontend/test/performance_tests/render_wasm/contracts_test.cljs @@ -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 diff --git a/frontend/test/performance_tests/render_wasm/feature_comparison_test.cljs b/frontend/test/performance_tests/render_wasm/feature_comparison_test.cljs index ea4356d89e..a1a46e3b09 100644 --- a/frontend/test/performance_tests/render_wasm/feature_comparison_test.cljs +++ b/frontend/test/performance_tests/render_wasm/feature_comparison_test.cljs @@ -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) diff --git a/frontend/test/performance_tests/render_wasm/merge_test.cljs b/frontend/test/performance_tests/render_wasm/merge_test.cljs index 7d3dd74442..8e4ee8e042 100644 --- a/frontend/test/performance_tests/render_wasm/merge_test.cljs +++ b/frontend/test/performance_tests/render_wasm/merge_test.cljs @@ -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 diff --git a/frontend/test/performance_tests/render_wasm/run_test.cljs b/frontend/test/performance_tests/render_wasm/run_test.cljs index 4033eda45a..7519d4b4b2 100644 --- a/frontend/test/performance_tests/render_wasm/run_test.cljs +++ b/frontend/test/performance_tests/render_wasm/run_test.cljs @@ -68,8 +68,6 @@ :params {} :view {:scale 1 :x 0 :y 0} :context context - :completion :render-full - :batch-size 1 :preparation {}}) (defn- next-count