fix: bound Monaco model lifecycle (#1407) and recover from worker-fallback breakage (#1406)#1435
fix: bound Monaco model lifecycle (#1407) and recover from worker-fallback breakage (#1406)#1435dawsontoth wants to merge 4 commits into
Conversation
…dels Monaco warns "potential listener LEAK detected" once ~200 text models are alive at the same time (each live TextModel holds a listener on a shared emitter, warning again every +100 — matching the 200/300 progression in the report, and one wild session that reached 1000). Three paths fed that population from the applications editor: - useApplicationTypeIntelligence registered a model for EVERY source file of the open project, unbounded. A 200+ file application trips the warning by design, and the same volume is eagerly structured-cloned to the TS worker — the source of the companion "DataCloneError: Data cannot be cloned, out of memory" / "FAILED to post message to worker" signatures (182 of each in a single RUM session). - The effect cleanup skipped whichever model was still attached to the editor, and `keepCurrentModel` kept it alive on swap — leaking one model per project switch and per applications-view visit, forever. - Installed packages loaded no intelligence at all, so every package file opened left a permanent model behind. Registration is now budgeted (150 models / 4 MiB total, with headroom under the 200-listener warning threshold) via selectFilesWithinModelBudget, and sweepStaleApplicationModels disposes detached file:/// models whenever the open file's project/package context changes — plus a deferred sweep at cleanup that catches the still-attached model once the editor releases it. Existing kept models are refreshed when their file's content changed. Verified live against stage: opening a file registers editor + siblings (13 models for the demo app), and three navigate-away/return cycles measure 13 -> 0 -> 13 -> 0 -> 13 -> 0 live models where the count previously only grew. Fixes #1407 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lback spam
When a language worker fails to spawn — a redeploy invalidated this tab's
hashed chunks ("Failed to fetch dynamically imported module"), the network
dropped, or the tab hit the OOM fixed in #1407 — Monaco silently swaps in a
main-thread fallback worker and caches it. That fallback cannot host foreign
worker modules (monaco-yaml, JSON, ...), so EVERY language-feature call —
folding, links, symbols, validation, code actions — rejects with "Missing
requestHandler or method: <fn>" for the rest of the session. RUM shows 161
such events from 7 sessions in 7 days, each session also carrying its trigger:
chunk fetch failures or the #1407 DataCloneError OOM.
Two changes:
- installStaleDeployReload: reload once on Vite's `vite:preloadError` (the
canonical recovery), so a stale deploy heals the session — routes AND
workers — instead of leaving the editor silently degraded. Rate-limited via
sessionStorage so persistent failures (offline) surface instead of looping.
- shouldKeepEvent: drop the "Missing requestHandler or method" per-call echoes
from RUM. The root causes carry their own, far more useful signals; the
echoes only add volume (dozens per broken session).
Reproduced live: blocking the YAML worker's spawn and opening config.yaml
yields the per-call error spam; dispatching `vite:preloadError` reloads once
and a second dispatch within the window is correctly suppressed.
Fixes #1406
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces Monaco model housekeeping to prevent listener leaks and out-of-memory crashes by capping sibling models and sweeping stale ones. It also adds a self-healing reload mechanism to recover from stale-deploy chunk failures and filters out noisy worker-fallback errors in Datadog. Feedback highlights a sliding-window bug in the rate-limiting logic of the reload mechanism, and suggests using a Set instead of a single module-level variable to track active projects to prevent race conditions when multiple hook instances are mounted.
Stamping sessionStorage on every vite:preloadError slid the cooldown window forward, so failures arriving faster than the window (e.g. a reload that landed on another stale copy of the app) suppressed recovery indefinitely. Stamp only when actually reloading: a fixed cooldown from the last reload. Review feedback from #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Fixes the two leaks it sets out to fix (#1407/#1406), with real tests covering the new bounded-registration and stale-sweep logic. A few things worth a look before this is fully closed out:
None of these are blockers — the PR is a real improvement over what's there — but #1 and #2 in particular mean the "bounded" in the title isn't fully true yet for the same-project case. — KrAIs |
…n the model budget Review follow-ups from #1435: - Same-project browsing was still unbounded: no context switch fires while staying inside one project, so opening many files the budget skipped (or peeked library declarations) grew the model population past the ceiling. An onDidCreateModel enforcer now retires the oldest detached file:/// models once the tab-wide ceiling is exceeded, never touching the just-created or attached model. - The character budget only seeded the model count, so a registration batch could stack another 4 MiB on top of already-live models. Both the count and the character budget now seed from every live file:/// model (getValueLength() is O(1)). - A package peek no longer evicts the active project's sibling set: the last real project's models are kept while the context is an installed package (or no file), so a brief detour doesn't force a full refetch. Unmount still releases everything. - Keep-prefixes are now built via monaco.Uri, so a project name that needs percent-encoding still matches uri.toString() output in the sweep. Verified live against stage: a package peek keeps all 13 project models (13 -> 14 -> 13, package model swept on return), and leaving the apps view still sweeps to 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @kriszyp — all four addressed in b7a7e1d:
15 housekeeping tests now (6 new), full suite green. 🤖 Addressed by Claude Code |
Summary
Fixes the two related Monaco RUM issues from the daily reviews:
[RUM] Monaco editor: event listener leak (200-300+ listeners) on cluster view #1407 — listener leak (200–1000 listeners) / model leak. Each live Monaco text model holds a listener on a shared emitter; Monaco warns at 200 and every +100 after. Three paths grew the model population without bound:
useApplicationTypeIntelligenceregistered a model for every source file of the open project, uncapped — a 200+ file app trips the warning by design, and the same content is eagerly structured-cloned to the TS worker, producing the companionDataCloneError: … out of memory/FAILED to post message to workererrors (182 of each in one RUM session, which also logged the leak warning at 1000 listeners).keepCurrentModelthen kept alive forever — one leaked model per project switch and per applications-view visit.Fix: new
modelHousekeepinghelpers — registration is budgeted (150 models / 4 MiB total, headroom under the 200-listener threshold, logged when trimmed), and a sweep disposes detachedfile:///models whenever the open file's project/package context changes, plus a deferred sweep that catches the still-attached model once the editor releases it.[RUM] Monaco editor: missing LSP request handlers (getFoldingRanges, findDocumentSymbols, findLinks, doValidation, getCodeAction) #1406 —
Missing requestHandler or method: getFoldingRanges/findDocumentSymbols/findLinks/doValidation/getCodeAction. When a language worker fails to spawn, Monaco silently swaps in a main-thread fallback that cannot host foreign worker modules (monaco-yaml, JSON, …) and caches it — after which every language-feature call rejects with this error for the rest of the session. RUM shows all 7 affected sessions (161 events / 7 days) also carry the trigger: stale-deploy chunk failures (Failed to fetch dynamically imported module), network errors, or the [RUM] Monaco editor: event listener leak (200-300+ listeners) on cluster view #1407 OOM.Fix:
installStaleDeployReloadreloads once on Vite'svite:preloadError(rate-limited via sessionStorage so offline sessions don't loop), healing routes and workers after a redeploy;shouldKeepEventdrops the per-call error echoes from RUM since the root causes carry their own signals.Verification
config.yamlreproduces the per-call error spam; dispatchingvite:preloadErrorreloads exactly once and a second dispatch inside the rate-limit window is suppressed.Fixes #1407
Fixes #1406
🤖 Generated with Claude Code