The WASM scanner could fill its fixed 4096-op buffer on large inputs (e.g. 5MB of newlines or OSC traffic) and previously failed or appeared hung. ESC op coalescing also corrupted the lengths field used for dispatch, causing millions of redundant handler invocations on OSC benchmark data. WASM scanner (parser.c / parser.h): - Add scan_offset to ParserScanHeader; scan(offset, length) resumes after a partial fill instead of returning -1 for the whole buffer. - Return early from scan when the op buffer is full; all emit sites use emit_or_stop() so scan_offset records where to continue. - Coalesce consecutive OP_EXECUTE and OP_ESC ops with the same code/ident; lengths holds repeat count, starts holds the input index for ESC. - Keep CSI/DCS hook layout unchanged (lengths = final byte index, starts = param arena offset). TypeScript dispatch (EscapeSequenceParserWasm.ts): - Drive parsing via runWasmScanChunks(): scan a slice, dispatch ops, advance inputOffset to nextOffset until the chunk is consumed. - Fix ESC dispatch: chunkPos uses starts (input index); escCount uses lengths (repeat count). Avoid treating the CSI-style final-byte index as a loop bound. - Throw if nextOffset does not advance to catch stuck scans. EscapeSequenceParser.ts: - Extend _scanCache for chunked scan state; reset() clears inputOffset and cached scan data so later parses are not skipped. Benchmarks: - Import compiled modules from ../../out/common and ../../out/browser so npm run benchmark works without NODE_PATH after path-mapping removal. - Update benchmark skill notes accordingly. Build: - build_parser_wasm.sh selects a clang with wasm32 support (Homebrew LLVM on macOS when Xcode clang lacks the target). Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
4.1 KiB
TypeScript
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 '../../out/common/services/DecorationService';
|
|
import { MockLogService, MockBufferService, MockOptionsService } from '../../out/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();
|
|
});
|
|
});
|