-
Notifications
You must be signed in to change notification settings - Fork 0
Rendering and Performance
How PolyGraph draws thousands of nodes smoothly, the scaling work behind it, and the telemetry that measures it.
A Rust crate compiled to WASM that draws the graph as GPU vectors via Vello (0.9) over WebGPU. It's loaded client-side via dynamic import() (components/VelloGraphCanvas.tsx) because WebGPU is browser-only; the compiled output lives in vello-renderer/pkg/ and is committed.
It exposes one handle, VelloCanvas:
| Method | Purpose |
|---|---|
create(canvas) |
Async init against an HTMLCanvasElement
|
set_data(json) |
Feed a new scene ({ nodes, edges, clusters }) |
set_camera(x, y, scale) / fit()
|
Pan/zoom; fit() returns the framing transform |
set_selection(id?) / set_search(query)
|
Highlight state (selection, search dimming) |
set_phase(p) |
Drives the marching-ants edge animation |
set_theme(dark) |
Light/dark |
render() |
Draw a frame |
pick(px, py) |
Hit-test → node / cluster / edge id |
stats() |
Per-frame render telemetry (JSON) |
It draws: cards (light fill, color-coded left edge, tinted icon chip, language badge on file nodes), curved animated edges colored by relationship — thicker with a ×N label for aggregated relationships — and GPU-rendered text labels. Pan/zoom/selection/search and edge picking all update a single GPU scene.
To bound per-frame work the renderer applies several caps (vello-renderer/src/lib.rs):
| Cap | Value | Effect |
|---|---|---|
EDGE_DASH_BUDGET |
300 | Above this many on-screen edges, stop dashing (solid strokes) — dash tessellation is the expensive part |
EDGE_RENDER_CAP |
60 000 | Max edges encoded per frame; above it, a uniform subset is sampled |
EDGE_RENDER_CAP_LOD |
8 000 | Tighter edge cap when zoomed out (below the label scale) |
DOT_LOD_PX |
7 px | Below this on-screen card height, draw a dot instead of a full card |
These make frame cost track what's on screen, not the size of the graph.
The tracking design is docs/SCALE-100K.md. The target is rendering a repo like linux/drivers (~100k nodes). Three independent walls made that fail:
-
Layout hangs first —
smart → dagreon one giant component isO(V·E), with no cap or timeout. - The JSON bridge would OOM — serializing ~100k nodes + ~200k edges on the main thread each scene change.
- The Rust edge encoder overruns — every visible curve re-encoded per frame.
The fix borrows UE5 Nanite's idea: store the whole hierarchy once; each frame pick a per-region cut where on-screen error is ~1px. A codebase is a hierarchy (symbol → file → directory → package → workspace), so the cut opens a cluster to its children only when its on-screen box is big enough to be legible. Payoff: laid-out/rendered element count tracks screen real estate, not repo size.
Status (phased):
- v0 — make it render now (done): renderer edge cap + font hoist; adaptive auto-collapse (seed the collapsed set with top-level dirs); layout size cap + worker timeout. These addressed all three walls enough to render.
-
v1 — adaptive LOD (mostly done): the cut is computed in pure TypeScript (
lib/graph/lod-cut.tscomputeCut), reading each directory's box straight from the live scene and changing only which directories are collapsed as the camera zooms — the Vello renderer is unchanged. It's wired behind anadaptiveLodflag (now default on); the remaining step is calibrating the thresholds (LOD_OPEN_PX,LOD_MAX_CARDS, band step) on a real WebGPU run. -
v2 — lazy hierarchical layout in Rust (not started): lay out each cluster's interior on demand and move force/grid layout into the WASM engine with
rayon.
The layout half of this story — budget guards, the PivotMDS stress engine — is in Layout Engine Internals.
Deep, structured instrumentation — and the reason v1's calibration is even possible. Telemetry's stated focus is exactly LOD (the adaptive cut) and rendering, plus the analysis engine.
-
events.ts— a bounded ring buffer of{ t, category, level, event, data? }. Categories:analysis | layout | scene | lod | render | interaction | app. -
metrics.ts— named rolling histograms (count,mean,p50/p95/p99,min/max). -
telemetry.ts— the singleton bus:event(...),metric(...),count(...),time(...),toNDJSON(). Enabled by default, persisted tolocalStorage["polygraph.telemetry"]; when disabled, every hook is a zero-cost no-op. -
global-errors.ts+ the ReactErrorBoundary— capture uncaught errors / promise rejections / render crashes into the same log, so failures are visible rather than silent.
What's recorded: per-frame fps/ms and render-scene stats (nodes drawn/culled, edges encoded) from the Rust stats(); LOD recompute decisions; analysis timings (scanMs/analyzeMs/round-trip); and layout runs + cache hits.
Surfacing: there's no live HUD. The Settings pane has a Local logs on/off toggle and a Download log button (NDJSON). Console mirroring is gated by the toggle.
Performance and correctness are guarded by the bench/ suite — golden-graph snapshots, layout-stability (±2px), and a synthetic 100k-scale path. See Development & Building.
PolyGraph — explore, audit, enforce, and compare software architecture across 26 languages, 100% locally. Repository · Releases · Issues · Licensed Apache-2.0 OR MIT
📖 Using PolyGraph
- Installation
- Getting Started
- The Graph Model
- Layouts & Navigation
- Impact Analysis & Insights
- Query Language
- CLI: Rules & Diff
- Exports, Workspaces & Editor
- Troubleshooting & FAQ
🔧 Internals & Contributing