Skip to content

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 > 2500 or edge_count > 7500recommend_precomputed_projection: true, reason graph_shape_exceeds_client_projection_limit.
  • node_count > 600 or edge_count > 1200recommend_precomputed_projection: true, reason dense_graph_prefers_server_projection_or_cached_layout.
  • duration_ms > 120recommend_precomputed_projection: true, reason measured_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 M

last_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:

  1. Publish or symlink the candidate to .dev/published/math-engineering-support (or run make publish-math-engineering-support-index).

  2. 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.)

  3. Open Canva, select scope Universe, wait until the worker idle status appears in the System disclosure.

  4. 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_count and what triggered it.
  5. Perform the acceptance interactions:

    1. Click 10 different planets in succession (selection-only path, should not flush server queries).
    2. Hover-sweep across 30 planets (cheap-hover counter should climb; rebuild count should not).
    3. Switch scope UniverseKanbanUniverse (forces full rebuild on each scope change; observe last-rebuild ms).
    4. Open the source inspector on a course planet (latency from click to first row).
  6. Capture screen shots / readings into the verification notes for the release.

Acceptance thresholds

The universal Canva is acceptable for release when:

SignalThresholdSource
is_dense on Universe scopetruehover must use render-only path
hover_updates_rebuild_scenefalse on Universedense dispatch
last_scene_rebuild_ms on full Universe rebuild≤ 1500 ms on a shared-cpu-1x-class machineacceptable cold-start latency for an opt-in dense view
cheap_hover_render_count after 30 hovers≥ 25hovers must not silently fall through to rebuild
scene_rebuild_count after 30 hoversunchanged (delta == 0)confirms render-only hover holds under load
Source-inspector first-row latency≤ 500 ms after planet clickcatalog query + source-ref expansion budget
Scope switch cancellationprior 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.json with 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 by make build-math-engineering-index.)
  • Lazy renderer bundle. AtlasCanvas.svelte is 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: 2500 for Universe scope, and internal/graphview/build.go caps 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 the graph_shape_exceeds_client_projection_limit recommendation.

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).
  • 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.