mirror of
https://github.com/penpot/penpot.git
synced 2026-11-05 17:40:39 +00:00
✨ Integrate renderer benchmark case phases
- Run preparation, main and reset through the shared command executor. - Refactor preparation into named operations and pure data projections. - Fully document the browser bridge. AI-assisted-by: gpt-6.1-sol
This commit is contained in:
parent
c369753949
commit
0bd3fcd679
@ -61,8 +61,8 @@ The suite has three parts:
|
||||
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 runtime context with the case data, scene snapshot, renderer
|
||||
module, view, and hooks. Standard operations load, pan, and zoom.
|
||||
It receives a command context with the case data, scene snapshot, current
|
||||
view, and hooks. Standard operations load, pan, and zoom.
|
||||
- **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.
|
||||
@ -90,9 +90,9 @@ The suite has three parts:
|
||||
context, snapshot, and upload serve all attempts of the case.
|
||||
- **Preparation**: setup for one graphics context. It covers module
|
||||
instantiation, WebGL2 context creation, scene build, snapshot validation,
|
||||
fingerprinting, upload-order derivation, and upload. A warm preparation also
|
||||
restores the starting view and drains to `Full`. One page owns one
|
||||
preparation.
|
||||
fingerprinting, upload-order derivation, and upload. It then runs the optional
|
||||
preparation callback and drains to `Full` unless the case declares
|
||||
`:preparation {:render :none}`. One page owns one preparation.
|
||||
- **Attempt**: one execution of a case against a preparation. An attempt
|
||||
records one outcome, one set of metrics, and a raw trace. A failed attempt
|
||||
still consumes its allocated slot.
|
||||
@ -234,7 +234,7 @@ The browser bridge owns everything inside the page:
|
||||
- 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 restores the starting view outside timing for warm cases;
|
||||
- it carries each phase's returned context into the next phase or attempt;
|
||||
- it runs the case operation and records the requested measurements;
|
||||
- it returns plain data and disposes the context when asked.
|
||||
|
||||
@ -264,7 +264,7 @@ sequenceDiagram
|
||||
B->>B: build, validate, fingerprint the snapshot
|
||||
B->>W: upload shapes in tree order
|
||||
B->>W: render until Full
|
||||
B-->>N: metrics, slices, and preparation evidence
|
||||
B-->>N: preparation evidence and raw phase blocks
|
||||
N->>N: persist the run record
|
||||
```
|
||||
|
||||
@ -378,10 +378,10 @@ derives the scene from that keyword namespace and attaches the run function.
|
||||
{:params default-params
|
||||
:view {:scale 1 :x 0 :y 0}
|
||||
:context :reuse
|
||||
:completion :render-full
|
||||
:operation {:steps 20 :dx 200 :dy 0 :settle-ms 100}}
|
||||
[rtx]
|
||||
(camera/pan! rtx))
|
||||
:reset (fn [rtx]
|
||||
(camera/pan rtx {:steps 20 :dx -200 :dy 0 :settle-ms 100}))}
|
||||
[rtx]
|
||||
(camera/pan* rtx {:steps 20 :dx 200 :dy 0 :settle-ms 100}))
|
||||
```
|
||||
|
||||
The case declares:
|
||||
@ -393,14 +393,15 @@ The case declares:
|
||||
- `:context`: `:fresh` creates a page, module, and context per attempt.
|
||||
`:reuse` keeps one page, module, context, snapshot, and upload for all
|
||||
attempts of the case.
|
||||
- `:completion`: the declared completion rule, `:call`, `:await`, or
|
||||
`:render-full`. The standard load body drains to `Full`; the standard pan and
|
||||
zoom bodies finish a camera session with a final `Full` drain.
|
||||
- `:operation`: settings read by the operation body.
|
||||
- `:batch-size` and `:preparation`: execution settings recorded in the
|
||||
descriptor and checked by comparisons. A batch size groups inner calls inside
|
||||
one attempt; a repetition is a separate attempt. Standard load, pan, and zoom
|
||||
cases run one call per attempt.
|
||||
- `:preparation`: an optional `:callback` function runs after upload and returns
|
||||
the next context or its Promise. Preparation then renders Full by default.
|
||||
`:render :none` completes preparation after the callback for cold-render cases.
|
||||
- `:reset`: an optional function that receives the context after successful
|
||||
main execution and returns the next context or its Promise. Reset runs after
|
||||
every successful main, including warmups and the final attempt.
|
||||
|
||||
Commands receive workload arguments from the case body. The callback, main body,
|
||||
and reset share the command context and can each call measured commands.
|
||||
|
||||
### Case collection and seeds
|
||||
|
||||
@ -410,6 +411,10 @@ the case's data fields, and checks that the descriptor survives a serialization
|
||||
round trip. Shape construction, browser startup, and renderer initialization
|
||||
take place during execution.
|
||||
|
||||
The local registry keeps the main, reset, and preparation callback functions.
|
||||
The descriptor carries the declared preparation data. The browser resolves the
|
||||
functions from its registry using the case id.
|
||||
|
||||
The scene seed depends only on the master seed, the scene id, and the canonical
|
||||
parameters. These inputs give load, pan, and zoom cases the same starting
|
||||
content across case bodies, collection orders, attempt counts, and feature
|
||||
@ -441,11 +446,9 @@ shape.
|
||||
| `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 |
|
||||
|
||||
A load case renders a fresh scene and times module start, graphics start,
|
||||
upload, and the first render to `Full`. A pan or zoom case prepares the scene
|
||||
once, restores the starting view outside timing for each attempt, then times a
|
||||
camera session. The standard camera sessions use 20 view updates and a 100 ms
|
||||
settling wait. Pan and zoom parameters vary by scene.
|
||||
Scene recipes supply canonical geometry for rendering and camera workloads.
|
||||
Camera sessions submit view updates and wait for settling before their final
|
||||
render. Each case declares its sequence, arguments, and reset behavior in code.
|
||||
|
||||
## Renderer commands
|
||||
|
||||
@ -629,6 +632,39 @@ the individual values that call produced. The command execution owns this
|
||||
history; its guard authorizes work for that execution. Pass the resulting
|
||||
context to `commands/execute` to start another command execution.
|
||||
|
||||
### Case phases
|
||||
|
||||
The browser executes preparation, main, and reset through the command executor.
|
||||
Preparation builds and uploads the scene, runs its optional callback once, and
|
||||
renders Full unless the case declares `:preparation {:render :none}`. A fresh
|
||||
context prepares once for its attempt; a reused context prepares once for its
|
||||
case. Each phase passes its returned context to the next phase.
|
||||
|
||||
A successful preparation response contains its raw block in
|
||||
`[:preparation :phases]`. A failed preparation returns its available blocks in
|
||||
top-level `:phases`. An attempt response contains main and any declared reset
|
||||
in `:phases`:
|
||||
|
||||
```clojure
|
||||
{:phase :main
|
||||
:preparation-id "preparation-1"
|
||||
:outcome :completed
|
||||
:events [{:command :benches.render-wasm.runtime.camera/render-sync
|
||||
:metrics {:command/elapsed [7]}}]}
|
||||
```
|
||||
|
||||
Each block retains ordered command events, raw numeric vectors, and any
|
||||
completion diagnostics. A phase can complete with an empty event vector.
|
||||
Initialization and upload durations stay in preparation `:diagnostics`.
|
||||
Automatic initial rendering retains its raw trace under
|
||||
`[:diagnostics :setup-render]`. Measured commands in the callback supply the
|
||||
preparation block's selected events.
|
||||
|
||||
Reset runs after successful main. Its returned context supplies the next reused
|
||||
attempt. A phase failure releases its owned context and returns the failed
|
||||
block with earlier events and available traces. Completed preparation retains
|
||||
its own evidence when main or reset fails.
|
||||
|
||||
### Runtime code
|
||||
|
||||
[`runtime/commands.clj`](runtime/commands.clj) declares commands;
|
||||
@ -643,7 +679,8 @@ declarations and functions during catalogue collection. Renderer operations 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
|
||||
cases restore their starting view outside interaction timing before each attempt.
|
||||
cases carry their returned context into the next attempt; their reset function
|
||||
controls restoration.
|
||||
|
||||
## Measurement
|
||||
|
||||
@ -659,16 +696,18 @@ Work around those intervals includes:
|
||||
- request and result serialization;
|
||||
- validation, logging, and screenshots;
|
||||
- module script import;
|
||||
- camera restore and the drain that prepares a warm case.
|
||||
- the automatic initial Full render during preparation.
|
||||
|
||||
The timed work includes:
|
||||
Preparation diagnostics time:
|
||||
|
||||
- module instantiation;
|
||||
- graphics context creation and initial size synchronization;
|
||||
- shape batch upload, including the loading lifecycle and first view setup;
|
||||
- the render drain to `Full`;
|
||||
- for warm cases, the camera session: view updates, cached previews, settling,
|
||||
ending the interaction, and the final drain.
|
||||
- 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,
|
||||
ending the interaction, and the final drain.
|
||||
|
||||
### Lifecycle of a fresh load
|
||||
|
||||
@ -679,22 +718,25 @@ flowchart TD
|
||||
C --> D[Build and validate snapshot - untimed]
|
||||
D --> E[Fingerprint and derive upload order - untimed]
|
||||
E --> F[Time upload and first view setup]
|
||||
F --> G[Time render drain to Full]
|
||||
G --> H[Record result]
|
||||
H --> I[Dispose page and context]
|
||||
F --> G[Run preparation callback]
|
||||
G --> H[Render initial Full unless render is none]
|
||||
H --> I[Run main]
|
||||
I --> J[Run reset after successful main]
|
||||
J --> K[Return raw phase records]
|
||||
K --> L[Dispose page and context]
|
||||
```
|
||||
|
||||
The module script import is untimed because the browser loads it once per page.
|
||||
Module instantiation starts a new renderer state. Graphics start creates the
|
||||
WebGL2 context and synchronizes the drawing buffer. Upload covers the loading
|
||||
lifecycle, batch serialization, and tile preparation. The first render clock
|
||||
starts after upload ends.
|
||||
lifecycle, batch serialization, and tile preparation. Each measured command
|
||||
starts its clock immediately before its work.
|
||||
|
||||
### Warm interactions
|
||||
|
||||
A warm case owns one prepared page. Before each attempt, the browser restores
|
||||
the case's starting view and drains to `Full` outside timing. The operation then
|
||||
runs a camera session:
|
||||
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:
|
||||
|
||||
1. start the camera interaction;
|
||||
2. for each view update, wait for the next animation frame, apply the view, and
|
||||
@ -711,19 +753,18 @@ the run also records that call's duration.
|
||||
|
||||
### Slices and metrics
|
||||
|
||||
The browser returns raw evidence for each attempt. A fresh attempt also reports
|
||||
`module-init-ms`, `graphics-init-ms`, and `upload-ms` from preparation.
|
||||
The browser returns ordered events within each phase. Preparation diagnostics
|
||||
retain `module-init-ms`, `graphics-init-ms`, and `upload-ms` separately.
|
||||
|
||||
A fresh attempt reports one drain:
|
||||
A render drain retains these trace fields:
|
||||
|
||||
- `slices`: one record per renderer call with `timestamp`, `flags`,
|
||||
`frame-type`, and `duration-ms`;
|
||||
- `viewport-ready-ms`: the first `ViewportReady` or `Full` completion from the
|
||||
drain origin;
|
||||
- `full-ms`: the `Full` completion from the drain origin;
|
||||
- `first-render-ms`: the single first-render metric for a fresh case.
|
||||
- `full-ms`: the `Full` completion from the drain origin.
|
||||
|
||||
A warm attempt reports the camera session and the final drain:
|
||||
A camera 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
|
||||
@ -746,13 +787,9 @@ 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.
|
||||
|
||||
Both kinds of attempt report `renderer-call-total-ms`, the sum of the renderer
|
||||
call durations, computed after timing.
|
||||
|
||||
A saved run metric declares its unit, semantics, basis, and zero policy. A batch
|
||||
average records its call count; one attempt supplies one observation. Raw slices
|
||||
keep zero durations and non-finite values. The metric entry classifies the value
|
||||
for analysis and records a reason when it is invalid.
|
||||
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 for analysis.
|
||||
|
||||
### Renderer completion
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,8 @@
|
||||
|
||||
A scene declares an id, description, parameters schema and build function.
|
||||
A case declares an id namespaced by its scene, parameters, view, context
|
||||
policy, completion mode and optional operation identity and run function.
|
||||
policy and a run function. Optional preparation and reset functions transform
|
||||
the context before and after its main execution.
|
||||
|
||||
Ordinary :require loads the declaration macros and runtime helpers.
|
||||
Collection validates declarations and produces plain descriptors."
|
||||
@ -83,11 +84,15 @@
|
||||
|
||||
The scene comes from the namespace part of the :scene/case id. An
|
||||
optional argument vector and body supply the run function, which accepts
|
||||
one execution context. Collection produces plain case descriptors.
|
||||
one command context and returns its next context or a Promise of it.
|
||||
`[:preparation :callback]` runs once after upload. Preparation renders
|
||||
Full by default; `:render :none` selects a cold first render. `:reset`
|
||||
runs after each successful main execution. Collection produces plain
|
||||
case descriptors.
|
||||
|
||||
Example:
|
||||
|
||||
(defcase :rects/pan options [context] (pan! context))"
|
||||
(defcase :rects/pan options [context] (pan* context pan-options))"
|
||||
([id opts]
|
||||
(case-registration-form id (scene-from-id id) opts nil))
|
||||
([id opts argv & body]
|
||||
@ -97,12 +102,11 @@
|
||||
#?(:cljs
|
||||
(do
|
||||
;; Case execution context:
|
||||
;; :case - collected descriptor, including operation and completion
|
||||
;; :case - collected descriptor
|
||||
;; :scene - scene snapshot {:objects ... :refs ...}
|
||||
;; :module - WASM module handle
|
||||
;; :view - prepared camera view
|
||||
;; :view - current camera view
|
||||
;; :hooks - clock, scheduling, guard and renderer operations
|
||||
;; Case bodies return a measurement or a Promise of one.
|
||||
;; Case bodies return the next context or a Promise of it.
|
||||
|
||||
;; Declarations and their transitive imports load in Node and the browser.
|
||||
;; Browser capabilities enter through the execution context; resource
|
||||
@ -152,7 +156,11 @@
|
||||
[:completion {:optional true} [:enum :call :await :render-full]]
|
||||
[:batch-size {:optional true} [:int {:min 1}]]
|
||||
[:operation {:optional true} [:map]]
|
||||
[:preparation {:optional true} [:map]]
|
||||
[:preparation {:optional true}
|
||||
[:map
|
||||
[:render {:optional true} [:enum :full :none]]
|
||||
[:callback {:optional true} fn?]]]
|
||||
[:reset {:optional true} fn?]
|
||||
[:run! {:optional true} fn?]])
|
||||
|
||||
(def schema:collected-case
|
||||
@ -166,7 +174,7 @@
|
||||
[:context [:enum :fresh :reuse]]
|
||||
[:completion [:enum :call :await :render-full]]
|
||||
[:batch-size [:int {:min 1}]]
|
||||
[:preparation [:map]]
|
||||
[:preparation [:map [:render {:optional true} [:enum :full :none]]]]
|
||||
[:operation {:optional true} [:map]]])
|
||||
|
||||
(defonce registry
|
||||
@ -348,7 +356,7 @@
|
||||
(= encoded (-> encoded t/decode-str t/encode-str))))))
|
||||
|
||||
(defn project-case
|
||||
"Returns the case's descriptor fields with defaults for completion,
|
||||
"Returns the case's data descriptor with defaults for completion,
|
||||
batch size and preparation."
|
||||
[bench-case]
|
||||
(-> bench-case
|
||||
@ -356,7 +364,7 @@
|
||||
:operation :preparation])
|
||||
(update :completion #(or % :render-full))
|
||||
(update :batch-size #(or % 1))
|
||||
(update :preparation #(or % {}))))
|
||||
(update :preparation #(dissoc (or % {}) :callback))))
|
||||
|
||||
(defn check-collected-case
|
||||
"Validates a collected case descriptor and its Transit representation,
|
||||
|
||||
@ -23,9 +23,7 @@
|
||||
- `:view`: the current view.
|
||||
- `:hooks`: the clock, scheduling, renderer and execution guard functions.
|
||||
|
||||
The runtime context for a case operation also includes `:module`, the
|
||||
instantiated WebAssembly renderer. `pan!` and `zoom!` read movement settings
|
||||
from `[:case :operation]`.
|
||||
`pan!` and `zoom!` read movement settings from `[:case :operation]`.
|
||||
|
||||
## Rendering and camera sessions
|
||||
|
||||
|
||||
@ -107,15 +107,20 @@
|
||||
(t/deftest declared-preparation-survives-collection-and-wire
|
||||
(with-synthetic-scene :contracts-prepared {:count 3}
|
||||
(fn []
|
||||
(let [preparation {:settle-ms 25}
|
||||
(let [preparation {:render :none :settle-ms 25}
|
||||
registered (decl/registered-case :contracts-prepared/case)]
|
||||
(decl/register-case! (assoc registered :preparation preparation))
|
||||
(decl/register-case! (assoc registered
|
||||
:preparation (assoc preparation :callback identity)
|
||||
:reset identity))
|
||||
(let [collected (cases/collect-cases {:master-seed 42 :filter "contracts-prepared/"})
|
||||
decoded (transit/decode-str (transit/encode-str collected))]
|
||||
(t/is (= [:contracts-prepared/case] (mapv :id decoded)))
|
||||
(t/is (= collected decoded))
|
||||
(t/is (= preparation (:preparation (first decoded))))
|
||||
(t/is (= (first decoded) (decl/check-collected-case (first decoded)))))))))
|
||||
(t/is (= (first decoded) (decl/check-collected-case (first decoded)))))
|
||||
(decl/register-case! (assoc registered :preparation {:asset {:loader identity}}))
|
||||
(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
|
||||
(let [collected (rect-cases)]
|
||||
|
||||
@ -17,11 +17,7 @@
|
||||
[benches.render-wasm.cases :as cases]
|
||||
[benches.render-wasm.declarations :as decl]
|
||||
[benches.render-wasm.errors :as errors]
|
||||
[benches.render-wasm.measurement :as measurement]
|
||||
[benches.render-wasm.report.compare :as compare]
|
||||
[benches.render-wasm.report.format :as format]
|
||||
[benches.render-wasm.report.summarize :as summary]
|
||||
[benches.render-wasm.result :as result]
|
||||
[benches.render-wasm.runtime.camera :as camera]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
;; Forward declaration: entry-test helpers below build args against the
|
||||
@ -32,10 +28,25 @@
|
||||
;; `frontend-tests.render-wasm.webgl-test`, which also pins the success,
|
||||
;; nil-context and registration-failure cases.
|
||||
|
||||
(defn- register-phase-case
|
||||
[options run]
|
||||
(let [params (:params (decl/registered-case :rects/load))]
|
||||
(decl/defcase :rects/lifecycle
|
||||
(-> (merge {:params params
|
||||
:view {:scale 1 :x 0 :y 0}
|
||||
:context :fresh}
|
||||
options)
|
||||
(update :params #(merge params %)))
|
||||
[rtx]
|
||||
(run rtx))))
|
||||
|
||||
(defn- load-case
|
||||
"Collected `:rects/load` descriptor for entry tests."
|
||||
"Returns the collected rectangle case for entry tests."
|
||||
[]
|
||||
(first (cases/collect-cases {:master-seed 42 :filter "rects/load"})))
|
||||
(first (cases/collect-cases {:master-seed 42
|
||||
:filter (if (decl/registered-case :rects/lifecycle)
|
||||
"rects/lifecycle"
|
||||
"rects/load")})))
|
||||
|
||||
(defn- scene-args
|
||||
"Transit request for `browser/load-scene`. `overrides` change the
|
||||
@ -424,9 +435,11 @@
|
||||
(try
|
||||
(browser/dispose!)
|
||||
(finally
|
||||
(decl/unregister-case! :rects/lifecycle)
|
||||
(restore-dom!))))]
|
||||
(reset-bench-globals!)
|
||||
(try
|
||||
(register-phase-case {} camera/render-sync*)
|
||||
(f cleanup)
|
||||
(catch :default cause
|
||||
(cleanup)
|
||||
@ -458,7 +471,9 @@
|
||||
(load-result (scene-args 7 {})
|
||||
(fn [m]
|
||||
(t/is (= "ok" (:status m)))
|
||||
(t/is (= 1 (count (get-in m [:trace :slices]))))
|
||||
(t/is (= 1 (count (get-in m [:preparation :diagnostics :setup-render :slices]))))
|
||||
(t/is (= [::camera/render-sync]
|
||||
(mapv :command (get-in m [:phases 0 :events]))))
|
||||
(t/is (= 1001 (:shapes (:scene m))) "the canonical scene uploads whole")
|
||||
(let [graphics (:effective-graphics m)]
|
||||
(t/is (= 1234 (:drawing-buffer-width graphics))
|
||||
@ -475,35 +490,7 @@
|
||||
(cleanup)
|
||||
(done)))))))
|
||||
|
||||
(t/deftest successful-load-produces-portable-reports
|
||||
(t/async done
|
||||
(with-entry-env
|
||||
(fn [cleanup]
|
||||
(load-result
|
||||
(scene-args 7 {})
|
||||
(fn [evidence]
|
||||
(let [case (load-case)
|
||||
preparation (:preparation evidence)
|
||||
run (-> (result/create-run {:run-id "load-report" :started-at "start"
|
||||
:plan {:cases [case] :warmups 0 :repetitions 1}
|
||||
:metadata {:git {:sha nil :dirty nil}
|
||||
:build {:functional {:mode :test}}
|
||||
:environment {} :scored? false}})
|
||||
(result/record-preparation preparation)
|
||||
(result/record-attempt (measurement/attempt (:id case) (:preparation-id preparation) false evidence))
|
||||
(result/finish-run "end" {:reason :completed}))
|
||||
text (result/encode run)
|
||||
report (summary/summarize (result/decode text))
|
||||
comparison (compare/compare-runs run run)
|
||||
comparison-text (.compare browser/bridge text text (transit/encode-str {}))]
|
||||
(t/is (= report (transit/decode-str (.summarize browser/bridge text))))
|
||||
(t/is (= (format/format-run report) (.formatRun browser/bridge text)))
|
||||
(t/is (= comparison (result/decode comparison-text)))
|
||||
(t/is (= (format/format-comparison comparison) (.formatComparison browser/bridge comparison-text))))
|
||||
(cleanup)
|
||||
(done)))))))
|
||||
|
||||
(t/deftest exported-load-scene-retains-progressive-frame-evidence
|
||||
(t/deftest preparation-retains-progressive-frame-evidence
|
||||
(t/async done
|
||||
(with-entry-env
|
||||
(fn [cleanup]
|
||||
@ -515,9 +502,10 @@
|
||||
(-> (.loadScene browser/bridge (scene-args 7 {}))
|
||||
(.then (fn [text]
|
||||
(t/is (string? text) "the exported loadScene returns Transit")
|
||||
(let [{:keys [status slices viewport-ready-ms full-ms metrics trace scene]}
|
||||
(transit/decode-str text)
|
||||
retained (:slices trace)
|
||||
(let [{:keys [status preparation scene]} (transit/decode-str text)
|
||||
{:keys [slices viewport-ready-ms full-ms]}
|
||||
(get-in preparation [:diagnostics :setup-render])
|
||||
retained slices
|
||||
render-calls (filterv #(= "render" (first %)) (read-calls))]
|
||||
(t/is (= "ok" status))
|
||||
(t/is (= {:shapes 1001 :seed 7} scene)
|
||||
@ -538,7 +526,6 @@
|
||||
["render" timestamp flags])
|
||||
retained))
|
||||
"decoded slices retain every WASM call's timestamp and flags in order")
|
||||
(t/is (= slices retained) "raw and trace slices survive Transit together")
|
||||
(t/is (= 37 viewport-ready-ms
|
||||
(+ (:timestamp (nth retained 1))
|
||||
(:duration-ms (nth retained 1))))
|
||||
@ -548,12 +535,10 @@
|
||||
(:duration-ms (nth retained 2))))
|
||||
"Full completion ends with the final slice")
|
||||
(t/is (< viewport-ready-ms full-ms))
|
||||
(t/is (= full-ms (get-in metrics [:first-render-ms :value])))
|
||||
(t/is (= viewport-ready-ms (get-in metrics [:viewport-ready-ms :value])))
|
||||
(t/is (= 10
|
||||
(reduce + 0 (map :duration-ms retained))
|
||||
(get-in metrics [:renderer-call-total-ms :value]))
|
||||
"renderer-call total sums the retained calls without frame waits"))
|
||||
(t/is (= [{:phase :preparation
|
||||
:preparation-id (:preparation-id preparation)
|
||||
:outcome :completed :events []}]
|
||||
(:phases preparation))))
|
||||
(cleanup)
|
||||
(done)))
|
||||
(.catch (fn [cause]
|
||||
@ -579,108 +564,264 @@
|
||||
(cleanup)
|
||||
(done))))))))))
|
||||
|
||||
(t/deftest warm-pan-restores-and-reaches-full
|
||||
(t/deftest compiled-pan-finishes-at-its-final-render
|
||||
(t/async done
|
||||
(with-entry-env
|
||||
(fn [cleanup]
|
||||
(load-result (scene-args 7 {:id :rects/pan :context :reuse})
|
||||
(register-phase-case {:context :reuse}
|
||||
#(camera/pan* % {:dx 10 :dy 5 :steps 2 :settle-ms 0}))
|
||||
(load-result (scene-args 7 {})
|
||||
(fn [m]
|
||||
(t/is (= "ok" (:status m)))
|
||||
(t/is (= 20 (count (:cached-slices m))))
|
||||
(t/is (= 100 (:settling-requested-ms m)))
|
||||
(t/is (= [4] (mapv :flags (:slices m))))
|
||||
(t/is (= [2] (mapv :frame-type (:slices m))))
|
||||
(t/is (= 3 (effect-count (read-calls) "render"))
|
||||
"setup, restore and finalization each drain once")
|
||||
(let [phase (first (:phases m))
|
||||
event (first (:events phase))
|
||||
trace (:trace event)]
|
||||
(t/is (= "ok" (:status m)))
|
||||
(t/is (= :main (:phase phase)))
|
||||
(t/is (= ::camera/pan (:command event)))
|
||||
(t/is (= 2 (count (:cached-slices trace))))
|
||||
(t/is (= 0 (:settling-requested-ms trace)))
|
||||
(t/is (= [4] (mapv :flags (:slices trace))))
|
||||
(t/is (= [2] (mapv :frame-type (:slices trace))))
|
||||
(t/is (= 2 (effect-count (read-calls) "render"))
|
||||
"preparation and camera finalization each render once"))
|
||||
(cleanup)
|
||||
(done)))))))
|
||||
|
||||
(t/deftest warm-zoom-reaches-full
|
||||
(t/async done
|
||||
(with-entry-env
|
||||
(fn [cleanup]
|
||||
(load-result (scene-args 7 {:id :rects/zoom :context :reuse})
|
||||
(fn [m]
|
||||
(t/is (= "ok" (:status m)))
|
||||
(t/is (= 20 (count (:cached-slices m))))
|
||||
(t/is (= [2] (mapv :frame-type (:slices m))))
|
||||
(cleanup)
|
||||
(done)))))))
|
||||
(t/deftest ^:async reused-context-prepares-once-and-resets-each-main
|
||||
(await
|
||||
(with-entry-env
|
||||
(^:async fn [cleanup]
|
||||
(try
|
||||
(let [views (atom [])
|
||||
options {:dy 0 :steps 1 :settle-ms 0}
|
||||
pan (fn [phase dx rtx]
|
||||
(swap! views conj [phase (get-in rtx [:view :x])])
|
||||
(camera/pan* rtx (assoc options :dx dx)))]
|
||||
(register-phase-case {:context :reuse
|
||||
:preparation {:callback #(pan :preparation 5 %)}
|
||||
:reset #(pan :reset -10 %)}
|
||||
#(pan :main 10 %))
|
||||
(let [prepared (transit/decode-str (await (browser/prepare-scene-request (scene-args 7 {}))))
|
||||
prep (:preparation prepared)
|
||||
request (transit/encode-str {:preparation-id (:preparation-id prep)})
|
||||
first (transit/decode-str (await (browser/run-prepared request)))
|
||||
second (transit/decode-str (await (browser/run-prepared request)))]
|
||||
(t/is (= ["ok" "ok" "ok"] (mapv :status [prepared first second])))
|
||||
(t/is (= [[:preparation 0] [:main 5] [:reset 15] [:main 5] [:reset 15]] @views))
|
||||
(t/is (= [:preparation] (mapv :phase (:phases prep))))
|
||||
(t/is (= [::camera/pan] (mapv :command (get-in prep [:phases 0 :events]))))
|
||||
(t/is (= prep (:preparation first) (:preparation second)))
|
||||
(doseq [execution [first second]]
|
||||
(t/is (= [:main :reset] (mapv :phase (:phases execution))))
|
||||
(t/is (= [:completed :completed] (mapv :outcome (:phases execution))))
|
||||
(t/is (= [[::camera/pan] [::camera/pan]]
|
||||
(mapv #(mapv :command (:events %)) (:phases execution)))))
|
||||
(t/is (= 1 (effect-count (read-calls) "create")))
|
||||
(t/is (= 6 (effect-count (read-calls) "render"))
|
||||
"the callback, initial Full, and four main/reset gestures render")
|
||||
(t/is (= 1 (count (filter #(= ["call" "_init_shapes_pool"] %) (read-calls)))))))
|
||||
(finally
|
||||
(cleanup)))))))
|
||||
|
||||
(t/deftest prepared-warm-scene-runs-twice-with-one-context-and-upload
|
||||
(t/async done
|
||||
(with-entry-env
|
||||
(fn [cleanup]
|
||||
(-> (browser/prepare-scene-request
|
||||
(scene-args 7 {:id :rects/pan :context :reuse
|
||||
:operation {:steps 2 :dx 10 :dy 5 :settle-ms 0}}))
|
||||
(.then (fn [text]
|
||||
(let [prepared (transit/decode-str text)
|
||||
prep (:preparation prepared)
|
||||
request (transit/encode-str {:preparation-id (:preparation-id prep)})]
|
||||
(t/is (= "ok" (:status prepared)))
|
||||
(t/is (seq (get-in prep [:diagnostics :setup-render :slices])))
|
||||
(-> (browser/run-prepared request)
|
||||
(.then (fn [first-text]
|
||||
(let [first-result (transit/decode-str first-text)]
|
||||
(t/is (= "ok" (:status first-result)))
|
||||
(t/is (not (contains? (:metrics first-result) :upload-ms)))
|
||||
(t/is (= prep (:preparation first-result)))
|
||||
(browser/run-prepared request))))
|
||||
(.then (fn [second-text]
|
||||
(let [second-result (transit/decode-str second-text)
|
||||
calls (read-calls)]
|
||||
(t/is (= "ok" (:status second-result)))
|
||||
(t/is (= prep (:preparation second-result)))
|
||||
(t/is (= 1 (effect-count calls "create")))
|
||||
(t/is (= 5 (effect-count calls "render")))
|
||||
(t/is (= 1 (count (filter #(= ["call" "_init_shapes_pool"] %) calls))))
|
||||
(t/is (= 2 (count (:cached-slices second-result))))
|
||||
(cleanup)
|
||||
(done))))))))
|
||||
(.catch (fn [cause]
|
||||
(t/is false (str "prepared reuse failed: " cause))
|
||||
(cleanup)
|
||||
(done))))))))
|
||||
(t/deftest ^:async fresh-contexts-prepare-and-reset-once
|
||||
(await
|
||||
(with-entry-env
|
||||
(^:async fn [cleanup]
|
||||
(try
|
||||
(let [visits (atom [])
|
||||
work (fn [phase rtx]
|
||||
(swap! visits conj phase)
|
||||
(camera/render-sync* rtx))]
|
||||
(register-phase-case {:preparation {:callback #(work :preparation %)}
|
||||
:reset #(work :reset %)}
|
||||
#(work :main %))
|
||||
(dotimes [_ 2]
|
||||
(let [prepared (transit/decode-str (await (browser/prepare-scene-request (scene-args 7 {}))))
|
||||
prep (:preparation prepared)
|
||||
request (transit/encode-str {:preparation-id (:preparation-id prep)})
|
||||
evidence (transit/decode-str (await (browser/run-prepared request)))
|
||||
repeated (transit/decode-str (await (browser/run-prepared request)))]
|
||||
(t/is (= "ok" (:status evidence)))
|
||||
(t/is (= [:main :reset] (mapv :phase (:phases evidence))))
|
||||
(t/is (= [::camera/render-sync] (mapv :command (get-in prep [:phases 0 :events]))))
|
||||
(t/is (= "invalid-args" (:phase repeated)))))
|
||||
(t/is (= [:preparation :main :reset :preparation :main :reset] @visits))
|
||||
(t/is (= 2 (effect-count (read-calls) "create")))
|
||||
(t/is (= 2 (effect-count (read-calls) "render"))))
|
||||
(finally
|
||||
(cleanup)))))))
|
||||
|
||||
(t/deftest fresh-preparation-runs-once-and-saves-structured-metrics
|
||||
(t/async done
|
||||
(with-entry-env
|
||||
(fn [cleanup]
|
||||
(-> (browser/prepare-scene-request (scene-args 7 {}))
|
||||
(.then (fn [text]
|
||||
(let [request (transit/encode-str
|
||||
{:preparation-id (get-in (transit/decode-str text)
|
||||
[:preparation :preparation-id])})]
|
||||
(t/is (zero? (effect-count (read-calls) "render")))
|
||||
(-> (browser/run-prepared request)
|
||||
(.then (fn [text]
|
||||
(let [evidence (transit/decode-str text)]
|
||||
(t/is (= "ok" (:status evidence)))
|
||||
(t/is (= (:full-ms evidence) (get-in evidence [:metrics :first-render-ms :value])))
|
||||
(t/is (contains? (:metrics evidence) :upload-ms))
|
||||
(t/is (not (contains? (:metrics evidence) :full-ms)))
|
||||
(t/is (not (contains? evidence :first-render-ms)))
|
||||
(t/is (not (contains? evidence :render-frames)))
|
||||
(browser/run-prepared request))))
|
||||
(.then (fn [text]
|
||||
(t/is (= "invalid-args" (:phase (transit/decode-str text))))
|
||||
(cleanup)
|
||||
(done)))))))
|
||||
(.catch (fn [cause]
|
||||
(t/is false (str "fresh preparation failed: " cause))
|
||||
(cleanup)
|
||||
(done))))))))
|
||||
(t/deftest ^:async preparation-policy-keeps-selected-events-and-empty-main
|
||||
(doseq [context [:fresh :reuse]
|
||||
render [nil :none]]
|
||||
(await
|
||||
(with-entry-env
|
||||
(^:async fn [cleanup]
|
||||
(try
|
||||
(unchecked-set js/globalThis "__benchNow" 0)
|
||||
(register-phase-case
|
||||
{:params {:count 2}
|
||||
:context context
|
||||
:preparation (cond-> {:callback #(-> % (camera/render-sync*) (camera/render-sync*))}
|
||||
render (assoc :render render))}
|
||||
identity)
|
||||
(let [evidence (transit/decode-str (await (browser/load-scene (scene-args 7 {}))))
|
||||
prep (:preparation evidence)
|
||||
block (first (:phases prep))]
|
||||
(t/is (= "ok" (:status evidence)))
|
||||
(t/is (= :completed (:outcome block)))
|
||||
(t/is (= [{:command ::camera/render-sync :metrics {:command/elapsed [0]}}
|
||||
{:command ::camera/render-sync :metrics {:command/elapsed [0]}}]
|
||||
(:events block)))
|
||||
(t/is (= [{:phase :main :preparation-id (:preparation-id prep)
|
||||
:outcome :completed :events []}]
|
||||
(:phases evidence)))
|
||||
(t/is (= (if (= render :none) 0 1) (effect-count (read-calls) "render"))))
|
||||
(finally
|
||||
(cleanup))))))))
|
||||
|
||||
(t/deftest ^:async reused-context-carries-view-without-a-reset
|
||||
(await
|
||||
(with-entry-env
|
||||
(^:async fn [cleanup]
|
||||
(try
|
||||
(let [views (atom [])]
|
||||
(register-phase-case {:params {:count 2} :context :reuse}
|
||||
(fn [rtx]
|
||||
(swap! views conj (:view rtx))
|
||||
(camera/pan rtx {:dx 10 :dy 5 :steps 1 :settle-ms 0})))
|
||||
(let [prepared (transit/decode-str (await (browser/prepare-scene-request (scene-args 7 {}))))
|
||||
request (transit/encode-str {:preparation-id (get-in prepared [:preparation :preparation-id])})
|
||||
first (transit/decode-str (await (browser/run-prepared request)))
|
||||
second (transit/decode-str (await (browser/run-prepared request)))]
|
||||
(t/is (= ["ok" "ok"] (mapv :status [first second])))
|
||||
(t/is (= [{:scale 1 :x 0 :y 0} {:scale 1 :x 10 :y 5}] @views))
|
||||
(t/is (= [[] []] (mapv #(get-in % [:phases 0 :events]) [first second])))))
|
||||
(finally
|
||||
(cleanup)))))))
|
||||
|
||||
(t/deftest ^:async phase-failure-keeps-raw-evidence-and-stops-the-context
|
||||
(doseq [context [:fresh :reuse]
|
||||
failed-phase [:preparation :main :reset]]
|
||||
(await
|
||||
(with-entry-env
|
||||
(^:async fn [cleanup]
|
||||
(try
|
||||
(unchecked-set js/globalThis "__benchNow" 0)
|
||||
(let [visits (atom [])
|
||||
work (fn [phase rtx]
|
||||
(swap! visits conj phase)
|
||||
(-> (camera/render-sync* rtx)
|
||||
(.then (fn [next-rtx]
|
||||
(when (= phase failed-phase)
|
||||
(throw (js/Error. "phase fixture failed")))
|
||||
next-rtx))))]
|
||||
(register-phase-case {:params {:count 2} :context context
|
||||
:preparation {:callback #(work :preparation %)}
|
||||
:reset #(work :reset %)}
|
||||
#(work :main %))
|
||||
(let [request (-> (transit/decode-str (scene-args 7 {}))
|
||||
(assoc :preparation-id "phase-failure")
|
||||
(transit/encode-str))
|
||||
prepared (transit/decode-str (await (browser/prepare-scene-request request)))
|
||||
run (transit/encode-str {:preparation-id "phase-failure"})
|
||||
evidence (if (= failed-phase :preparation)
|
||||
prepared
|
||||
(transit/decode-str (await (browser/run-prepared run))))
|
||||
stopped (transit/decode-str (await (browser/run-prepared run)))
|
||||
blocks (:phases evidence)]
|
||||
(t/is (= "failed" (:status evidence)))
|
||||
(t/is (= (name failed-phase) (:phase evidence)))
|
||||
(t/is (= "phase fixture failed" (:message evidence)))
|
||||
(t/is (= (case failed-phase
|
||||
:preparation [:preparation]
|
||||
:main [:preparation :main]
|
||||
:reset [:preparation :main :reset])
|
||||
@visits))
|
||||
(t/is (= (if (= failed-phase :reset) [:main :reset] [failed-phase])
|
||||
(mapv :phase blocks)))
|
||||
(t/is (= :failed (:outcome (last blocks))))
|
||||
(t/is (= (repeat (count blocks)
|
||||
[{:command ::camera/render-sync :metrics {:command/elapsed [0]}}])
|
||||
(map :events blocks)))
|
||||
(when (= failed-phase :preparation)
|
||||
(let [setup (get-in evidence [:partial :setup])]
|
||||
(t/is (number? (:module-ms setup)))
|
||||
(t/is (number? (:graphics-ms setup)))
|
||||
(t/is (number? (:upload-ms setup)))
|
||||
(t/is (string? (get-in setup [:fingerprint :digest])))))
|
||||
(when-not (= failed-phase :preparation)
|
||||
(t/is (= (:preparation prepared) (:preparation evidence)))
|
||||
(t/is (= :completed (get-in evidence [:preparation :phases 0 :outcome]))))
|
||||
(t/is (= "invalid-args" (:phase stopped)))
|
||||
(t/is (= 1 (effect-count (read-calls) "remove"))))
|
||||
(register-phase-case {:params {:count 2}} camera/render-sync*)
|
||||
(let [next-case (transit/decode-str (await (browser/load-scene (scene-args 8 {}))))]
|
||||
(t/is (= "ok" (:status next-case)))
|
||||
(t/is (= [::camera/render-sync]
|
||||
(mapv :command (get-in next-case [:phases 0 :events]))))))
|
||||
(finally
|
||||
(cleanup))))))))
|
||||
|
||||
(t/deftest ^:async superseded-phase-failure-keeps-its-own-preparation
|
||||
(doseq [failed-phase [:main :reset]]
|
||||
(await
|
||||
(with-entry-env
|
||||
(^:async fn [cleanup]
|
||||
(try
|
||||
(let [entered (volatile! nil)
|
||||
reject (volatile! nil)
|
||||
started (js/Promise. (fn [resolve _reject] (vreset! entered resolve)))
|
||||
work (fn [phase rtx]
|
||||
(-> (camera/render-sync* rtx)
|
||||
(.then (fn [next-rtx]
|
||||
(if (= phase failed-phase)
|
||||
(js/Promise. (fn [_resolve fail]
|
||||
(vreset! reject fail)
|
||||
(@entered nil)))
|
||||
next-rtx)))))]
|
||||
(register-phase-case {:params {:count 2} :context :reuse
|
||||
:reset #(work :reset %)}
|
||||
#(work :main %))
|
||||
(let [request (-> (transit/decode-str (scene-args 7 {}))
|
||||
(assoc :preparation-id "first")
|
||||
(transit/encode-str))
|
||||
prepared (transit/decode-str (await (browser/prepare-scene-request request)))
|
||||
first-run (browser/run-prepared (transit/encode-str {:preparation-id "first"}))]
|
||||
(await started)
|
||||
(register-phase-case {:params {:count 2}} camera/render-sync*)
|
||||
(let [next-request (-> (transit/decode-str (scene-args 8 {}))
|
||||
(assoc :preparation-id "second")
|
||||
(transit/encode-str))
|
||||
next-prepared (transit/decode-str (await (browser/prepare-scene-request next-request)))]
|
||||
(@reject (js/Error. "late phase failure"))
|
||||
(let [failed (transit/decode-str (await first-run))
|
||||
next-run (transit/decode-str
|
||||
(await (browser/run-prepared (transit/encode-str {:preparation-id "second"}))))]
|
||||
(t/is (= "failed" (:status failed)))
|
||||
(t/is (= (name failed-phase) (:phase failed)))
|
||||
(t/is (= (:preparation prepared) (:preparation failed)))
|
||||
(t/is (= (if (= failed-phase :main) [:main] [:main :reset])
|
||||
(mapv :phase (:phases failed))))
|
||||
(t/is (= :failed (:outcome (last (:phases failed)))))
|
||||
(t/is (= (repeat (count (:phases failed)) "first")
|
||||
(map :preparation-id (:phases failed))))
|
||||
(t/is (= ["ok" "ok"] (mapv :status [next-prepared next-run])))
|
||||
(t/is (= "second" (get-in next-run [:preparation :preparation-id])))))))
|
||||
(finally
|
||||
(cleanup))))))))
|
||||
|
||||
(t/deftest rejected-camera-body-releases-owner
|
||||
(t/async done
|
||||
(with-entry-env
|
||||
(fn [cleanup]
|
||||
(unchecked-set js/globalThis "__benchThrowIn" "_render_from_cache")
|
||||
(load-result (scene-args 7 {:id :rects/pan :context :reuse})
|
||||
(register-phase-case {:context :reuse}
|
||||
#(camera/pan* % {:dx 10 :dy 5 :steps 2 :settle-ms 0}))
|
||||
(load-result (scene-args 7 {})
|
||||
(fn [m]
|
||||
(t/is (= "failed" (:status m)))
|
||||
(t/is (= "interact" (:phase m)))
|
||||
(t/is (= "main" (:phase m)))
|
||||
(t/is (= 1 (effect-count (read-calls) "remove")))
|
||||
(t/is (nil? @@#'browser/canvas*))
|
||||
(cleanup)
|
||||
@ -698,6 +839,7 @@
|
||||
(fn [m]
|
||||
(t/is (= "failed" (:status m)))
|
||||
(t/is (= "module-init" (:phase m)))
|
||||
(t/is (= {} (get-in m [:partial :setup])))
|
||||
(cleanup)
|
||||
(done)))))))
|
||||
|
||||
@ -727,6 +869,7 @@
|
||||
(fn [m]
|
||||
(t/is (= "failed" (:status m)))
|
||||
(t/is (= "graphics-init" (:phase m)))
|
||||
(t/is (= {} (get-in m [:partial :setup])))
|
||||
(t/is (= "graphics init failed" (:message m))
|
||||
"top message stays phase-labeled")
|
||||
(assert-wasm-cause (:cause m)
|
||||
@ -756,6 +899,10 @@
|
||||
(assert-wasm-cause (:cause m)
|
||||
"_set_view"
|
||||
#"fake _set_view failure")
|
||||
(let [setup (get-in m [:partial :setup])]
|
||||
(t/is (number? (:module-ms setup)))
|
||||
(t/is (number? (:graphics-ms setup)))
|
||||
(t/is (string? (get-in setup [:fingerprint :digest]))))
|
||||
(cleanup)
|
||||
(done)))))))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user