Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

some buffer handling optimizations #4115

Merged
merged 14 commits into from
Dec 7, 2022
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