M11 renderer benchmark — 10,000 nodes
Status: one measurement, on one machine. This records what was observed, the method that produced it, and what it does not establish. It is not a claim that M11's acceptance criterion is met — that is the project owner's judgement at milestone acceptance, and it needs more than a single run.
The criterion
M11's acceptance line reads:
10k-node graph at 60 FPS on the dev machine (measured)
"60 FPS" is taken here to mean a frame budget of 16.67 ms, and the headline figure is the median frame interval over a sustained observation window. A single instantaneous reading is not evidence; neither is a mean, which one long pause can drag around. The minimum and the 95th percentile are reported beside the median, because a median that passes while one frame in twenty takes 40 ms is a stutter a person would see.
Method
The instrument is desktop/src/Benchmark.tsx, mounted at ?bench=auto. It is gated behind import.meta.env.DEV, and the guard wraps the dynamic import() itself rather than only the JSX — verified by building and confirming no benchmark chunk is emitted.
Fixture.
desktop/public/scene-10k.json, generated bycrates/cartograph-desktop/tests/fixture_10k.rsfrom the reallayout::layoutand the realscene::compose. Coordinates are not invented for the benchmark; if the layout engine changes, the fixture changes with it. Regenerate with:cargo test -p cartograph-desktop --test fixture_10k -- --ignored --nocaptureThe generator is asserted deterministic and asserted to keep its shape — many clusters, a realistic edge count, and genuinely disconnected nodes — so a change that quietly made the workload easier fails a test rather than improving the number.
Construction and initialisation are timed separately, using the same
buildGraphand the same exportedSIGMA_SETTINGSthe window uses.Sustained interaction. The camera is moved every frame for the whole window. This is the part that makes the number mean anything: Sigma only redraws when something changes, so a
requestAnimationFrameloop that left the camera alone would measure an idle browser and report a meaningless 60 FPS. Every sample below is a frame that actually drew 10,000 nodes and 11,444 edges.Warm-up. The first 30 frames are discarded — shader compilation and texture upload happen once and are not a sustained frame rate.
Window. 10 seconds, long enough to contain a garbage-collection pause. A two-second sample can miss one entirely and flatter the result.
The run happens inside the real desktop window — the one npm run tauri dev opens — not in a browser tab. WebView2 does not forward console.log to the launching terminal, so the page POSTs its result to a development-only Vite middleware that writes it to disk. That middleware is apply: "serve" and does not exist in a build.
Environment
| Machine | Intel Core 7 240H, 10C/16T · 23.6 GB RAM |
| OS | Windows 11 Home Single Language 10.0.26200 |
| Runtime | WebView2 / Chromium 151.0.0.0 (Edg/151.0.0.0), reported by the page itself |
| Tauri | 2.11.5 |
| Sigma | 3.0.3 · Graphology 0.26.0 |
| Build | debug (tauri dev), not a release build |
Workload
| Nodes | 10,000 |
| Edges | 11,444 |
| Clusters | 115 |
| Fixture | 1,745,219 bytes of JSON |
Results — six runs
Every run taken is reported. None was discarded, and the headline is the range rather than the best. Runs 1–3 were taken during Slice 3 and Slice 4; runs 4–6 during M11 acceptance, all with the window foregrounded.
| Run | Median | p95 | Worst | Frames | Over 16.67 ms |
|---|---|---|---|---|---|
| 1 | 9.60 ms | 13.00 | 39.80 | 993 | 1.31% |
| 2 | 9.40 ms | 13.60 | 44.20 | 997 | 1.10% |
| 3 | 9.80 ms | 13.70 | 40.10 | 966 | 0.83% |
| 4 | 10.00 ms | 14.30 | 46.80 | 925 | 1.41% |
| 5 | 7.60 ms | 11.50 | 37.90 | 1,228 | 0.49% |
| 6 | 6.70 ms | 11.10 | 31.50 | 1,326 | 0.08% |
Aggregate: median 6.70–10.00 ms, p95 11.10–14.30 ms, worst frame 31.50–46.80 ms, frames over budget 0.08%–1.41%.
Every median and every p95 is inside the 16.67 ms budget, in every run.
A correction to an earlier conclusion
An earlier version of this record reported a 6.1 ms median and explained it as a vsync floor on a 165 Hz display (1000/165 ≈ 6.06 ms), concluding the figure said nothing about headroom.
That reasoning was wrong and is superseded. Across the six runs above the median moves between 6.70 and 10.00 ms — which it could not do if it were hard bound to the refresh interval. Under load, frames take two refresh intervals instead of one. Headroom is finite and measurable, not unbounded.
The correct reading:
- the median is not a constant of the display; it responds to real load;
- the criterion is met on the evidence that matters — median and p95 inside the frame budget across six runs in varying machine states;
- the tail is the real limitation: worst frames of 31–47 ms, roughly one in a hundred, are visible stutter. A median inside budget does not remove them.
The superseded explanation is recorded here rather than deleted, because the mistake is instructive: a single run made a load-dependent number look like a hardware constant.
Judgement
PASS. Not on the median alone — on median and 95th percentile both inside 16.67 ms across six runs spanning idle and loaded states, with over-budget frames at or below 1.41%.
Recorded caveats: worst-frame stutter of 31–47 ms; a debug build; one machine; a 165 Hz display, on which a 60 Hz panel's behaviour cannot be inferred.
What this does not establish
firstFrameMsis not a usable measurement and is excluded. Across runs it varied roughly nineteen-fold, which no rendering cost explains; it tracks when the window was given focus, because WebView2 throttlesrequestAnimationFramefor a background window. Measuring time-to-first-frame properly needs the activation state controlled, which this harness does not do. The variation is reported rather than hidden and the number is not interpreted.- Six runs on one machine cannot characterise other hardware, and a 60 Hz panel would produce a different-looking median for the same renderer.
- Debug build. A release build would likely do better; no claim either way.
- Debug build. A release build would likely do better; no claim is made either way.
- The React wrapper is not measured. The benchmark constructs Sigma directly with the same graph and settings
GraphViewuses, so it covers the renderer and the data path but not the component's re-render behaviour. - This is one machine. Nothing here says anything about other hardware, and CI deliberately makes no FPS assertion — a hosted runner's frame rate is not reproducible enough to gate on.