From 7a6e724bfe7093beae13e2f2ea1f6e5df5d5847b Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Fri, 17 Apr 2026 13:32:01 -0400 Subject: [PATCH 1/2] Fix dispose leaks: register CursorBlinkStateManager + _pausedResizeTask MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related disposable-registration gaps that leak xterm Terminal instances when the host application unmounts a Terminal. Evidence + full writeup at juspay/kolu#606. 1. addon-webgl/WebglRenderer._cursorBlinkStateManager was declared `= new MutableDisposable()` without the `this._register(...)` wrapper that every sibling MutableDisposable in the class uses. CursorBlinkStateManager runs a setInterval for the cursor blink and its `dispose()` correctly clears it — but dispose() was never called because the disposable wasn't registered. The live timer kept the renderer (and thus the Terminal) alive past host unmount. 2. common/TaskQueue.DebouncedIdleTask had no `dispose()` method. Added one that clears the internal IdleTaskQueue (cancelling any pending idle callback). 3. browser/services/RenderService._pausedResizeTask (a DebouncedIdleTask) was not registered. Wrapped with `this._register(...)` so RenderService.dispose propagates. Verified in a downstream app (kolu) with a repro that toggles Focus vs Canvas layout modes: - Pre-fix: 24/28 disposed xterm Terminals retained past host unmount + forced GC. - Post-fix: 0/28 retained. Retention chain (before fix): DOMTimer → ScheduledAction → V8Function → CursorBlinkStateManager._renderCallback → WebglRenderer → Terminal --- addons/addon-webgl/src/WebglRenderer.ts | 2 +- src/browser/services/RenderService.ts | 2 +- src/common/TaskQueue.ts | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index e985bf2ef..83f20c9e7 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -30,7 +30,7 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; - private _cursorBlinkStateManager: MutableDisposable = new MutableDisposable(); + private _cursorBlinkStateManager: MutableDisposable = this._register(new MutableDisposable()); private _textBlinkStateManager: TextBlinkStateManager; private _charAtlasDisposable = this._register(new MutableDisposable()); private _charAtlas: ITextureAtlas | undefined; diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index ada3df307..83e3d3379 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -68,7 +68,7 @@ export class RenderService extends Disposable implements IRenderService { ) { super(); - this._pausedResizeTask = new DebouncedIdleTask(this._logService); + this._pausedResizeTask = this._register(new DebouncedIdleTask(this._logService)); this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), this._coreBrowserService); this._register(this._renderDebouncer); diff --git a/src/common/TaskQueue.ts b/src/common/TaskQueue.ts index 6f3b40bc4..3cad0a140 100644 --- a/src/common/TaskQueue.ts +++ b/src/common/TaskQueue.ts @@ -168,4 +168,8 @@ export class DebouncedIdleTask { public flush(): void { this._queue.flush(); } + + public dispose(): void { + this._queue.clear(); + } } From 08ae141ef7865a16578c8b0bb9a0cec0dca905b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:13:57 -0700 Subject: [PATCH 2/2] Improve overview ruler lifecycle Fixes #5826 --- .../decorations/OverviewRulerRenderer.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index 4302b3420..b28b25965 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -94,6 +94,12 @@ export class OverviewRulerRenderer extends Disposable { this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true))); this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true))); this._register(this._themeService.onChangeColors(() => this._queueRefresh())); + this._register(toDisposable(() => { + if (this._animationFrame !== undefined) { + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = undefined; + } + })); this._queueRefresh(true); } @@ -136,6 +142,9 @@ export class OverviewRulerRenderer extends Disposable { } private _refreshCanvasDimensions(): void { + if (this._store.isDisposed || !this._renderService.hasRenderer()) { + return; + } const cssCanvasHeight = this._renderService.dimensions.css.canvas.height; const deviceCanvasHeight = this._renderService.dimensions.device.canvas.height; this._canvas.style.width = `${this._width}px`; @@ -147,6 +156,9 @@ export class OverviewRulerRenderer extends Disposable { } private _refreshDecorations(): void { + if (this._store.isDisposed || !this._renderService.hasRenderer()) { + return; + } if (this._shouldUpdateDimensions) { this._refreshCanvasDimensions(); } @@ -200,13 +212,18 @@ export class OverviewRulerRenderer extends Disposable { } private _queueRefresh(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void { + if (this._store.isDisposed) { + return; + } this._shouldUpdateDimensions = updateCanvasDimensions || this._shouldUpdateDimensions; this._shouldUpdateAnchor = updateAnchor || this._shouldUpdateAnchor; if (this._animationFrame !== undefined) { return; } this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { - this._refreshDecorations(); + if (!this._store.isDisposed) { + this._refreshDecorations(); + } this._animationFrame = undefined; }); }