Files
xterm.js/test/benchmark/EscapeSequenceParser.benchmark.ts
T
Daniel ImmsandCursor 31b8409120 fix(parser): chunked WASM scan, op coalescing, and benchmark runtime paths
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>
2026-05-31 15:06:00 -07:00

422 lines
15 KiB
TypeScript

/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-benchmark';
import { EscapeSequenceParser } from '../../out/common/parser/EscapeSequenceParser';
import { C0, C1 } from '../../out/common/data/EscapeSequences';
import { IDcsHandler, IOscHandler, IApcHandler, IParams } from '../../out/common/parser/Types';
import { OscHandler } from '../../out/common/parser/OscParser';
import { DcsHandler } from '../../out/common/parser/DcsParser';
import { ApcHandler } from '../../out/common/parser/ApcParser';
const SIZE = 5000000;
function toUtf32(s: string): Uint32Array {
const result = new Uint32Array(s.length);
for (let i = 0; i < s.length; ++i) {
result[i] = s.charCodeAt(i);
}
return result;
}
class FastDcsHandler implements IDcsHandler {
public hook(params: IParams): void {}
public put(data: Uint32Array, start: number, end: number): void {}
public unhook(success: boolean): boolean { return true; }
}
class FastOscHandler implements IOscHandler {
public start(): void {}
public put(data: Uint32Array, start: number, end: number): void {}
public end(success: boolean): boolean { return true; }
}
class FastApcHandler implements IApcHandler {
public start(): void {}
public put(data: Uint32Array, start: number, end: number): void {}
public end(success: boolean): boolean { return true; }
}
perfContext('Parser throughput - 50MB data', () => {
let parsed: Uint32Array;
let parser: EscapeSequenceParser;
beforeEach(() => {
parser = new EscapeSequenceParser();
parser.setPrintHandler((data, start, end) => {});
parser.registerCsiHandler({ final: '@' }, params => true);
parser.registerCsiHandler({ final: 'A' }, params => true);
parser.registerCsiHandler({ final: 'B' }, params => true);
parser.registerCsiHandler({ final: 'C' }, params => true);
parser.registerCsiHandler({ final: 'D' }, params => true);
parser.registerCsiHandler({ final: 'E' }, params => true);
parser.registerCsiHandler({ final: 'F' }, params => true);
parser.registerCsiHandler({ final: 'G' }, params => true);
parser.registerCsiHandler({ final: 'H' }, params => true);
parser.registerCsiHandler({ final: 'I' }, params => true);
parser.registerCsiHandler({ final: 'J' }, params => true);
parser.registerCsiHandler({ final: 'K' }, params => true);
parser.registerCsiHandler({ final: 'L' }, params => true);
parser.registerCsiHandler({ final: 'M' }, params => true);
parser.registerCsiHandler({ final: 'P' }, params => true);
parser.registerCsiHandler({ final: 'S' }, params => true);
parser.registerCsiHandler({ final: 'T' }, params => true);
parser.registerCsiHandler({ final: 'X' }, params => true);
parser.registerCsiHandler({ final: 'Z' }, params => true);
parser.registerCsiHandler({ final: '`' }, params => true);
parser.registerCsiHandler({ final: 'a' }, params => true);
parser.registerCsiHandler({ final: 'b' }, params => true);
parser.registerCsiHandler({ final: 'c' }, params => true);
parser.registerCsiHandler({ final: 'd' }, params => true);
parser.registerCsiHandler({ final: 'e' }, params => true);
parser.registerCsiHandler({ final: 'f' }, params => true);
parser.registerCsiHandler({ final: 'g' }, params => true);
parser.registerCsiHandler({ final: 'h' }, params => true);
parser.registerCsiHandler({ final: 'l' }, params => true);
parser.registerCsiHandler({ final: 'm' }, params => true);
parser.registerCsiHandler({ final: 'n' }, params => true);
parser.registerCsiHandler({ final: 'p' }, params => true);
parser.registerCsiHandler({ final: 'q' }, params => true);
parser.registerCsiHandler({ final: 'r' }, params => true);
parser.registerCsiHandler({ final: 's' }, params => true);
parser.registerCsiHandler({ final: 'u' }, params => true);
parser.setExecuteHandler(C0.BEL, () => true);
parser.setExecuteHandler(C0.LF, () => true);
parser.setExecuteHandler(C0.VT, () => true);
parser.setExecuteHandler(C0.FF, () => true);
parser.setExecuteHandler(C0.CR, () => true);
parser.setExecuteHandler(C0.BS, () => true);
parser.setExecuteHandler(C0.HT, () => true);
parser.setExecuteHandler(C0.SO, () => true);
parser.setExecuteHandler(C0.SI, () => true);
parser.setExecuteHandler(C1.IND, () => true);
parser.setExecuteHandler(C1.NEL, () => true);
parser.setExecuteHandler(C1.HTS, () => true);
parser.registerOscHandler(0, new OscHandler(data => true));
parser.registerOscHandler(1, new FastOscHandler());
parser.registerEscHandler({ final: '7' }, () => true);
parser.registerEscHandler({ final: '8' }, () => true);
parser.registerEscHandler({ final: 'D' }, () => true);
parser.registerEscHandler({ final: 'E' }, () => true);
parser.registerEscHandler({ final: 'H' }, () => true);
parser.registerEscHandler({ final: 'M' }, () => true);
parser.registerEscHandler({ final: '=' }, () => true);
parser.registerEscHandler({ final: '>' }, () => true);
parser.registerEscHandler({ final: 'c' }, () => true);
parser.registerEscHandler({ final: 'n' }, () => true);
parser.registerEscHandler({ final: 'o' }, () => true);
parser.registerEscHandler({ final: '|' }, () => true);
parser.registerEscHandler({ final: '}' }, () => true);
parser.registerEscHandler({ final: '~' }, () => true);
parser.registerEscHandler({ intermediates: '%', final: '@' }, () => true);
parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => true);
parser.registerDcsHandler({ final: 'p' }, new DcsHandler(data => true));
parser.registerDcsHandler({ final: 'q' }, new FastDcsHandler());
parser.registerApcHandler({ final: 'X' }, new ApcHandler(data => true));
parser.registerApcHandler({ final: 'Y' }, new FastApcHandler());
});
perfContext('PRINT - a', () => {
before(() => {
const data = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('EXECUTE - \\n', () => {
before(() => {
const data = '\n\n\n\n\n\n\n';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('ESCAPE - ESC E', () => {
before(() => {
const data = '\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('ESCAPE with collect - ESC % G', () => {
before(() => {
const data = '\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('CSI - CSI A', () => {
before(() => {
const data = '\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('CSI with collect - CSI ? p', () => {
before(() => {
const data = '\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('CSI with params (short) - CSI 1;2 m', () => {
before(() => {
const data = '\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('CSI with params (long) - CSI 1;2;3;4;5;6;7;8;9;0 m', () => {
before(() => {
const data = '\x1b[1;2;3;4;5;6;7;8;9;0m\x1b[1;2;3;4;5;6;7;8;9;0m\x1b[1;2;3;4;5;6;7;8;9;0m';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('OSC string interface (short seq) - OSC 0;hi ST', () => {
before(() => {
const data = '\x1b]0;hi\x1b\\\x1b]0;hi\x1b\\\x1b]0;hi\x1b\\\x1b]0;hi\x1b\\x1b]0;hi\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('OSC string interface (long seq) - OSC 0;<text> ST', () => {
before(() => {
const data = '\x1b]0;Lorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('OSC class interface (short seq) - OSC 0;hi ST', () => {
before(() => {
const data = '\x1b]1;hi\x1b\\\x1b]1;hi\x1b\\\x1b]1;hi\x1b\\\x1b]1;hi\x1b\\x1b]1;hi\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('OSC class interface (long seq) - OSC 0;<text> ST', () => {
before(() => {
const data = '\x1b]1;Lorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('DCS string interface (short seq)', () => {
before(() => {
const data = '\x1bPphi\x1b\\\x1bPphi\x1b\\\x1bPphi\x1b\\\x1bPphi\x1b\\\x1bPphi\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('DCS string interface (long seq)', () => {
before(() => {
const data = '\x1bPpLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('DCS class interface (short seq)', () => {
before(() => {
const data = '\x1bPqhi\x1b\\\x1bPqhi\x1b\\\x1bPqhi\x1b\\\x1bPqhi\x1b\\\x1bPqhi\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('DCS class interface (long seq)', () => {
before(() => {
const data = '\x1bPqLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('APC string interface (short seq)', () => {
before(() => {
const data = '\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('APC string interface (long seq)', () => {
before(() => {
const data = '\x1b_XLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('APC class interface (short seq)', () => {
before(() => {
const data = '\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
perfContext('APC class interface (long seq)', () => {
before(() => {
const data = '\x1b_YLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
let content = '';
while (content.length < SIZE) {
content += data;
}
parsed = toUtf32(content);
});
new ThroughputRuntimeCase('', async () => {
parser.parse(parsed, parsed.length);
return { payloadSize: parsed.length };
}, { fork: true }).showAverageThroughput();
});
});