Skip to content

fix(memory): Brain panel memory list accumulates event listeners and holds raster tiles #89

Description

@jdmanring

Summary

The Brain panel memory list grows RSS unbounded during hover interaction and does not reclaim memory after the panel closes. ~956 MiB RSS observed; closing the panel does not reduce it.

Root causes (in order of impact)

1. document.addEventListener accumulation (critical)
renderMemoryList() adds one document.addEventListener('click', ..., { once: false }) for every memory item on every render pass (line 956). With 50 items and 10 renders, 500 persistent document-level listeners accumulate, each holding a closure over a dropdown DOM element. These are never removed. Zero removeEventListener calls exist anywhere in memory.js (51 add / 0 remove).

2. No event listener cleanup on re-render
memoryList.innerHTML = '' at line 685 destroys the old item DOM nodes, but all their event listeners remain alive in memory until the GC happens to collect them. Because Qt does not forward OS memory pressure, that GC never fires during normal use.

3. Animations run when modal is hidden
All ::after sweep animations on memory items continue running when #memory-modal gains .hidden. The compositor keeps raster tiles allocated for all items (including off-screen). Qt never evicts them.

4. No cleanup or GC trigger on panel close
Closing the Brain modal only toggles .hidden. No event listeners are released, no open dropdowns are removed, no GC is triggered.

Fix

  • Add an AbortController scoped to each renderMemoryList() call. Abort the previous controller before re-rendering to release all item listener closures before the DOM is cleared. Thread { signal } through all 14 item-level addEventListener calls.
  • Move the document.addEventListener for outside-click dropdown close inside the menuBtn.click handler with { once: true, signal } — eliminates per-render accumulation and ensures it fires at most once per dropdown open.
  • Add a MutationObserver on #memory-modal that runs cleanup (close open dropdown, abort controller, trigger GC) when the modal becomes hidden.
  • CSS: add animation-play-state: paused when #memory-modal.hidden to stop all compositor animation work while the panel is not visible.
  • Add odysseus:modal-closed CustomEvent in modalManager.js (parallel to existing odysseus:modal-opened).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions