Share WebGL context budget across same-origin iframes - #149
Conversation
The context governor caps live WebGL contexts per document, but the browser's ~16-context limit is process-wide — shared across every same-origin iframe in a tab. A page that renders each chart in its own iframe (docs sites, SaaS dashboards, the examples/fastapi gallery, which needs iframes to host each standalone to_html document) defeated the governor entirely: each per-document governor saw only its single chart and never released, so once enough iframes loaded the browser began LRU-evicting live charts, and the evicted charts fought to recover and re-evict — a scroll-driven "Too many active WebGL contexts" storm. Coordinate one shared budget across same-origin frames over a BroadcastChannel: each frame announces its live-context count, and any frame over the shared budget sheds its own off-screen views (never a visible one, so a sibling frame loading cannot blank a chart the user is looking at). IntersectionObserver already clips to the top-level viewport, so an off-screen iframe's chart correctly reports not-visible — the budget accounting was the only gap. Cross-origin and sandboxed frames get an isolated channel scope and fall back to the per-document behavior. Reproduced with the real bundle (24 lazy iframes): before, 8 charts browser-evicted and 45 "Too many active WebGL contexts" warnings on scroll; after, 0 evictions and 0 warnings, off-screen charts released under the governor and revived on scroll-in. Single-document dashboards are unchanged (no peer frames -> foreignLive() is 0 and every added path is a no-op). Regenerate the static bundles, guard the new machinery with client-source marker tests, and update the dossier (Sec. 18), deployment matrix, and production-readiness notes.
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThis PR shares the WebGL context budget across same-origin frames. The main changes are:
Confidence Score: 5/5This looks safe to merge. The latest changes restore frame membership after cache restoration. Peer state is rebuilt to avoid retaining frames that left while frozen. Context recovery waits for the asynchronous loss event. No additional blocking issue was found in the changed code. No files require additional attention.
What T-Rex did
Important Files Changed
Reviews (3): Last reviewed commit: "Drop stale peer membership on bfcache re..." | Re-trigger Greptile |
Follow-ups on the shared-budget governor, found via the failing reflex_lifecycle_smoke index-iframe pass and PR review: - Recovery race (the smoke failure): a governed release is WEBGL_lose_context.loseContext(); re-acquire is restoreContext(). Chromium silently drops a restoreContext() issued before that context's webglcontextlost event has dispatched — or synchronously inside the dispatch — leaving the canvas lost forever. A chart shed by a peer's message and scrolled back into view in the same task hit exactly that window, so it never repainted (the index gallery left the 11th chart blank). Defer recovery until the loss event lands (_ctxLostPending) and retry on a fresh task. - Back/forward cache: pagehide posts "bye" (a frozen frame cannot participate), but nothing re-announced on restore, so peers omitted a bfcache-restored frame indefinitely and the page could exceed the cap. Re-announce on pageshow(persisted). - Concurrent shedding: frames over budget released the whole computed excess from an async snapshot, so several frames observing the same over-budget state each dropped the full deficit and collectively over-released. Shed one off-screen view per turn, announce, and re-evaluate against the fresher count — converging on the budget. Verified against the real example (all 20 index iframes paint; lifecycle smoke passes) and the many-iframe repro (0 "Too many active WebGL contexts" warnings, 0 browser evictions). Regenerate the static bundles, extend the client marker tests, and update the dossier (Sec. 18).
|
Pushed 6cba934 — it fixes the CI failure and both review findings, which turned out to share a root cause in the governed release/recover path. CI ( Concurrent shedding (P1, line 213): frames now release one off-screen view per event-loop turn, announce, and re-evaluate against the fresher count — so several frames observing the same over-budget snapshot converge on the budget instead of each dropping the full deficit. (Over-releasing an off-screen view was always safe — it revives on demand — but this avoids the needless rebuilds.) BFCache (P1, line 159): re-announce on Verified locally against the real example — all 20 index iframes paint and One caveat on the BFCache fix: a true bfcache restore can't be exercised in headless Chromium (it reports Generated by Claude Code |
A document frozen in the back/forward cache can miss a peer's `bye` (BroadcastChannel does not deliver to a frozen frame), so after restore its `foreign` map may still count contexts for frames that have since gone away — over-counting the shared budget and triggering unnecessary off-screen releases. Clear the map on `pageshow(persisted)` and rebuild it from live peers' replies to the re-`hello`, instead of carrying the stale membership forward. Regenerate the static bundles and add a marker for the clear.
Summary
Extend the WebGL context governor to coordinate a shared budget across same-origin frames using
BroadcastChannel. This prevents "Too many active WebGL contexts" storms on chart-per-iframe pages (docs sites, SaaS dashboards, theexamples/fastapigallery) where the browser's process-wide cap is shared but the per-document governor sees only its own charts.Changes
Cross-frame coordination via BroadcastChannel: Each frame announces its live-context count and listens to peers' counts. The effective budget is now
localLive() + foreignLive() - budget()instead of justlocalLive() - budget().New governor methods:
_initCrossFrame(): Lazily initialize the channel on firstregister(), with frame ID and peer tracking_onForeignMessage(): Handlelive(count update),hello(peer joined), andbye(peer left) messageslocalLive()/foreignLive(): Count live contexts in this frame and across peers_announceLive(force): Broadcast this frame's live-context count when it changes (deduped)_rebalance(): Release off-screen views when the shared budget is exceededVisibility-aware shedding:
_rebalance()only releases off-screen views (!view._ctxVisible), never visible ones — a sibling frame loading cannot blank a chart the user is looking at.Announcement hooks: Call
_announceLive()after context loss, recovery, rebuild, and release; call_rebalance()when a chart scrolls off-screen.Graceful degradation: Cross-origin frames, sandboxed contexts, and browsers without
BroadcastChannelfall back to per-document budgeting. Coordination is best-effort: transient overshoots during synchronous construction are self-healing, and a crashed frame that never sendsbyeonly lowers the effective budget (a few extra off-screen releases, revived on demand).Implementation Details
${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}to avoid collisions._announcedLivecache dedupes redundant broadcasts when the count hasn't changed.IntersectionObserveralready reports off-screen iframes' charts as not-intersecting (clips to top-level viewport), so visibility signals are correct across frame boundaries.spec/design-dossier.md(§18) andspec/process/production-readiness.mdto document the shared-budget behavior and cross-origin fallback.tests/test_static_client_security.pyto prevent silent removal of the BroadcastChannel machinery.https://claude.ai/code/session_017AuTTjAitxe94Lp3tPEtN4