Local Performance Evidence
Graphora performance timings are local diagnostic evidence, not cross-machine guarantees or CI thresholds. The deterministic fixtures exercise the Canvas 2D renderer and browser Worker force layout; they do not establish arbitrary large-graph support.
2026-09-10 Atomic Layout Application And Worker Run
Command: pnpm perf:manual
Environment: Node v22.23.2, Chromium 149.0.7827.55, Linux 6.8.0-138-generic x64, AMD EPYC-Milan Processor. Three runs per scenario; figures below are medians in milliseconds. The deterministic force seed was 82; the 1,000-node cases used 90 iterations and the 5,000-node cases used 120.
| Nodes | Edges | Density | Worker compute | Atomic apply | First painted nonblank Canvas | Ready | Click highlight |
|---|---|---|---|---|---|---|---|
| 1,000 | 1,800 | 1.8 | 641.9 | 8.5 | 44.4 | 728.3 | 40.8 |
| 1,000 | 3,000 | 3.0 | 599.9 | 9.7 | 69.8 | 725.3 | 74.9 |
| 5,000 | 9,000 | 1.8 | 4,078.6 | 25.0 | 344.9 | 4,516.4 | 198.1 |
| 5,000 | 15,000 | 3.0 | 4,084.1 | 36.7 | 409.8 | 4,615.6 | 302.4 |
The triggering Node diagnostic used the same 1.8-edge-per-node shape and found layout-result application at about 2,680 ms for 1,000 nodes and 148,328 ms for 5,000 nodes. It exposed that GraphEngine.applyLayoutResult called the single-node mutation once per result, rebuilding graph indices each time. The atomic position operation now validates the full result and rebuilds once.
The before and after figures came from different harnesses, so they demonstrate the removed algorithmic bottleneck rather than a controlled benchmark ratio. Use pnpm perf:manual for future like-for-like runs. The JSON Lines output includes exact counts, run count, browser/Node/OS/CPU metadata, and median/min/max for every measured phase.
A repeat of the original Node diagnostic after the change, on Node v22.23.2 and the same CPU category, measured result application at 21.66 ms for 1,000/1,800 and 62.98 ms for 5,000/9,000. This corroborates the removed repeated-rebuild bottleneck; the browser harness remains the maintained measurement surface.
2026-09-10 Clustered And Dense-Hub Baseline
Command: GRAPHORA_BASE_URL=http://127.0.0.1:4182 pnpm perf:manual
Source: 69958da32c8b2bb8413f31330ae79faba1c11bfe, clean working tree. Environment: Node v22.23.2, headless Chromium 149.0.7827.55, Linux 6.8.0-139-generic x64, AMD EPYC-Milan Processor, 1280 × 800 viewport. Three runs per scenario; figures below are medians in milliseconds. Fixture seed 20260910 and layout seed 82 were held constant. The 1,000-node cases used 90 force iterations and the 5,000-node cases used 120. The eight raw JSON Lines records, including every min/median/max value, are in performance-evidence-2026-09-10.jsonl.
| Shape | Nodes | Edges | Maximum degree | Worker compute | Atomic apply | First nonblank Canvas | Ready |
|---|---|---|---|---|---|---|---|
| clustered | 1,000 | 1,800 | 4 | 552.2 | 7.4 | 37.5 | 629.2 |
| clustered | 1,000 | 3,000 | 6 | 589.6 | 8.8 | 49.1 | 729.5 |
| clustered | 5,000 | 9,000 | 4 | 3,463.3 | 24.1 | 112.1 | 3,731.3 |
| clustered | 5,000 | 15,000 | 6 | 3,511.1 | 31.2 | 151.0 | 3,893.0 |
| dense hubs | 1,000 | 1,800 | 403 | 774.2 | 10.1 | 43.2 | 874.0 |
| dense hubs | 1,000 | 3,000 | 1,003 | 722.6 | 14.6 | 49.5 | 847.7 |
| dense hubs | 5,000 | 9,000 | 411 | 4,084.1 | 32.0 | 115.8 | 4,348.1 |
| dense hubs | 5,000 | 15,000 | 1,012 | 4,805.1 | 35.5 | 182.2 | 5,180.4 |
The interaction workload targets the highest-degree node. Hit and miss picking figures are totals for five synchronous public CanvasGraphRenderer.pick calls. Render-boundary figures include the public update, scheduled render and two requestAnimationFrame callbacks. They do not claim that pixels reached a physical display.
| Shape | Nodes / edges | Highlighted edges | Pan boundary | Zoom boundary | 5 node hits | 5 full misses | Highlight derivation | Highlight boundary |
|---|---|---|---|---|---|---|---|---|
| clustered | 1,000 / 1,800 | 4 | 32.4 | 32.3 | 1.8 | 11.9 | 0.6 | 25.0 |
| clustered | 1,000 / 3,000 | 6 | 42.9 | 32.2 | 1.8 | 18.3 | 0.8 | 30.8 |
| clustered | 5,000 / 9,000 | 4 | 107.3 | 123.3 | 5.5 | 61.8 | 1.0 | 111.6 |
| clustered | 5,000 / 15,000 | 6 | 139.4 | 122.4 | 5.2 | 85.1 | 2.4 | 142.9 |
| dense hubs | 1,000 / 1,800 | 403 | 41.0 | 30.0 | 1.8 | 10.1 | 8.8 | 34.1 |
| dense hubs | 1,000 / 3,000 | 1,003 | 45.1 | 38.9 | 1.5 | 19.2 | 27.1 | 67.5 |
| dense hubs | 5,000 / 9,000 | 411 | 104.9 | 103.4 | 3.7 | 65.0 | 31.3 | 133.1 |
| dense hubs | 5,000 / 15,000 | 1,012 | 144.0 | 121.2 | 5.2 | 90.3 | 91.1 | 244.0 |
The next measured optimization should target dense neighborhood highlighting before a general spatial index. At 5,000/15,000, dense-hub highlight derivation rose from 2.4 to 91.1 ms and its render boundary rose from 142.9 to 244.0 ms, while a full-scene miss averaged about 18.1 ms per call. Source inspection shows linear endpoint lookup during neighborhood derivation and repeated array membership checks during interaction style resolution. A follow-up should use the public render lifecycle to separate snapshot preparation from Canvas draw, then test indexed endpoint lookup and set-backed membership against identical highlight results. Geometry reuse for full-scene miss picking remains the next profiled candidate. These results do not establish an optimization speedup; they are the controlled pre-change baseline.
2026-09-10 Matched Dense-Neighborhood Optimization
Command: GRAPHORA_BASE_URL=http://127.0.0.1:4182 node scripts/performance-benchmark.mjs --nodes 5000 --density 3 --runs 3
The baseline artifact came from clean revision 3a40131; the optimized artifact came from clean revision 0ad5be1. Both were served as prebuilt static output and measured sequentially with Node v22.23.2, headless Chromium 149.0.7827.55, Linux 6.8.0-139-generic x64, an AMD EPYC-Milan Processor and a 1280 × 800 viewport. Fixture seed 20260910, layout seed 82, 5,000 nodes, 15,000 edges, 120 layout iterations and three runs per shape were held constant. Other worktrees were running release checks and Playwright workers on the shared host during this window. The min/max ranges below expose the resulting variability. The four complete JSON Lines records are appended to performance-evidence-2026-09-10.jsonl.
| Shape | Revision | Highlight derivation median (min–max) | Schedule + snapshot median (min–max) | Canvas call median (min–max) | Highlight boundary median (min–max) |
|---|---|---|---|---|---|
| clustered | baseline 3a40131 | 5.6 (2.3–6.2) | — | — | 202.5 (158.4–242.0) |
| clustered | optimized 0ad5be1 | 1.7 (1.5–2.0) | 67.9 (58.7–71.0) | 60.6 (50.9–67.4) | 158.1 (138.2–190.5) |
| dense hubs | baseline 3a40131 | 104.3 (93.9–106.8) | — | — | 291.9 (243.2–309.9) |
| dense hubs | optimized 0ad5be1 | 2.8 (2.4–2.9) | 70.0 (66.1–71.8) | 33.7 (32.0–49.8) | 140.7 (135.9–153.4) |
For this matched dense-hub fixture, the endpoint lookup and immutable-state membership changes reduced median neighborhood derivation by 97.3% and the two-frame highlight boundary by 51.8%. The dense derivation ranges did not overlap. The clustered derivation is close to the timer floor and its boundary ranges overlap, so that row supports semantic and workload comparison rather than a stable speedup claim.
The two new optimized-only phases provide attribution, not a before/after phase comparison. render:before fires after snapshot construction, so “schedule + snapshot” measures the public update, scheduling delay and snapshot preparation. The Canvas figure times calls to CanvasGraphRenderer.render; it does not measure physical display paint. The two-frame boundary also contains the small cost of the added lifecycle listener and renderer wrapper. Historical 302.4 ms click timing used a different harness and is not part of the speedup calculation.
This completes the first measured GAP-R11 highlight slice. Full-scene miss picking, geometry reuse, a spatial index, culling, pan/zoom invalidation and general LOD remain separate measured candidates. These local results do not add a graph-size, frame-rate or device/browser support guarantee.
2026-09-11 Spatial scene cache, picking and culling
SOTA-02 implements immutable scene caching, bounded lazy spatial grids, conservative viewport culling and reuse of world items during camera changes. Mutable direct renderer snapshots retain the reference path. Labels keep the full placement pass. The existing performance example includes a repeatable lifecycle workload button.
Budgets were recorded before optimization in the spec: 25% faster five-miss median for both 5k fixtures and no more than 20% first/pan/zoom regression. Both budgets pass in the reviewed paired measurements below. At 5k, misses fell from 112.7/88.7 ms to 4.2/4.0 ms (96.3%/95.5%); dense-hub first-render increased 19.6%, within the budget but a remaining optimization opportunity. This does not establish a 60 FPS or 10k support guarantee.
Same deterministic density-3 fixtures, seed 20260910, force seed 82, 1280×800, Node 22.23.2, headless Chromium 149.0.7827.55, Linux 6.8.0-139-generic x64, AMD EPYC-Milan Processor. Baseline revision ae99232; reviewed runtime cf1f800. d1c72ce changes only the optional lifecycle diagnostic, not measured runtime. A subsequent compatibility fix retains an already deeply frozen interaction object by identity; timing figures above/below identify the measured revision, not that later identity-only fix. Three samples per 5k/10k shape. Early first-render regressions led to removing snapshot copies and camera-time style reconstruction, then repeating the baseline. The remaining 1k boundary variation triggered one seven-sample pair on unchanged runtime; that larger pair is reported below. All earlier results remain available, including failed intermediate budgets and dirty diagnostic-only worktree metadata.
| Shape | Nodes | First nonblank (ms) | Pan boundary (ms) | Zoom boundary (ms) | Five node hits (ms) | Five misses (ms) |
|---|---|---|---|---|---|---|
| clustered | 1000 | 49.1 → 46.5 | 43.2 → 30.5 | 34.8 → 31.8 | 2.7 → 2 | 16.2 → 1.4 |
| dense-hubs | 1000 | 74.3 → 52.6 | 44.3 → 34.9 | 50.5 → 29.3 | 1.8 → 1.7 | 20.6 → 1.5 |
| clustered | 5000 | 190.4 → 181.6 | 155 → 119.8 | 136.1 → 114.1 | 5.9 → 3 | 112.7 → 4.2 |
| dense-hubs | 5000 | 150.3 → 179.7 | 145.3 → 92.7 | 135.3 → 95.1 | 3.6 → 4 | 88.7 → 4 |
| clustered | 10000 | 305.3 → 353.1 | 265.2 → 155.6 | 296.1 → 220.4 | 9.5 → 4.3 | 223.6 → 14.7 |
| dense-hubs | 10000 | 287.8 → 309.3 | 272 → 170.8 | 279 → 174 | 6 → 5.4 | 225.8 → 5.9 |
Values are baseline → reviewed medians. The 10k comparison is additional evidence, using the original baseline, not the later 1k/5k pair. Timing boundaries include scheduling and animation frames, not physical display paint. Initial lazy index construction is included in the first hit/miss workload, not hidden by a dedicated warm-up. Geometry/bounds construction is included in render time. The hit workload builds the node grid; the miss workload builds the edge grid on its first call.
All 32 raw scenario records retain phase, exact revision, environment and min/median/max. Reproduce each phase using pnpm perf:manual -- --nodes 5000 --density 3 --runs 3; use 1000/10000 as needed, and --runs 7 for the larger 1k pair. Use the corresponding revision's prebuilt artifact. This is a shared VM with no isolated CPU reservation; small boundary changes are noisy and the recorded ranges matter.
Disposal and memory
After pnpm build && pnpm examples:build:performance-medium, run node scripts/renderer-lifecycle-diagnostic.mjs. It uses real Canvas and default ResizeObservers, five warm-up cycles, then three batches of ten 1k/3k-node/edge engines, each mounted twice, rendered, picked, unmounted and destroyed. The raw Chromium CDP record includes forced-GC heap and DOM counts after each batch. Across the 30 measured destroys / 60 mounts, DOM nodes stayed 56, listeners 19 and backing storage 963,068 bytes. Retained JS heap rose 102,912 bytes from the warm baseline (3,063,108 to 3,166,020); batch increments diminished. No host children remained. This bounded diagnostic does not prove zero memory leaks. Repeated workload excludes layout workers; existing worker lifecycle tests cover cancellation/termination separately.
Coverage here is automated headless Chromium on this Linux VM. Physical devices, other browser engines and assistive technologies were not verified by this run. Broader combined-feature profiling remains SOTA-24; LOD remains SOTA-08.
Explicit display detail (SOTA-08a)
Paired three-run measurements at clean revision 8be7ff1, Node 22.23.2, Chromium 149.0.7827.55 headless, Linux x64 AMD EPYC-Milan, 1280×800. Raw results include full commands, seeds, ranges and environment. Identical 1,000-node / 1,800-edge clustered graphs label every node and edge. Worker layout uses 90 iterations and the same seed.
| Metric | Off median (range), ms | Auto median (range), ms |
|---|---|---|
| First render | 63.8 (62.5–68.0) | 41.3 (36.7–41.9) |
| Pan boundary | 43.8 (37.3–45.0) | 27.1 (25.2–27.5) |
| Zoom boundary | 49.7 (49.1–50.7) | 33.4 (33.3–33.5) |
At the fitted initial viewport, off sends all 2,800 labels to Canvas text drawing; auto sends zero (node count and fitted zoom independently reduce detail). labelWorkload counts policy-eligible labels from that exact fitted snapshot, after timing; default Canvas all mode draws those labels. It is not a count of on-screen glyphs or measured pixels. Interaction workload subsequently selects a node, whose label remains eligible. Unit/browser evidence verifies suppression before measurement, direct interaction exemptions, exact picks and matching PNG.
These small local samples demonstrate an attributable label-work reduction, not a cross-device performance guarantee. No numeric release threshold is added. Earlier exploratory results used a pre-fit label counter and are excluded from this comparison; timing was not used to select a favorable run. Viewport-motion and custom-visual policies remain subsequent SOTA-08 work.