Merge pull request #5902 from Tyriar/tyriar/cell_opt
Optimize forEachDecorationAtCell by maintaining cache against buffer line index
This commit is contained in:
@@ -73,6 +73,42 @@ export class TimeoutTimer implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled
|
||||
* microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not
|
||||
* run yet.
|
||||
*/
|
||||
export class MicrotaskTimer implements IDisposable {
|
||||
private _isScheduled = false;
|
||||
private _isDisposed = false;
|
||||
|
||||
public dispose(): void {
|
||||
this.cancel();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this._isScheduled = false;
|
||||
}
|
||||
|
||||
public set(runner: () => void): void {
|
||||
if (this._isDisposed) {
|
||||
throw new Error('Calling set on a disposed MicrotaskTimer');
|
||||
}
|
||||
if (this._isScheduled) {
|
||||
return;
|
||||
}
|
||||
this._isScheduled = true;
|
||||
queueMicrotask(() => {
|
||||
if (!this._isScheduled) {
|
||||
return;
|
||||
}
|
||||
this._isScheduled = false;
|
||||
runner();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class IntervalTimer implements IDisposable {
|
||||
private _disposable: IDisposable | undefined;
|
||||
private _isDisposed = false;
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { DecorationService } from './DecorationService';
|
||||
import { DecorationLineCache, DecorationService } from './DecorationService';
|
||||
import { IMarker } from 'common/Types';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Emitter } from 'common/Event';
|
||||
import { MockLogService } from 'common/TestUtils.test';
|
||||
import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
|
||||
function createFakeMarker(line: number): IMarker {
|
||||
return Object.freeze(new class extends Disposable {
|
||||
@@ -19,11 +21,16 @@ function createFakeMarker(line: number): IMarker {
|
||||
}());
|
||||
}
|
||||
|
||||
function createDecorationService(): DecorationService {
|
||||
const bufferService = new MockBufferService(80, 24, new MockOptionsService());
|
||||
return new DecorationService(new MockLogService(), bufferService);
|
||||
}
|
||||
|
||||
const fakeMarker: IMarker = createFakeMarker(1);
|
||||
|
||||
describe('DecorationService', () => {
|
||||
it('should set isDisposed to true after dispose', () => {
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const service = createDecorationService();
|
||||
const decoration = service.registerDecoration({
|
||||
marker: fakeMarker
|
||||
});
|
||||
@@ -35,7 +42,7 @@ describe('DecorationService', () => {
|
||||
|
||||
describe('forEachDecorationAtCell', () => {
|
||||
it('should find decoration at its marker line', () => {
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const service = createDecorationService();
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
width: 10
|
||||
@@ -48,7 +55,7 @@ describe('DecorationService', () => {
|
||||
});
|
||||
|
||||
it('should find decoration with height > 1 on subsequent lines', () => {
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const service = createDecorationService();
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
width: 10,
|
||||
@@ -74,7 +81,7 @@ describe('DecorationService', () => {
|
||||
});
|
||||
|
||||
it('should not find decoration outside its x range', () => {
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const service = createDecorationService();
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
x: 5,
|
||||
@@ -99,11 +106,35 @@ describe('DecorationService', () => {
|
||||
service.forEachDecorationAtCell(8, 5, undefined, d => foundAtX8.push(d));
|
||||
assert.strictEqual(foundAtX8.length, 0);
|
||||
});
|
||||
|
||||
it('should find multi-line decoration when single-line decorations exist on other lines', () => {
|
||||
const bufferService = new MockBufferService(80, 24, new MockOptionsService());
|
||||
const serviceWithBuffer = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
serviceWithBuffer.registerDecoration({
|
||||
marker: buffer.addMarker(i),
|
||||
width: 5
|
||||
});
|
||||
}
|
||||
const multiLine = serviceWithBuffer.registerDecoration({
|
||||
marker: buffer.addMarker(10),
|
||||
width: 10,
|
||||
height: 3
|
||||
});
|
||||
assert.ok(multiLine);
|
||||
|
||||
const found: typeof multiLine[] = [];
|
||||
serviceWithBuffer.forEachDecorationAtCell(0, 11, undefined, d => found.push(d));
|
||||
assert.include(found, multiLine);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDecorationsAtCell', () => {
|
||||
it('should find decoration with height > 1 on subsequent lines', () => {
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const service = createDecorationService();
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
width: 10,
|
||||
@@ -117,4 +148,73 @@ describe('DecorationService', () => {
|
||||
assert.strictEqual([...service.getDecorationsAtCell(0, 8)].length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DecorationLineCache', () => {
|
||||
it('should return undefined for lines with no indexed decorations', () => {
|
||||
const cache = new DecorationLineCache();
|
||||
assert.isUndefined(cache.getDecorationsOnLine(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('line index maintenance', () => {
|
||||
it('should keep lookups correct after buffer trim', () => {
|
||||
const bufferService = new MockBufferService(80, 5, new MockOptionsService({ scrollback: 0 }));
|
||||
const service = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
const marker = buffer.addMarker(buffer.lines.length - 1);
|
||||
const decoration = service.registerDecoration({ marker, width: 10 });
|
||||
assert.ok(decoration);
|
||||
|
||||
buffer.lines.onTrimEmitter.fire(1);
|
||||
|
||||
const found: typeof decoration[] = [];
|
||||
service.forEachDecorationAtCell(0, marker.line, undefined, d => found.push(d));
|
||||
assert.strictEqual(found.length, 1);
|
||||
});
|
||||
|
||||
it('should remove decoration from line index when marker is trimmed off buffer', () => {
|
||||
const bufferService = new MockBufferService(80, 5, new MockOptionsService({ scrollback: 0 }));
|
||||
const service = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
const marker = buffer.addMarker(0);
|
||||
const decoration = service.registerDecoration({ marker, width: 10 });
|
||||
assert.ok(decoration);
|
||||
|
||||
buffer.lines.onTrimEmitter.fire(1);
|
||||
assert.isTrue(marker.isDisposed);
|
||||
assert.isTrue(decoration!.isDisposed);
|
||||
|
||||
const found: typeof decoration[] = [];
|
||||
service.forEachDecorationAtCell(0, 0, undefined, d => found.push(d));
|
||||
assert.strictEqual(found.length, 0);
|
||||
});
|
||||
|
||||
it('should keep multi-line decoration indexed after line insert', async () => {
|
||||
const bufferService = new MockBufferService(80, 10, new MockOptionsService({ scrollback: 100 }));
|
||||
const service = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
const marker = buffer.addMarker(3);
|
||||
const decoration = service.registerDecoration({ marker, width: 10, height: 3 });
|
||||
assert.ok(decoration);
|
||||
|
||||
buffer.lines.splice(5, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve));
|
||||
|
||||
const foundOnSpan: typeof decoration[] = [];
|
||||
for (let line = marker.line; line < marker.line + 3; line++) {
|
||||
service.forEachDecorationAtCell(0, line, undefined, d => foundOnSpan.push(d));
|
||||
}
|
||||
assert.include(foundOnSpan, decoration);
|
||||
|
||||
const foundOutsideSpan: typeof decoration[] = [];
|
||||
service.forEachDecorationAtCell(0, marker.line + 3, undefined, d => foundOutsideSpan.push(d));
|
||||
assert.strictEqual(foundOutsideSpan.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { IDeleteEvent, IInsertEvent } from 'common/CircularList';
|
||||
import { MicrotaskTimer } from 'common/Async';
|
||||
import { css } from 'common/Color';
|
||||
import { Disposable, DisposableStore, toDisposable } from 'common/Lifecycle';
|
||||
import { IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
|
||||
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
|
||||
import { SortedList } from 'common/SortedList';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IColor, ICircularList } from 'common/Types';
|
||||
import { IDecoration, IDecorationOptions, IMarker } from '@xterm/xterm';
|
||||
import { Emitter } from 'common/Event';
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $xmin = 0;
|
||||
let $xmax = 0;
|
||||
let $ymin = 0;
|
||||
let $ymax = 0;
|
||||
|
||||
export class DecorationService extends Disposable implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
@@ -27,6 +27,8 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
*/
|
||||
private readonly _decorations: SortedList<IInternalDecoration>;
|
||||
|
||||
private readonly _lineCache = this._register(new DecorationLineCache());
|
||||
|
||||
private readonly _onDecorationRegistered = this._register(new Emitter<IInternalDecoration>());
|
||||
public readonly onDecorationRegistered = this._onDecorationRegistered.event;
|
||||
private readonly _onDecorationRemoved = this._register(new Emitter<IInternalDecoration>());
|
||||
@@ -34,12 +36,19 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
|
||||
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
|
||||
|
||||
constructor(@ILogService private readonly _logService: ILogService) {
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._decorations = new SortedList(e => e?.marker.line, this._logService);
|
||||
|
||||
this._register(toDisposable(() => this.reset()));
|
||||
this._register(this._bufferService.buffers.onBufferActivate(() => {
|
||||
this._lineCache.attachToBufferLines(this._bufferService.buffer.lines);
|
||||
}));
|
||||
this._lineCache.attachToBufferLines(this._bufferService.buffer.lines);
|
||||
}
|
||||
|
||||
public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
|
||||
@@ -53,12 +62,14 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
listener.dispose();
|
||||
if (decoration) {
|
||||
if (this._decorations.delete(decoration)) {
|
||||
this._lineCache.remove(decoration);
|
||||
this._onDecorationRemoved.fire(decoration);
|
||||
}
|
||||
markerDispose.dispose();
|
||||
}
|
||||
});
|
||||
this._decorations.insert(decoration);
|
||||
this._lineCache.add(decoration);
|
||||
this._onDecorationRegistered.fire(decoration);
|
||||
}
|
||||
return decoration;
|
||||
@@ -69,19 +80,17 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
d.dispose();
|
||||
}
|
||||
this._decorations.clear();
|
||||
this._lineCache.clear();
|
||||
}
|
||||
|
||||
public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration> {
|
||||
const bucket = this._lineCache.getDecorationsOnLine(line);
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
let xmin = 0;
|
||||
let xmax = 0;
|
||||
let ymin = 0;
|
||||
let ymax = 0;
|
||||
for (const d of this._decorations.values()) {
|
||||
ymin = d.marker.line;
|
||||
ymax = ymin + (d.options.height ?? 1);
|
||||
if (line < ymin || line >= ymax) {
|
||||
continue;
|
||||
}
|
||||
for (const d of bucket) {
|
||||
xmin = d.options.x ?? 0;
|
||||
xmax = xmin + (d.options.width ?? 1);
|
||||
if (x >= xmin && x < xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
@@ -91,12 +100,11 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
}
|
||||
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
|
||||
for (const d of this._decorations.values()) {
|
||||
$ymin = d.marker.line;
|
||||
$ymax = $ymin + (d.options.height ?? 1);
|
||||
if (line < $ymin || line >= $ymax) {
|
||||
continue;
|
||||
}
|
||||
const bucket = this._lineCache.getDecorationsOnLine(line);
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
for (const d of bucket) {
|
||||
$xmin = d.options.x ?? 0;
|
||||
$xmax = $xmin + (d.options.width ?? 1);
|
||||
if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
@@ -106,10 +114,235 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-logical-line index of decorations for fast cell lookup.
|
||||
*
|
||||
* Keys are marker.line coordinates (logical buffer lines), not CircularList ring slots.
|
||||
* Multi-line decorations appear in every line bucket they span. The index is kept aligned
|
||||
* with marker.line updates via buffer line trim/insert/delete events.
|
||||
*/
|
||||
export class DecorationLineCache extends Disposable {
|
||||
private readonly _decorationsByLine: Map<number, IInternalDecoration[]> = new Map();
|
||||
private readonly _decorations = new Set<IInternalDecoration>();
|
||||
private readonly _bufferLineListeners = this._register(new MutableDisposable<DisposableStore>());
|
||||
private readonly _lineIndexSyncTimer = this._register(new MicrotaskTimer());
|
||||
private _lineIndexSyncCallbacks: (() => void)[] = [];
|
||||
|
||||
public clear(): void {
|
||||
this._lineIndexSyncCallbacks.length = 0;
|
||||
this._lineIndexSyncTimer.cancel();
|
||||
this._decorationsByLine.clear();
|
||||
this._decorations.clear();
|
||||
}
|
||||
|
||||
public add(decoration: IInternalDecoration): void {
|
||||
this._decorations.add(decoration);
|
||||
this._addToLineBuckets(decoration);
|
||||
}
|
||||
|
||||
public remove(decoration: IInternalDecoration): void {
|
||||
this._decorations.delete(decoration);
|
||||
this._removeFromLineBuckets(decoration);
|
||||
}
|
||||
|
||||
public getDecorationsOnLine(line: number): ReadonlyArray<IInternalDecoration> | undefined {
|
||||
return this._decorationsByLine.get(line);
|
||||
}
|
||||
|
||||
public attachToBufferLines(lines: ICircularList<unknown>): void {
|
||||
const store = new DisposableStore();
|
||||
this._bufferLineListeners.value = store;
|
||||
store.add(lines.onTrim(amount => this._handleBufferLinesTrim(amount)));
|
||||
store.add(lines.onInsert(event => this._handleBufferLinesInsert(event)));
|
||||
store.add(lines.onDelete(event => this._handleBufferLinesDelete(event)));
|
||||
}
|
||||
|
||||
private _getDecorationHeight(decoration: IInternalDecoration): number {
|
||||
return decoration.options.height ?? 1;
|
||||
}
|
||||
|
||||
private _addToLineBuckets(decoration: IInternalDecoration): void {
|
||||
const start = decoration.marker.line;
|
||||
if (start < 0) {
|
||||
return;
|
||||
}
|
||||
decoration._indexedStartLine = start;
|
||||
const height = this._getDecorationHeight(decoration);
|
||||
for (let line = start; line < start + height; line++) {
|
||||
let bucket = this._decorationsByLine.get(line);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
bucket.push(decoration);
|
||||
}
|
||||
}
|
||||
|
||||
private _removeFromLineBuckets(decoration: IInternalDecoration): void {
|
||||
const start = decoration._indexedStartLine;
|
||||
const height = this._getDecorationHeight(decoration);
|
||||
for (let line = start; line < start + height; line++) {
|
||||
const bucket = this._decorationsByLine.get(line);
|
||||
if (!bucket) {
|
||||
continue;
|
||||
}
|
||||
const index = bucket.indexOf(decoration);
|
||||
if (index !== -1) {
|
||||
bucket.splice(index, 1);
|
||||
}
|
||||
if (bucket.length === 0) {
|
||||
this._decorationsByLine.delete(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _reindexDecoration(decoration: IInternalDecoration): void {
|
||||
this._removeFromLineBuckets(decoration);
|
||||
if (!decoration.marker.isDisposed && decoration.marker.line >= 0) {
|
||||
this._addToLineBuckets(decoration);
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-index after marker line updates (buffer listeners may run before markers). */
|
||||
private _scheduleLineIndexSync(callback: () => void): void {
|
||||
this._lineIndexSyncCallbacks.push(callback);
|
||||
this._lineIndexSyncTimer.set(() => {
|
||||
const callbacks = this._lineIndexSyncCallbacks;
|
||||
this._lineIndexSyncCallbacks = [];
|
||||
for (const cb of callbacks) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _handleBufferLinesTrim(amount: number): void {
|
||||
if (amount <= 0) {
|
||||
return;
|
||||
}
|
||||
const newMap = new Map<number, IInternalDecoration[]>();
|
||||
for (const [line, bucket] of this._decorationsByLine) {
|
||||
const newLine = line - amount;
|
||||
if (newLine < 0) {
|
||||
continue;
|
||||
}
|
||||
const existing = newMap.get(newLine);
|
||||
if (existing) {
|
||||
existing.push(...bucket);
|
||||
} else {
|
||||
newMap.set(newLine, bucket.slice());
|
||||
}
|
||||
}
|
||||
this._decorationsByLine.clear();
|
||||
for (const [line, bucket] of newMap) {
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
for (const d of this._decorations) {
|
||||
if (!d.marker.isDisposed) {
|
||||
d._indexedStartLine -= amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBufferLinesInsert(event: IInsertEvent): void {
|
||||
this._scheduleLineIndexSync(() => this._applyBufferLinesInsert(event));
|
||||
}
|
||||
|
||||
private _handleBufferLinesDelete(event: IDeleteEvent): void {
|
||||
this._scheduleLineIndexSync(() => this._applyBufferLinesDelete(event));
|
||||
}
|
||||
|
||||
private _mergeLineBucket(newMap: Map<number, IInternalDecoration[]>, line: number, bucket: IInternalDecoration[]): void {
|
||||
const existing = newMap.get(line);
|
||||
if (existing) {
|
||||
existing.push(...bucket);
|
||||
} else {
|
||||
newMap.set(line, bucket.slice());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift indexed line keys and sync start lines. O(unique indexed lines), not O(decoration count).
|
||||
* Decorations that span the insert point are re-indexed individually (rare vs single-line hits).
|
||||
*/
|
||||
private _applyBufferLinesInsert(event: IInsertEvent): void {
|
||||
const { index, amount } = event;
|
||||
const spanCrossers: IInternalDecoration[] = [];
|
||||
for (const d of this._decorations) {
|
||||
if (d.marker.isDisposed) {
|
||||
continue;
|
||||
}
|
||||
const start = d._indexedStartLine;
|
||||
if (start < index && start + this._getDecorationHeight(d) > index) {
|
||||
spanCrossers.push(d);
|
||||
this._removeFromLineBuckets(d);
|
||||
}
|
||||
}
|
||||
const newMap = new Map<number, IInternalDecoration[]>();
|
||||
for (const [line, bucket] of this._decorationsByLine) {
|
||||
const newLine = line >= index ? line + amount : line;
|
||||
this._mergeLineBucket(newMap, newLine, bucket);
|
||||
}
|
||||
this._decorationsByLine.clear();
|
||||
for (const [line, bucket] of newMap) {
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
for (const d of this._decorations) {
|
||||
if (d.marker.isDisposed) {
|
||||
continue;
|
||||
}
|
||||
if (d._indexedStartLine >= index) {
|
||||
d._indexedStartLine = d.marker.line;
|
||||
}
|
||||
}
|
||||
for (const d of spanCrossers) {
|
||||
this._addToLineBuckets(d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop deleted line keys, shift keys below, sync start lines. Full re-index only when a
|
||||
* multi-line decoration spans across the deleted range but survives.
|
||||
*/
|
||||
private _applyBufferLinesDelete(event: IDeleteEvent): void {
|
||||
const deleteEnd = event.index + event.amount;
|
||||
const newMap = new Map<number, IInternalDecoration[]>();
|
||||
for (const [line, bucket] of this._decorationsByLine) {
|
||||
if (line >= event.index && line < deleteEnd) {
|
||||
continue;
|
||||
}
|
||||
const newLine = line >= deleteEnd ? line - event.amount : line;
|
||||
this._mergeLineBucket(newMap, newLine, bucket);
|
||||
}
|
||||
this._decorationsByLine.clear();
|
||||
for (const [line, bucket] of newMap) {
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
const toReindex: IInternalDecoration[] = [];
|
||||
for (const d of this._decorations) {
|
||||
if (d.marker.isDisposed) {
|
||||
continue;
|
||||
}
|
||||
const start = d._indexedStartLine;
|
||||
const height = this._getDecorationHeight(d);
|
||||
if (start >= deleteEnd) {
|
||||
d._indexedStartLine = d.marker.line;
|
||||
} else if (start < event.index && start + height > deleteEnd) {
|
||||
toReindex.push(d);
|
||||
}
|
||||
}
|
||||
for (const d of toReindex) {
|
||||
this._reindexDecoration(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Decoration extends DisposableStore implements IInternalDecoration {
|
||||
public readonly marker: IMarker;
|
||||
public element: HTMLElement | undefined;
|
||||
|
||||
/** Start line used for line-index removal when marker.line is cleared on dispose. */
|
||||
public _indexedStartLine: number;
|
||||
|
||||
public readonly onRenderEmitter = this.add(new Emitter<HTMLElement>());
|
||||
public readonly onRender = this.onRenderEmitter.event;
|
||||
private readonly _onDispose = this.add(new Emitter<void>());
|
||||
@@ -144,6 +377,7 @@ class Decoration extends DisposableStore implements IInternalDecoration {
|
||||
) {
|
||||
super();
|
||||
this.marker = options.marker;
|
||||
this._indexedStartLine = options.marker.line;
|
||||
if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) {
|
||||
this.options.overviewRulerOptions.position = 'full';
|
||||
}
|
||||
|
||||
@@ -396,4 +396,6 @@ export interface IInternalDecoration extends IDecoration {
|
||||
readonly backgroundColorRGB: IColor | undefined;
|
||||
readonly foregroundColorRGB: IColor | undefined;
|
||||
readonly onRenderEmitter: Emitter<HTMLElement>;
|
||||
/** @internal Start line for line-index removal; kept in sync on buffer line shifts. */
|
||||
_indexedStartLine: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
const enum Constants {
|
||||
COLS = 80,
|
||||
ROWS = 30,
|
||||
SINGLE_LINE_DECORATION_COUNT = 20_000,
|
||||
MULTI_LINE_DECORATION_COUNT = 19_999,
|
||||
MULTI_LINE_HEIGHT = 2,
|
||||
VIEWPORT_SCAN_ITERATIONS = 20
|
||||
}
|
||||
|
||||
function registerSingleLineDecorations(service: DecorationService, bufferService: MockBufferService, count: number): void {
|
||||
const buffer = bufferService.buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const line = i % 5000;
|
||||
const marker = buffer.addMarker(line);
|
||||
service.registerDecoration({ marker, width: Constants.COLS });
|
||||
}
|
||||
}
|
||||
|
||||
function registerMixedDecorations(service: DecorationService, bufferService: MockBufferService): void {
|
||||
const buffer = bufferService.buffer;
|
||||
for (let i = 0; i < Constants.MULTI_LINE_DECORATION_COUNT; i++) {
|
||||
const line = i % 5000;
|
||||
const marker = buffer.addMarker(line);
|
||||
service.registerDecoration({ marker, width: Constants.COLS });
|
||||
}
|
||||
const multiLineMarker = buffer.addMarker(10);
|
||||
service.registerDecoration({
|
||||
marker: multiLineMarker,
|
||||
width: Constants.COLS,
|
||||
height: Constants.MULTI_LINE_HEIGHT
|
||||
});
|
||||
}
|
||||
|
||||
function scanVisibleGrid(service: DecorationService, rows: number, cols: number): number {
|
||||
let hitCount = 0;
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
service.forEachDecorationAtCell(col, row, undefined, () => {
|
||||
hitCount++;
|
||||
});
|
||||
}
|
||||
}
|
||||
return hitCount;
|
||||
}
|
||||
|
||||
perfContext('DecorationService.forEachDecorationAtCell', () => {
|
||||
perfContext('single-line dense / sparse line hit', () => {
|
||||
let service: DecorationService;
|
||||
let bufferService: MockBufferService;
|
||||
before(() => {
|
||||
bufferService = new MockBufferService(Constants.COLS, Constants.ROWS, new MockOptionsService());
|
||||
service = new DecorationService(new MockLogService(), bufferService);
|
||||
registerSingleLineDecorations(service, bufferService, Constants.SINGLE_LINE_DECORATION_COUNT);
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
let hitCount = 0;
|
||||
for (let i = 0; i < Constants.VIEWPORT_SCAN_ITERATIONS; i++) {
|
||||
service.forEachDecorationAtCell(0, 0, undefined, () => {
|
||||
hitCount++;
|
||||
});
|
||||
}
|
||||
return { payloadSize: Constants.VIEWPORT_SCAN_ITERATIONS, hitCount };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
|
||||
perfContext('mixed single-line + multi-line / sparse line hit', () => {
|
||||
let service: DecorationService;
|
||||
let bufferService: MockBufferService;
|
||||
before(() => {
|
||||
bufferService = new MockBufferService(Constants.COLS, Constants.ROWS, new MockOptionsService());
|
||||
service = new DecorationService(new MockLogService(), bufferService);
|
||||
registerMixedDecorations(service, bufferService);
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
let hitCount = 0;
|
||||
for (let i = 0; i < Constants.VIEWPORT_SCAN_ITERATIONS; i++) {
|
||||
service.forEachDecorationAtCell(0, 10, undefined, () => {
|
||||
hitCount++;
|
||||
});
|
||||
}
|
||||
return { payloadSize: Constants.VIEWPORT_SCAN_ITERATIONS, hitCount };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
|
||||
perfContext('viewport grid scan', () => {
|
||||
let service: DecorationService;
|
||||
let bufferService: MockBufferService;
|
||||
before(() => {
|
||||
bufferService = new MockBufferService(Constants.COLS, Constants.ROWS, new MockOptionsService());
|
||||
service = new DecorationService(new MockLogService(), bufferService);
|
||||
registerSingleLineDecorations(service, bufferService, Constants.SINGLE_LINE_DECORATION_COUNT);
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
let totalHits = 0;
|
||||
for (let i = 0; i < Constants.VIEWPORT_SCAN_ITERATIONS; i++) {
|
||||
totalHits += scanVisibleGrid(service, Constants.ROWS, Constants.COLS);
|
||||
}
|
||||
return { payloadSize: Constants.VIEWPORT_SCAN_ITERATIONS * Constants.ROWS * Constants.COLS, totalHits };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user