Skip to content

Commit a3ce881

Browse files
authored
Merge pull request #1372 from Tyriar/1341_memory_leak
Remove terminal references on dispose
2 parents cf8fa10 + 58c50f0 commit a3ce881

File tree

5 files changed

+64
-19
lines changed

5 files changed

+64
-19
lines changed

src/EventEmitter.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { XtermListener } from './Types';
77
import { IEventEmitter, IDisposable } from 'xterm';
88

9-
export class EventEmitter implements IEventEmitter {
9+
export class EventEmitter implements IEventEmitter, IDisposable {
1010
private _events: {[type: string]: XtermListener[]};
1111

1212
constructor() {
@@ -75,7 +75,7 @@ export class EventEmitter implements IEventEmitter {
7575
return this._events[type] || [];
7676
}
7777

78-
protected destroy(): void {
78+
public dispose(): void {
7979
this._events = {};
8080
}
8181
}

src/Terminal.ts

+28-16
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { Linkifier } from './Linkifier';
3838
import { SelectionManager } from './SelectionManager';
3939
import { CharMeasure } from './utils/CharMeasure';
4040
import * as Browser from './shared/utils/Browser';
41+
import * as Dom from './utils/Dom';
4142
import * as Strings from './Strings';
4243
import { MouseHelper } from './utils/MouseHelper';
4344
import { clone } from './utils/Clone';
@@ -46,7 +47,8 @@ import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
4647
import { MouseZoneManager } from './input/MouseZoneManager';
4748
import { AccessibilityManager } from './AccessibilityManager';
4849
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
49-
import { ITheme, ILocalizableStrings, IMarker } from 'xterm';
50+
import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm';
51+
import { removeTerminalFromCache } from './renderer/atlas/CharAtlas';
5052

5153
// reg + shift key mappings for digits and special chars
5254
const KEYCODE_KEY_MAPPINGS = {
@@ -122,11 +124,13 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
122124
rightClickSelectsWord: Browser.isMac
123125
};
124126

125-
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
127+
export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
126128
public textarea: HTMLTextAreaElement;
127129
public element: HTMLElement;
128130
public screenElement: HTMLElement;
129131

132+
private _disposables: IDisposable[];
133+
130134
/**
131135
* The HTMLElement that the terminal is created in, set by Terminal.open.
132136
*/
@@ -248,7 +252,28 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
248252
this._setup();
249253
}
250254

255+
public dispose(): void {
256+
super.dispose();
257+
this._disposables.forEach(d => d.dispose());
258+
this._disposables.length = 0;
259+
removeTerminalFromCache(this);
260+
this.handler = () => {};
261+
this.write = () => {};
262+
if (this.element && this.element.parentNode) {
263+
this.element.parentNode.removeChild(this.element);
264+
}
265+
}
266+
267+
/**
268+
* @deprecated Use dispose instead.
269+
*/
270+
public destroy(): void {
271+
this.dispose();
272+
}
273+
251274
private _setup(): void {
275+
this._disposables = [];
276+
252277
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
253278
if (this.options[key] == null) {
254279
this.options[key] = DEFAULT_OPTIONS[key];
@@ -690,7 +715,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
690715
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
691716
// dprchange should handle this case, we need this as well for browsers that don't support the
692717
// matchMedia query.
693-
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
718+
this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
694719
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows));
695720
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
696721

@@ -1083,19 +1108,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
10831108
});
10841109
}
10851110

1086-
/**
1087-
* Destroys the terminal.
1088-
*/
1089-
public destroy(): void {
1090-
super.destroy();
1091-
this.handler = () => {};
1092-
this.write = () => {};
1093-
if (this.element && this.element.parentNode) {
1094-
this.element.parentNode.removeChild(this.element);
1095-
}
1096-
// this.emit('close');
1097-
}
1098-
10991111
/**
11001112
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
11011113
* opportunity.

src/renderer/atlas/CharAtlas.ts

+22
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ let charAtlasCache: ICharAtlasCacheEntry[] = [];
2626
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
2727
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
2828

29+
// TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed)
30+
2931
// Check to see if the terminal already owns this config
3032
for (let i = 0; i < charAtlasCache.length; i++) {
3133
const entry = charAtlasCache[i];
@@ -70,3 +72,23 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
7072
charAtlasCache.push(newEntry);
7173
return newEntry.bitmap;
7274
}
75+
76+
/**
77+
* Removes a terminal reference from the cache, allowing its memory to be freed.
78+
* @param terminal The terminal to remove.
79+
*/
80+
export function removeTerminalFromCache(terminal: ITerminal): void {
81+
for (let i = 0; i < charAtlasCache.length; i++) {
82+
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
83+
if (index !== -1) {
84+
if (charAtlasCache[i].ownedBy.length === 1) {
85+
// Remove the cache entry if it's the only terminal
86+
charAtlasCache.splice(i, 1);
87+
} else {
88+
// Remove the reference from the cache entry
89+
charAtlasCache[i].ownedBy.splice(index, 1);
90+
}
91+
break;
92+
}
93+
}
94+
}

src/utils/TestUtils.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export class MockTerminal implements ITerminal {
6363
selectAll(): void {
6464
throw new Error('Method not implemented.');
6565
}
66+
dispose(): void {
67+
throw new Error('Method not implemented.');
68+
}
6669
destroy(): void {
6770
throw new Error('Method not implemented.');
6871
}

typings/xterm.d.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ declare module 'xterm' {
251251
/**
252252
* The class that represents an xterm.js terminal.
253253
*/
254-
export class Terminal implements IEventEmitter {
254+
export class Terminal implements IEventEmitter, IDisposable {
255255
/**
256256
* The element containing the terminal.
257257
*/
@@ -451,8 +451,16 @@ declare module 'xterm' {
451451
*/
452452
selectLines(start: number, end: number): void;
453453

454+
/*
455+
* Disposes of the terminal, detaching it from the DOM and removing any
456+
* active listeners.
457+
*/
458+
dispose(): void;
459+
454460
/**
455461
* Destroys the terminal and detaches it from the DOM.
462+
*
463+
* @deprecated Use dispose() instead.
456464
*/
457465
destroy(): void;
458466

0 commit comments

Comments
 (0)