Skip to content

fix(frontend): release listeners and registries that outlive owners - #831

Merged
Neonforge98 merged 1 commit into
perf/heavy-component-leaksfrom
perf/frontend-lifecycle-leaks
Aug 17, 2026
Merged

fix(frontend): release listeners and registries that outlive owners#831
Neonforge98 merged 1 commit into
perf/heavy-component-leaksfrom
perf/frontend-lifecycle-leaks

Conversation

@Harry19081

Copy link
Copy Markdown
Member

Stacked on #830 (perf/heavy-component-leaks) → #829. Merge in order; this PR's diff is only the top commit.

Summary

Third frontend RAM pass: two thorough lifecycle sweeps (effects without cleanup, module-scope maps, registries holding DOM/xterm/EditorView, xterm/webview teardown, per-session atom families) over WorkStation, engines, hooks and scaffold. The codebase turned out disciplined; this PR fixes the genuine defects found — every change is behavior-neutral (nothing a user can see changes; only what is retained after it should have been released). Each has a regression test.

Problem

  • SnapshotCacheManager.subscribeSession returned a disposer that captured the listener Set it was created with and deleted the registry entry whenever that Set emptied. evictSessionCache (reached by reload / manual compact / edit-message / sidebar delete while consumers are still mounted) drops the entry; a later subscriber installs a fresh Set, and the stale disposer then silently unregistered the live Set — mounted consumers stopped receiving es:changed pushes and pinned their last full snapshot.
  • useSearchResults.loadMore unlistened its two Tauri listeners only on the success path; a rejected searchCodeStreaming (invalid regex, repo unmounted) left both — each closing over the entire current result set — registered for the process lifetime and running on every later search-result event.
  • OSC-633 command-detection state (up to 200 command entries per terminal) was never pruned: removeCommandDetectionAtom had zero callers.
  • Deleting a session disposed its tab workspace but not its session:<id> editor cache / active-repo pointer (heap + a localStorage blob parsed at boot).
  • Chat/Communication/config.ts kept a module-scope single-slot memo of the last-built session's full SessionEvent[] and MessageEntry trees, outliving unmount / session switch / session close.
  • A WebGL addon that threw inside loadAddon/activate was never disposed while its budget slot was returned (orphaned GL context; the 8-slot budget could be exceeded).
  • TurnMetadataFooterSlot touched turnMetadataAtomFamily with sessionId ?? "", creating empty-session-id entries that the loader's GC never retains or removes.

Solution

  • Disposer re-looks-up the current Set and only removes the registry entry when it is its own (snapshotCacheManager.ts).
  • Listeners hoisted and released in finally (useSearchResults.ts).
  • removeCommandDetectionAtom called from both terminal-removal paths (terminal/index.ts).
  • New disposeEditorCacheForSessionAtom (editorCache.ts), wired into the single dispose callback both session-delete paths use (useWorkstationSidebarHandlers.ts).
  • Memo → identity-keyed WeakMap (same hit semantics, no retention).
  • webglAddon?.dispose() on the throw path in terminalSetup.ts and XtermOutput/index.tsx.
  • TurnMetadataFooterSlot split into a wrapper (null/visibility gate) + body that only reads the family with a real session id.

Potential risks

  • Low. The disposer change only affects the stale-disposer-after-evict case; the normal last-subscriber-removes-entry path is unchanged and tested. WeakMap memo hits are a superset of the old single-slot memo (never stale — keyed by array identity). Everything else is cleanup on close/error paths.
  • Not changed (documented in the audit fix log as follow-ups): LRU-capping saved tab layouts across live sessions (would drop old sessions' tab state), cursorIdeTurnSummariesAtomFamily retention for browsed Cursor sessions (needs mount-gated GC), two ~100 B/session maps with reconcile semantics, and the unreferenced search/fileTrackingAtom.ts + search/cacheAtom.ts modules (dead-code deletion candidates).

Validation / Test plan

  • pnpm typecheck clean; lint-staged (eslint + tsc) passed on commit.
  • New tests: subscribeSessionDisposer.test.ts (stale disposer must not unregister a newer live Set), useSearchResults.loadMoreListeners.test.ts (both listeners released on rejection), commandDetectionLifecycle.test.ts (both removal paths prune), editorCache.test.ts dispose case (memory + storage), utils.test.ts memo identity/independence cases.
  • 137 test files / 1 172 tests green across the touched areas (SessionCore store, terminal, XtermOutput, ChatHistory, Communication, EditorPrimarySidebar, NavigationSidebar, workstation store).
  • Manual: reload / manual compact a session with subagents and confirm the chat keeps updating; trigger a failing search load-more then run further searches; open/close terminals; delete a session that had editor tabs.

Lifecycle-leak sweep over WorkStation, engines and hooks. All fixes are behavior-neutral: subscribeSession disposer no longer unregisters a newer live listener Set after evictSessionCache; loadMore search listeners are released in finally (rejected requests leaked both); command-detection state is pruned when a terminal is removed; a deleted session's editor cache and active-repo pointer are disposed with its workspace; the Communication message-list memo moved from module-scope slots to a WeakMap keyed by the events array; partially activated WebGL addons are disposed on the throw path before their slot is released; TurnMetadataFooterSlot only touches the turn-metadata family with a real session id. Tests cover each. Full audit notes in docs/memory-audit-2026-08-16.

Pre-commit hook ran. Total eslint: 0, total circular: 0
@Neonforge98
Neonforge98 merged commit 282b6ad into perf/heavy-component-leaks Aug 17, 2026
1 check passed
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.

2 participants