Fix dispose leaks: register CursorBlinkStateManager + _pausedResizeTask

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
This commit is contained in:
Sridhar Ratnakumar
2026-04-17 13:32:01 -04:00
parent 34e0179355
commit 7a6e724bfe
3 changed files with 6 additions and 2 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = this._register(new MutableDisposable());
private _textBlinkStateManager: TextBlinkStateManager;
private _charAtlasDisposable = this._register(new MutableDisposable());
private _charAtlas: ITextureAtlas | undefined;
+1 -1
View File
@@ -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);
+4
View File
@@ -168,4 +168,8 @@ export class DebouncedIdleTask {
public flush(): void {
this._queue.flush();
}
public dispose(): void {
this._queue.clear();
}
}