Co-Study4Grid / benchmarks /interaction_paint /FLUIDITY_FINDINGS.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
5.3 kB
# Pan/zoom fluidity — empirical evaluation of the "Smooth pan/zoom (GPU)" toggle
Grid: **pypsa_eur_eur220_225_380_400** (5247 VL / 8205 branches; real pypowsybl NAD
9.1 MB, ~99k–104k DOM nodes). Hardware: **Apple M4 Pro (Metal), Chrome 149, 120 Hz, dpr=1**.
Metric: **median rAF frame interval during a sustained 2.5 s gesture** (lower = smoother;
8.3 ms = 120 fps = idle ceiling). 100% "dropped" = never reaches the 120 Hz budget.
Harness: `bench_fluidity.html` driven over CDP (`cdp_driver.mjs`) in a Chrome launched
with anti-throttle flags so an occluded window still renders at full speed. The real-app
end-to-end numbers come from driving the actual `usePanZoom` handlers + the real Settings
toggle (`app_toggle_driver.mjs`, `app_n1_driver.mjs`).
## Results
| Measurement | gesture | plain (no cull) | **cull = toggle OFF** | **gpu transform = toggle ON** | toggle gain |
|---|---|---|---|---|---|
| Synthetic NAD (isolated) | pan | 91.7 | 58.3 | 49.8 | 1.17× |
| Synthetic NAD (isolated) | zoom | 108.6 | 65.0 | 49.6 | 1.31× |
| **Real pypowsybl NAD (isolated)** | pan | 100.0 | 58.0 | 41.9 | 1.38× |
| **Real pypowsybl NAD (isolated)** | zoom | 126.7 (p95 1008!) | 65.1 | 43.3 | 1.50× |
| **Real app, N tab (live usePanZoom)** | pan | — | 60.1 | 50.0 | 1.20× |
| **Real app, N tab (live usePanZoom)** | zoom | — | 75.0 | 50.1 | 1.50× |
| **Real app, N-1/Contingency tab** | pan | — | 82.6 | 58.4 | 1.41× |
| **Real app, N-1/Contingency tab** | zoom | — | 83.9 | 66.7 | 1.26× |
(N-1/action share the identical render path — `MemoizedSvgContainer` + `usePanZoom` +
`.svg-interacting`; the action tab differs only by a few highlight elements, so its
fluidity equals N/N-1.)
### Verdict on the toggle
- The toggle delivers a **real but modest ~1.2–1.5×** over the default. All four arms still
drop **100% of frames** — even ON, pan/zoom sit at **~15–24 fps**, never near 60/120.
- **Why it's small (proven):** a pure ±2 px CSS translate of the `will-change`'d `<svg>` layer
is *still* ~42–48 ms/frame (`micro_translate`). Chrome **re-rasterizes the ~100k-node vector
SVG on every transform** → `will-change:transform` never yields a reusable GPU texture, so
the gesture only saves the viewBox-attribute recompute + non-scaling-stroke re-eval.
## What WOULD make it fluid (proven)
**Rasterize the NAD to a `<canvas>` once at gesture start, transform the bitmap per frame,
bake back to viewBox on settle:** **8.3 ms = 120 fps, 0 dropped frames** — on both the
synthetic and the real NAD. That is **~6× over the toggle** and **~7–8× over the default**,
and it composites cheaply even on software/VDI (the case the GPU toggle deliberately skips).
### The catch (verified in code, Phase C)
The bitmap is rasterized via `new Image()`, which renders the SVG in isolation → **App.css
class-based styling is dropped**. On the **N-1 / action** tabs that means overload halos,
the contingency glow, and flow-delta colors **vanish** (they come from
`.nad-overloaded`/`.nad-contingency-highlight`/`.nad-action-target`/`.nad-delta-*` rules,
not inline attributes — `highlights.ts`). The N tab is bare, which is exactly why the
8.3 ms held there. → A production bitmap mode must **inline the highlight/delta computed
styles into the clone** before serializing, and fix cursor-anchored wheel-zoom (the
`getScreenCTM()` source moves from the live SVG to the canvas).
## Recommendations (ranked)
1. **(big bet) Bitmap-snapshot mode** as a 3rd `usePanZoom` mode (`'off' | 'gpu' | 'bitmap'`),
opt-in/default-OFF. Prereqs: inline halo/delta styles into the clone (fixes N-1/action
fidelity), strip `<foreignObject>` (canvas taint — bench already does), dpr-scale the
canvas, fix wheel-zoom CTM. Effort L–XL. Reference impl: `bench_fluidity.html`
`snapshotCanvas`/`runBitmap`.
2. **(quick win — REJECTED after measurement) Drop `vector-effect:non-scaling-stroke` during
`.svg-interacting`.** Phase C measured ~1.15× on the *synthetic* NAD, but a direct A/B on the
**real** pypowsybl NAD (`__runNss` arm, vector-effect verified flipping `none``non-scaling-stroke`)
gave **1.00× — zero gain — on both GPU and software rendering** (`--disable-gpu`): pan 50/50 ms,
zoom 58.3/58.3 ms. The real bottleneck is pure SVG raster; the stroke recompute is negligible.
**Not shipped** — ~60 lines of CSS + a halo/delta protection block for a measured no-op. The
synthetic 1.15× did not transfer. [IMPLEMENTED then reverted.]
3. **(SHIPPED) `commitViewBox` equality guard** (`usePanZoom.ts`) — free, avoids a settle-frame
React re-render when the viewBox nets back unchanged (the ~100k nodes are outside React's vdom
via `React.memo`+`replaceChildren`, so it's a tiny App-level saving, not a fps mover).
4. **(SHIPPED, hygiene) Clear `will-change` promptly** on settle in `endInteraction`
(smooth-mode only); does NOT shorten the 150 ms wheel debounce. Drops a promoted multi-MB
compositor layer between gestures. 0 fps, pure hygiene.
5. **Reject:** geometry-count culling (≤1.15×, some regress; `content-visibility:auto` is
0.89×; `.nad-edge` selectors don't even match the real grid) and a full canvas/WebGL
rewrite (same 8.3 ms ceiling at XL effort + breaks svgPatch/inspect/SLD/highlights).