mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #5038 from Tyriar/tyriar/4911
Speed up insertion and deletion of values in SortedList by batching and deferring to idle task
This commit is contained in:
+29
-1
@@ -44,7 +44,7 @@ if ('WebAssembly' in window) {
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
import { Terminal as TerminalType, ITerminalOptions } from '@xterm/xterm';
|
||||
import { Terminal as TerminalType, ITerminalOptions, type IDisposable } from '@xterm/xterm';
|
||||
|
||||
export interface IWindowWithTerminal extends Window {
|
||||
term: TerminalType;
|
||||
@@ -255,6 +255,7 @@ if (document.location.pathname === '/test') {
|
||||
document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters);
|
||||
document.getElementById('add-decoration').addEventListener('click', addDecoration);
|
||||
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
|
||||
document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest);
|
||||
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
|
||||
document.getElementById('bce').addEventListener('click', coloredErase);
|
||||
addVtButtons();
|
||||
@@ -1170,6 +1171,33 @@ function addOverviewRuler(): void {
|
||||
term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } });
|
||||
}
|
||||
|
||||
let decorationStressTestDecorations: IDisposable[] | undefined;
|
||||
function decorationStressTest(): void {
|
||||
if (decorationStressTestDecorations) {
|
||||
for (const d of decorationStressTestDecorations) {
|
||||
d.dispose();
|
||||
}
|
||||
decorationStressTestDecorations = undefined;
|
||||
} else {
|
||||
const t = term as Terminal;
|
||||
const buffer = t.buffer.active;
|
||||
const cursorY = buffer.baseY + buffer.cursorY;
|
||||
decorationStressTestDecorations = [];
|
||||
for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) {
|
||||
for (let y = 0; y < t.buffer.active.length; y++) {
|
||||
const cursorOffsetY = y - cursorY;
|
||||
decorationStressTestDecorations.push(t.registerDecoration({
|
||||
marker: t.registerMarker(cursorOffsetY),
|
||||
x,
|
||||
width: 4,
|
||||
backgroundColor: '#FF0000',
|
||||
overviewRulerOptions: { color: '#FF0000' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
|
||||
function getBox(width: number, height: number): any {
|
||||
return {
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
<dt>Decorations</dt>
|
||||
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
|
||||
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
|
||||
<dd><button id="decoration-stress-test" title="Toggle between adding and removing a decoration to each line">Stress Test</button></dd>
|
||||
|
||||
<dt>Weblinks Addon</dt>
|
||||
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
|
||||
|
||||
@@ -20,6 +20,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOption
|
||||
*/
|
||||
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
|
||||
|
||||
let $value = 0;
|
||||
|
||||
export class Terminal extends Disposable implements ITerminalApi {
|
||||
private _core: ITerminal;
|
||||
private _addonManager: AddonManager;
|
||||
@@ -249,16 +251,16 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
}
|
||||
|
||||
private _verifyIntegers(...values: number[]): void {
|
||||
for (const value of values) {
|
||||
if (value === Infinity || isNaN(value) || value % 1 !== 0) {
|
||||
for ($value of values) {
|
||||
if ($value === Infinity || isNaN($value) || $value % 1 !== 0) {
|
||||
throw new Error('This API only accepts integers');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _verifyPositiveIntegers(...values: number[]): void {
|
||||
for (const value of values) {
|
||||
if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) {
|
||||
for ($value of values) {
|
||||
if ($value && ($value === Infinity || isNaN($value) || $value % 1 !== 0 || $value < 0)) {
|
||||
throw new Error('This API only accepts positive integers');
|
||||
}
|
||||
}
|
||||
|
||||
+86
-10
@@ -3,16 +3,27 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IdleTaskQueue } from 'common/TaskQueue';
|
||||
|
||||
// Work variables to avoid garbage collection.
|
||||
let i = 0;
|
||||
|
||||
/**
|
||||
* A generic list that is maintained in sorted order and allows values with duplicate keys. This
|
||||
* list is based on binary search and as such locating a key will take O(log n) amortized, this
|
||||
* includes the by key iterator.
|
||||
* A generic list that is maintained in sorted order and allows values with duplicate keys. Deferred
|
||||
* batch insertion and deletion is used to significantly reduce the time it takes to insert and
|
||||
* delete a large amount of items in succession. This list is based on binary search and as such
|
||||
* locating a key will take O(log n) amortized, this includes the by key iterator.
|
||||
*/
|
||||
export class SortedList<T> {
|
||||
private readonly _array: T[] = [];
|
||||
private _array: T[] = [];
|
||||
|
||||
private readonly _insertedValues: T[] = [];
|
||||
private readonly _flushInsertedTask = new IdleTaskQueue();
|
||||
private _isFlushingInserted = false;
|
||||
|
||||
private readonly _deletedIndices: number[] = [];
|
||||
private readonly _flushDeletedTask = new IdleTaskQueue();
|
||||
private _isFlushingDeleted = false;
|
||||
|
||||
constructor(
|
||||
private readonly _getKey: (value: T) => number
|
||||
@@ -21,18 +32,50 @@ export class SortedList<T> {
|
||||
|
||||
public clear(): void {
|
||||
this._array.length = 0;
|
||||
this._insertedValues.length = 0;
|
||||
this._flushInsertedTask.clear();
|
||||
this._isFlushingInserted = false;
|
||||
this._deletedIndices.length = 0;
|
||||
this._flushDeletedTask.clear();
|
||||
this._isFlushingDeleted = false;
|
||||
}
|
||||
|
||||
public insert(value: T): void {
|
||||
if (this._array.length === 0) {
|
||||
this._array.push(value);
|
||||
return;
|
||||
this._flushCleanupDeleted();
|
||||
if (this._insertedValues.length === 0) {
|
||||
this._flushInsertedTask.enqueue(() => this._flushInserted());
|
||||
}
|
||||
this._insertedValues.push(value);
|
||||
}
|
||||
|
||||
private _flushInserted(): void {
|
||||
const sortedAddedValues = this._insertedValues.sort((a, b) => this._getKey(a) - this._getKey(b));
|
||||
let sortedAddedValuesIndex = 0;
|
||||
let arrayIndex = 0;
|
||||
|
||||
const newArray = new Array(this._array.length + this._insertedValues.length);
|
||||
|
||||
for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) {
|
||||
if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) <= this._getKey(this._array[arrayIndex])) {
|
||||
newArray[newArrayIndex] = sortedAddedValues[sortedAddedValuesIndex];
|
||||
sortedAddedValuesIndex++;
|
||||
} else {
|
||||
newArray[newArrayIndex] = this._array[arrayIndex++];
|
||||
}
|
||||
}
|
||||
|
||||
this._array = newArray;
|
||||
this._insertedValues.length = 0;
|
||||
}
|
||||
|
||||
private _flushCleanupInserted(): void {
|
||||
if (!this._isFlushingInserted && this._insertedValues.length > 0) {
|
||||
this._flushInsertedTask.flush();
|
||||
}
|
||||
i = this._search(this._getKey(value));
|
||||
this._array.splice(i, 0, value);
|
||||
}
|
||||
|
||||
public delete(value: T): boolean {
|
||||
this._flushCleanupInserted();
|
||||
if (this._array.length === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -49,14 +92,43 @@ export class SortedList<T> {
|
||||
}
|
||||
do {
|
||||
if (this._array[i] === value) {
|
||||
this._array.splice(i, 1);
|
||||
if (this._deletedIndices.length === 0) {
|
||||
this._flushDeletedTask.enqueue(() => this._flushDeleted());
|
||||
}
|
||||
this._deletedIndices.push(i);
|
||||
return true;
|
||||
}
|
||||
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
return false;
|
||||
}
|
||||
|
||||
private _flushDeleted(): void {
|
||||
this._isFlushingDeleted = true;
|
||||
const sortedDeletedIndices = this._deletedIndices.sort((a, b) => a - b);
|
||||
let sortedDeletedIndicesIndex = 0;
|
||||
const newArray = new Array(this._array.length - sortedDeletedIndices.length);
|
||||
let newArrayIndex = 0;
|
||||
for (let i = 0; i < this._array.length; i++) {
|
||||
if (sortedDeletedIndices[sortedDeletedIndicesIndex] === i) {
|
||||
sortedDeletedIndicesIndex++;
|
||||
} else {
|
||||
newArray[newArrayIndex++] = this._array[i];
|
||||
}
|
||||
}
|
||||
this._array = newArray;
|
||||
this._deletedIndices.length = 0;
|
||||
this._isFlushingDeleted = false;
|
||||
}
|
||||
|
||||
private _flushCleanupDeleted(): void {
|
||||
if (!this._isFlushingDeleted && this._deletedIndices.length > 0) {
|
||||
this._flushDeletedTask.flush();
|
||||
}
|
||||
}
|
||||
|
||||
public *getKeyIterator(key: number): IterableIterator<T> {
|
||||
this._flushCleanupInserted();
|
||||
this._flushCleanupDeleted();
|
||||
if (this._array.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -73,6 +145,8 @@ export class SortedList<T> {
|
||||
}
|
||||
|
||||
public forEachByKey(key: number, callback: (value: T) => void): void {
|
||||
this._flushCleanupInserted();
|
||||
this._flushCleanupDeleted();
|
||||
if (this._array.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -89,6 +163,8 @@ export class SortedList<T> {
|
||||
}
|
||||
|
||||
public values(): IterableIterator<T> {
|
||||
this._flushCleanupInserted();
|
||||
this._flushCleanupDeleted();
|
||||
// Duplicate the array to avoid issues when _array changes while iterating
|
||||
return [...this._array].values();
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
const decoration = new Decoration(options);
|
||||
if (decoration) {
|
||||
const markerDispose = decoration.marker.onDispose(() => decoration.dispose());
|
||||
decoration.onDispose(() => {
|
||||
const listener = decoration.onDispose(() => {
|
||||
listener.dispose();
|
||||
if (decoration) {
|
||||
if (this._decorations.delete(decoration)) {
|
||||
this._onDecorationRemoved.fire(decoration);
|
||||
|
||||
Reference in New Issue
Block a user