Files
xterm.js/test/benchmark/DecorationService.benchmark.ts
Daniel ImmsandCursor f484584b70 Speed up decoration cell lookup with a per-line index
Refs #5176.

forEachDecorationAtCell and getDecorationsAtCell previously scanned every
registered decoration on each call. With thousands of search highlights this
becomes O(total decorations) per cell and blocks rendering during viewport
refreshes.

Maintain a Map of buffer line -> decorations that cover that line. Multi-line
decorations are indexed on every line in their span, so mixed single-line and
multi-line workloads stay fast without falling back to a full scan.

Keep the index aligned with buffer mutations:
- Register and unregister decorations in the line buckets on add/dispose.
- On trim, shift line keys synchronously (before marker handlers run).
- On insert/delete, re-index affected decorations in a microtask after marker
  line updates are applied.

Inject IBufferService so line listeners follow the active buffer. Add unit
tests for trim, dispose-on-trim, insert, and dense single-line plus multi-line
lookup. Add DecorationService.benchmark.ts to measure sparse hits and
viewport-sized grid scans with 20k decorations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 09:30:25 -07:00

110 lines
4.1 KiB
TypeScript

/**
* 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();
});
});