Repository navigation
Conversation
SelectionManager added an anonymous mousedown listener to document that dispose() could not remove, and Terminal.dispose() cleared isOpen before the check that removes its mouseup listener. Both listeners kept a disposed terminal reachable; the first also kept its renderer and canvas, with the canvas's GPU backing store. An app that opens and disposes terminals over time keeps every canvas.
|
A second symptom of the same leak, from the Nimbalyst desktop app (Electron 43, Nimbalyst gives each terminal its own So in an app that rebuilds a terminal on each project switch, every new terminal in that window fails to start after roughly 60 switches, until a restart. See nimbalyst/nimbalyst#1818. Check in a real Electron We ship the same change as a pnpm patch for now (nimbalyst/nimbalyst#1819). The patch also sets |
Problem
dispose()leaves two listeners ondocument:SelectionManageradds an anonymousmousedownlistener (it tracks where a click started). Nothing keeps a reference to it, sodispose()cannot remove it.Terminal.dispose()setsisOpen = falsebeforecleanupComponents(), andcleanupComponents()removes themouseuplistener onlyif (this.isOpen …). So that listener stays too.The
mousedownlistener keeps the selection manager. The selection manager keeps the renderer, and the renderer keeps the canvas:An app that opens a new terminal each time the user moves to another one keeps every canvas. We found this in an Electron app that disposes a pane and opens a new one on each session switch. After four days the window held about 480 canvases, 12 GB of IOSurface in the GPU process. Blink counts canvas memory toward V8's global allocation limit, so V8 ran full GCs without a stop and the renderer used about 700% CPU.
Fix
mousedownhandler asboundDocumentMouseDownHandler, and remove it indispose()like the other document handlers.mouseuplistener without theisOpencheck. Removing a listener that was never added does nothing.Test
lib/terminal.test.tsgets a test that records each listener added todocumentwhile a terminal opens, and checks thatdispose()removes all of them. Before the fix, 2 are left.bun run fmt,bun run lintandbun run typecheckpass. I ranbun testagainst the 0.4.0ghostty-vt.wasm, because I did not build the WASM locally. With that file, one test fails,viewport scroll-growth corruption, and it fails with and without this change: it needs the WASM patch from #180.