Skip to content

Dashboard context governor: visibility-driven WebGL context recovery - #4

Merged
Alek99 merged 2 commits into
mainfrom
dashboard-context-governor
Jul 10, 2026
Merged

Dashboard context governor: visibility-driven WebGL context recovery#4
Alek99 merged 2 commits into
mainfrom
dashboard-context-governor

Conversation

@Alek99

@Alek99 Alek99 commented Jul 9, 2026

Copy link
Copy Markdown
Member

Problem

Browsers cap live WebGL contexts per page (~16 in Chrome) and LRU-evict the oldest on overflow. Measured on main with the dashboard telemetry harness: a 20-chart page permanently blanks its 4 earliest charts, a 50-chart page blanks 34 — all evicted during the create phase, with zero webglcontextrestored events, and scrolling back never recovers them.

Fix

The render client governs itself instead of letting the browser evict:

  • Context budget (default 12, window.FASTCHARTS_CONTEXT_BUDGET override). Before a view acquires a context at budget, the least-recently-visible off-screen view releases its own via WEBGL_lose_context — a controlled loss the existing, lifecycle-gated restore machinery undoes.
  • Visibility feed: a per-view IntersectionObserver (25% rootMargin hysteresis) maintains least-recently-visible order and re-acquires on scroll-into-view. Releases are demand-driven only, so fast scrolling never thrashes contexts.
  • Real evictions recover too (over-budget all-visible layouts): the dead canvas is swapped for a fresh clone (listeners retargeted) and rebuilt from the retained spec+payload — the same §18/§27 rebuild the restored path uses.
  • Observable (§28): data-fc-ctx on the canvas reads live / released / lost; views count releases/recoveries.
  • Under the budget nothing ever releases — small pages behave exactly as before.

The dashboard benchmark upgrades to measure this honestly: per-visit settle-wait scroll pass (reports recovery-latency p95), governed-release vs browser-eviction event classification, a governed health tier (fully usable above the budget), and a visible_stable_chart_ceiling. Verifier + fixtures enforce the new schema, including a test that an ungoverned eviction demotes a row to partial.

Perf: no degradation (measured, Chrome/macOS, same machine, 10/20/50 sweep)

metric main (ea589ac) this branch
10-chart health complete, 10/10, 0 losses complete, 10/10, 0 losses (unchanged)
10-chart render / heap 1823 ms / 11 MB 1816 ms / 11 MB
20-chart usable when visited 16/20 (4 permanently blank) 20/20, health governed
50-chart usable when visited 16/50 (34 permanently blank) 50/50, health governed
50-chart recovery p95 n/a (never recovers) 8.1 ms
50-chart render / heap 5439 ms / 28 MB 5339 ms / 28 MB
redraw-submission p95 ~0 ms ~0 ms
visible-stable ceiling 16 (hard browser cap) 50

Gates: 883 pytest, ruff, ty, check-benchmark-harness, artifact verification, render smoke (probe values byte-identical), real-Figure smoke, Reflex lifecycle smoke (18 charts × 2 loads, iframe remount/reload/hidden-boot-reveal, forced context-restore), interaction stress smoke (all gesture p95 budgets met, zero blank frames). The visual-regression smoke's histogram dark-pixel probe fails identically on unmodified main on this machine (local font AA) — pre-existing/environmental; CI should arbitrate.

Docs: CHANGELOG, benchmark.md notes, production-readiness "Not yet safe" reworded to the governed reality (don't claim more than ~budget simultaneously visible live charts).

Known limits: per-JS-realm governance (many same-origin iframes each budget independently); a layout keeping more than the budget visible at once can still hit browser eviction — now recovered on the next visibility pass instead of staying blank.

🤖 Generated with Claude Code

Alek99 added 2 commits July 9, 2026 14:36
Browsers cap live WebGL contexts per page (~16 in Chrome) and LRU-evict the
oldest on overflow, permanently blanking the earliest charts of a 20/50-chart
dashboard (measured: 16-of-50 usable, zero webglcontextrestored events).

The render client now governs itself instead of letting the browser evict:

- Context budget (default 12, window.FASTCHARTS_CONTEXT_BUDGET override).
  Before a view acquires a context at budget, the least-recently-visible
  off-screen view releases its own via WEBGL_lose_context — a controlled
  loss the existing (lifecycle-gated) restore machinery undoes.
- IntersectionObserver visibility feed (25% rootMargin hysteresis) tracks
  least-recently-visible order and re-acquires on scroll-into-view;
  releases are demand-driven only, so fast scrolling never thrashes.
- Real browser evictions (over-budget all-visible layouts) recover too:
  the dead canvas is swapped for a fresh clone (listeners retargeted) and
  rebuilt from the retained spec+payload.
- Observable per §28: data-fc-ctx on the canvas reads live/released/lost.
- Under the budget nothing ever releases — small pages are unaffected.

Dashboard benchmark upgrades to match: per-visit settle-wait scroll pass
(reports recovery-latency p95), governed-vs-evicted event classification,
a 'governed' health tier (fully usable above the budget), and a
visible_stable_chart_ceiling; verifier + fixtures enforce the new schema.

Measured (Chrome/macOS, 10/20/50 sweep): visible-stable ceiling 16 -> 50;
20/20 and 50/50 charts nonblank when visited (was 16 permanently); recovery
p95 8.1 ms at 50 charts; 10-chart dashboards stay 'complete' with zero
losses; render times, redraw-submission p95, and JS heap unchanged vs
baseline. Render/lifecycle/interaction smokes green (visual smoke's
histogram dark-pixel probe fails identically on unmodified main on this
machine — pre-existing, environmental).
@Alek99
Alek99 merged commit 75ce0db into main Jul 10, 2026
4 of 7 checks passed
FarhanAliRaza added a commit that referenced this pull request Jul 15, 2026
- Legend now (re)freezes options and swatch scaling via _attach() when
  added to an axes, so a legend constructed against one axes but
  attached to another picks up the host's dpi/rcParams (review #1).
- Legend.__init__ warns on handles/labels length mismatch and on
  handles without a legend entry instead of dropping them silently
  (review #4).
- JS tracks every legend box (not just the primary) so _resize
  refreshes max-height on extra legends too (review #2).
- JS line swatch uses ?? so an explicit lw=0 draws nothing instead of
  falling back to 1.5px (review #3).
- Drop unreachable return in _best_legend_loc (review #5).
FarhanAliRaza added a commit that referenced this pull request Jul 15, 2026
* Improve pyplot legend and dash parity

* Fix extra legend rendering regressions

* Address review: legend attach recompute, resize tracking, lw=0 swatch

- Legend now (re)freezes options and swatch scaling via _attach() when
  added to an axes, so a legend constructed against one axes but
  attached to another picks up the host's dpi/rcParams (review #1).
- Legend.__init__ warns on handles/labels length mismatch and on
  handles without a legend entry instead of dropping them silently
  (review #4).
- JS tracks every legend box (not just the primary) so _resize
  refreshes max-height on extra legends too (review #2).
- JS line swatch uses ?? so an explicit lw=0 draws nothing instead of
  falling back to 1.5px (review #3).
- Drop unreachable return in _best_legend_loc (review #5).

---------

Co-authored-by: Alek <alek@reflex.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant