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 transformwill-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 nonenon-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).