Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/common/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ export interface IBufferLine {
insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void;
deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void;
replaceCells(start: number, end: number, fill: ICellData, eraseAttr?: IAttributeData, respectProtect?: boolean): void;
resize(cols: number, fill: ICellData): void;
resize(cols: number, fill: ICellData): boolean;
cleanupMemory(): number;
fill(fillCellData: ICellData, respectProtect?: boolean): void;
copyFrom(line: IBufferLine): void;
clone(): IBufferLine;
Expand Down
31 changes: 29 additions & 2 deletions src/common/buffer/Buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Marker } from 'common/buffer/Marker';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { DEFAULT_CHARSET } from 'common/data/Charsets';
import { ExtendedAttrs } from 'common/buffer/AttributeData';
import { DebouncedIdleTask } from 'common/TaskQueue';

export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1

Expand Down Expand Up @@ -151,6 +152,9 @@ export class Buffer implements IBuffer {
// store reference to null cell with default attrs
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);

// defer memory cleanup of bufferlines
let needsCleanup = 0;

// Increase max length if needed before adjustments to allow space to fill
// as required.
const newMaxLength = this._getCorrectBufferLength(newRows);
Expand All @@ -164,7 +168,8 @@ export class Buffer implements IBuffer {
// Deal with columns increasing (reducing needs to happen after reflow)
if (this._cols < newCols) {
for (let i = 0; i < this.lines.length; i++) {
this.lines.get(i)!.resize(newCols, nullCell);
// +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}

Expand Down Expand Up @@ -243,13 +248,35 @@ export class Buffer implements IBuffer {
// Trim the end of the line off if cols shrunk
if (this._cols > newCols) {
for (let i = 0; i < this.lines.length; i++) {
this.lines.get(i)!.resize(newCols, nullCell);
// +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}
}

this._cols = newCols;
this._rows = newRows;

if (needsCleanup) {
this._memoryCleanupTask.set(() => this._cleanupMemory());
} else {
// FIXME: DebouncedIdleTask has no clear method?
this._memoryCleanupTask.set(() => {});
}
}

private _memoryCleanupTask: DebouncedIdleTask = new DebouncedIdleTask();

private _cleanupMemory(): void {
let counted = 0;
for (let i = 0; i < this.lines.length; i++) {
counted += this.lines.get(i)!.cleanupMemory();
// throttle to 5k lines
if (counted > 5000) {
this._memoryCleanupTask.set(() => this._cleanupMemory());
break;
}
}
}

private get _isReflowEnabled(): boolean {
Expand Down
78 changes: 54 additions & 24 deletions src/common/buffer/BufferLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
// Work variables to avoid garbage collection
let $startIndex = 0;

/** Factor when to cleanup underlying array buffer after shrinking. */
const CLEANUP_THRESHOLD = 2;

/**
* Typed array based bufferline implementation.
*
Expand Down Expand Up @@ -333,42 +336,69 @@ export class BufferLine implements IBufferLine {
}
}

public resize(cols: number, fillCellData: ICellData): void {
/**
* Resize BufferLine to `cols` filling excess cells with `fillCellData`.
* The underlying array buffer will not change if there is still enough space
* to hold the new buffer line data.
* Returns a boolean indicating, whether a `cleanBuffer` call would free
* excess memory (after shrinking > CLEANUP_THRESHOLD).
*/
public resize(cols: number, fillCellData: ICellData): boolean {
if (cols === this.length) {
return;
return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength;
}
const uint32Cells = cols * CELL_SIZE;
if (cols > this.length) {
const data = new Uint32Array(cols * CELL_SIZE);
if (this.length) {
if (cols * CELL_SIZE < this._data.length) {
data.set(this._data.subarray(0, cols * CELL_SIZE));
} else {
data.set(this._data);
}
if (this._data.buffer.byteLength >= uint32Cells * 4) {
// optimization: avoid alloc and data copy if buffer has enough room
this._data = new Uint32Array(this._data.buffer, 0, uint32Cells);
} else {
// slow path: new alloc and full data copy
const data = new Uint32Array(uint32Cells);
data.set(this._data);
this._data = data;
}
this._data = data;
for (let i = this.length; i < cols; ++i) {
this.setCell(i, fillCellData);
}
} else {
if (cols) {
const data = new Uint32Array(cols * CELL_SIZE);
data.set(this._data.subarray(0, cols * CELL_SIZE));
this._data = data;
// Remove any cut off combined data, FIXME: repeat this for extended attrs
const keys = Object.keys(this._combined);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i], 10);
if (key >= cols) {
delete this._combined[key];
}
// optimization: just shrink the view on existing buffer
this._data = this._data.subarray(0, uint32Cells);
// Remove any cut off combined data
const keys = Object.keys(this._combined);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i], 10);
if (key >= cols) {
delete this._combined[key];
}
}
// remove any cut off extended attributes
const extKeys = Object.keys(this._extendedAttrs);
for (let i = 0; i < extKeys.length; i++) {
const key = parseInt(extKeys[i], 10);
if (key >= cols) {
delete this._extendedAttrs[key];
}
} else {
this._data = new Uint32Array(0);
this._combined = {};
}
}
this.length = cols;
return uint32Cells * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength;
}

/**
* Cleanup underlying array buffer.
* A cleanup will be triggered if the array buffer exceeds the actual used
* memory by a factor of CLEANUP_THRESHOLD.
* Returns 0 or 1 indicating whether a cleanup happened.
*/
public cleanupMemory(): number {
if (this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength) {
const data = new Uint32Array(this._data.length);
data.set(this._data);
this._data = data;
return 1;
}
return 0;
}

/** fill a line with fillCharData */
Expand Down