Universal Canva Performance Baseline
Universal Canva Performance Baseline
Date written: 2026-05-15
Scope: 1,925-course universal Canva (.dev/published/math-engineering-support)
Purpose
This note documents the performance instrumentation that already exists for the universal Canva, the measurement protocol an operator should follow when validating a new index against the 1,925-course load, and the acceptance thresholds beyond which a regression should block a release. It closes the methodology side of gap §14.3 Todo #5 (the universal Canva performance pass).
It is not a one-shot benchmark report. It exists so that future performance regressions can be detected without redesigning the measurement procedure.
Instrumentation already in place
The Atlas client already captures three independent perf signals. No new measurement plumbing is required for the perf pass.
Preparation timing (synchronous, off-render-thread)
web/src/lib/atlas/performance.ts::measureAtlasPreparation measures
the cost of running the layout-input + node-map + edge-map adapter
over a graph view. Its purpose is to predict whether the in-browser
client layout will keep up or whether the request should fall back to
server-side projection hints.
Recommendation thresholds (already enforced):
node_count > 2500oredge_count > 7500→recommend_precomputed_projection: true, reasongraph_shape_exceeds_client_projection_limit.node_count > 600oredge_count > 1200→recommend_precomputed_projection: true, reasondense_graph_prefers_server_projection_or_cached_layout.duration_ms > 120→recommend_precomputed_projection: true, reasonmeasured_preparation_exceeds_demo_budget.
The 1,925-course Universe sits in the “dense graph” band (well under 2500 nodes but well over 600), so the server-side projection / cached-layout-hint code paths are the load-bearing renderers for it.
Render-path strategy classification
classifyAtlasInteractionStrategy decides whether hover events
rebuild the scene (small graphs) or do a render-only pass (dense
graphs). The dense path is what the universal Canva uses.
is_dense = node_count > 600 || edge_count > 1200.- Dense graphs: hover does not rebuild meshes; halos and color changes go through a cheap render call. Avoids the per-hover scene-rebuild cost.
- Small graphs: hover rebuilds halos for stronger affordances.
Live canvas metrics
AtlasCanvas.svelte emits an AtlasCanvasMetrics snapshot on every
scene rebuild and hover render via reportCanvasMetrics(). The
metrics include:
scene_rebuild_count— full-scene rebuilds since canvas mount.cheap_hover_render_count— render-only hover passes since mount.last_scene_rebuild_ms— wall time of the most recent rebuild.is_dense/hover_updates_rebuild_scene— current dispatch path.
These are surfaced in the lower-right System disclosure under
Render path:
dense · hover render-only · rebuilds N · last rebuild XX.Xms · cheap hovers Mlast_scene_rebuild_ms was added 2026-05-15 (this commit). Before
that the operator saw rebuild count but not rebuild cost.
Layout cancellation
web/src/lib/atlas/recompute.ts::RecomputeController cancels the
prior outstanding layout request when a newer one arrives.
#abortActive() aborts the live AbortController and bumps
cancelled_count. The worker honours an explicit cancel message
(web/src/lib/atlas/layout.worker.ts:368) and replies
layout_cancelled which propagates as LayoutRequestCancelledError
through the engine. Cancellation correctness is unit-tested; the perf
pass focuses on cancellation latency under universe load.
Measurement protocol
Operators reproduce the perf pass against a candidate index by:
-
Publish or symlink the candidate to
.dev/published/math-engineering-support(or runmake publish-math-engineering-support-index). -
Boot the dev stack against the published index:
Terminal window UWSCRAPE_INDEX_DIR=$(pwd)/.dev/published/math-engineering-support \pnpm --dir web exec vite dev(or
make dev-demo, which now prefers the published artefact.) -
Open Canva, select scope Universe, wait until the
worker idlestatus appears in the System disclosure. -
Record the System-disclosure readings:
- Active index:
course_count,credential_count. - Render path:
is_dense,last rebuild Xms,rebuilds N,cheap hovers M. - Layout: client/server backend label, node/edge counts.
- Recompute: any non-zero
cancelled_countand what triggered it.
- Active index:
-
Perform the acceptance interactions:
- Click 10 different planets in succession (selection-only path, should not flush server queries).
- Hover-sweep across 30 planets (cheap-hover counter should climb; rebuild count should not).
- Switch scope
Universe→Kanban→Universe(forces full rebuild on each scope change; observe last-rebuild ms). - Open the source inspector on a course planet (latency from click to first row).
-
Capture screen shots / readings into the verification notes for the release.
Acceptance thresholds
The universal Canva is acceptable for release when:
| Signal | Threshold | Source |
|---|---|---|
is_dense on Universe scope | true | hover must use render-only path |
hover_updates_rebuild_scene | false on Universe | dense dispatch |
last_scene_rebuild_ms on full Universe rebuild | ≤ 1500 ms on a shared-cpu-1x-class machine | acceptable cold-start latency for an opt-in dense view |
cheap_hover_render_count after 30 hovers | ≥ 25 | hovers must not silently fall through to rebuild |
scene_rebuild_count after 30 hovers | unchanged (delta == 0) | confirms render-only hover holds under load |
| Source-inspector first-row latency | ≤ 500 ms after planet click | catalog query + source-ref expansion budget |
| Scope switch cancellation | prior request observably cancelled (cancelled_count increments) | proves layout cancellation reaches the worker |
Numbers above are budgets, not measurements. The first run that records actual values for the active machine should update this table into a “measured vs budget” two-column form.
Known guardrails that protect the budget
- Server-side projection layout hints. The published index ships
graph-projection.jsonwith precomputed node positions when the active profile is dense enough; the client uses those hints instead of running force-directed layout in the worker for the full universe. (Built bymake build-math-engineering-index.) - Lazy renderer bundle.
AtlasCanvas.svelteis dynamically imported (AtlasWorkspace.svelte); the Three.js renderer chunk is not pulled into the initial shell. First paint stays around 50 kB minified. - Universe max-nodes cap. The frontend requests
max_courses: 2500for Universe scope, andinternal/graphview/build.gocaps at 2500; the 1,925-course broad candidate fits inside this cap with headroom, but a future broader profile (e.g. graduate+undergrad) would re-trigger thegraph_shape_exceeds_client_projection_limitrecommendation.
What’s intentionally out of scope
- No new benchmark suite. vitest doesn’t run a real WebGL context, so canvas frame-time benchmarks would require Playwright with an explicit perf harness. Adding that is deferred.
- No automated regression alert. The thresholds above are checked manually at release time. Wiring a CI perf check is a separate task (would belong to a P10.+ observability slice).
Related artefacts
- Existing instrumentation:
web/src/lib/atlas/performance.ts,web/src/lib/atlas/AtlasCanvas.svelte,web/src/lib/atlas/recompute.ts,web/src/lib/atlas/layout.worker.ts,web/src/lib/atlas/engine.ts. - Existing unit tests:
web/src/lib/atlas/performance.spec.ts. - Gap analysis context:
docs/audits/2026-05-12-course-first-math-universe-gap-analysis.md§14.3 Todo #5.