Add benchmark coverage for whole-word navigation and reverse regex whole-word searches to guard optimization regressions. Co-authored-by: Cursor <cursoragent@cursor.com>
259 lines
9.6 KiB
TypeScript
259 lines
9.6 KiB
TypeScript
/**
|
|
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
|
|
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { Terminal } from 'browser/public/Terminal';
|
|
import { SearchAddon } from 'SearchAddon';
|
|
import type { ISearchOptions } from '@xterm/addon-search';
|
|
|
|
const enum Constants {
|
|
COLS = 120,
|
|
ROWS = 40,
|
|
SCROLLBACK = 8000,
|
|
NAVIGATION_ITERATIONS = 250,
|
|
INCREMENTAL_ITERATIONS = 75,
|
|
DECORATION_REFRESH_ITERATIONS = 25
|
|
}
|
|
|
|
const DECORATION_OPTIONS: ISearchOptions = {
|
|
decorations: {
|
|
activeMatchBackground: '#0000ff',
|
|
activeMatchColorOverviewRuler: '#ff0000',
|
|
matchBackground: '#ffff00',
|
|
matchBorder: '#000000',
|
|
matchOverviewRuler: '#00ffff'
|
|
}
|
|
};
|
|
|
|
class TestTerminal extends Terminal {
|
|
public writeSync(data: string): void {
|
|
(this as any)._core.writeSync(data);
|
|
}
|
|
|
|
public override select(_column: number, _row: number, _length: number): void {
|
|
// SearchAddon always selects matches; skip selection internals in headless benchmark runs.
|
|
}
|
|
|
|
public override clearSelection(): void {
|
|
// SearchAddon clears the previous selection before selecting another match.
|
|
}
|
|
|
|
public override scrollLines(_amount: number): void {
|
|
// No viewport scrolling is needed for API runtime measurement.
|
|
}
|
|
}
|
|
|
|
function buildRealWorldBuffer(): string {
|
|
const fixture = readFileSync(resolve(__dirname, '../../fixtures/issue-2444'), 'utf8')
|
|
.replace(/\r?\n/g, '\r\n');
|
|
const lines: string[] = [fixture];
|
|
for (let i = 0; i < 2500; i++) {
|
|
const minute = (i % 60).toString().padStart(2, '0');
|
|
const second = ((i * 7) % 60).toString().padStart(2, '0');
|
|
lines.push(`2026-05-25T16:${minute}:${second}Z INFO [api-${i % 8}] requestId=req-${i.toString(16).padStart(6, '0')} path=/v1/projects/${i % 120}/search?q=opencv status=200 durationMs=${10 + (i % 90)}`);
|
|
if (i % 5 === 0) {
|
|
lines.push(`2026-05-25T16:${minute}:${second}Z WARN [worker-${i % 4}] slow query detected query="SELECT * FROM runs WHERE status='pending'" elapsedMs=${500 + (i % 1200)}`);
|
|
}
|
|
if (i % 11 === 0) {
|
|
lines.push(`2026-05-25T16:${minute}:${second}Z ERROR [worker-${i % 4}] command failed: package.json parse error at column ${10 + (i % 70)}; retrying in ${2 + (i % 7)}s`);
|
|
}
|
|
if (i % 17 === 0) {
|
|
lines.push(`https://example.internal/run/${i}/details?source=terminal-search&state=error`);
|
|
}
|
|
}
|
|
return lines.join('\r\n') + '\r\n';
|
|
}
|
|
|
|
perfContext('SearchAddon API on real-world terminal content', () => {
|
|
const bufferContent = buildRealWorldBuffer();
|
|
|
|
perfContext('findNext/plain navigation', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon();
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.NAVIGATION_ITERATIONS; i++) {
|
|
if (search.findNext('opencv')) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
if (foundCount !== Constants.NAVIGATION_ITERATIONS) {
|
|
throw new Error(`Expected ${Constants.NAVIGATION_ITERATIONS} matches, got ${foundCount}`);
|
|
}
|
|
return { payloadSize: Constants.NAVIGATION_ITERATIONS, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
|
|
perfContext('findNext/no match full scan', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon();
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.NAVIGATION_ITERATIONS; i++) {
|
|
if (search.findNext('zzzznotfoundzzzz')) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
if (foundCount !== 0) {
|
|
throw new Error(`Expected 0 matches, got ${foundCount}`);
|
|
}
|
|
return { payloadSize: Constants.NAVIGATION_ITERATIONS, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
|
|
perfContext('findNext/case insensitive navigation', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon();
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.NAVIGATION_ITERATIONS; i++) {
|
|
if (search.findNext('OPENCV', { caseSensitive: false })) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
if (foundCount !== Constants.NAVIGATION_ITERATIONS) {
|
|
throw new Error(`Expected ${Constants.NAVIGATION_ITERATIONS} matches, got ${foundCount}`);
|
|
}
|
|
return { payloadSize: Constants.NAVIGATION_ITERATIONS, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
|
|
perfContext('findNext/regex navigation', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon();
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.NAVIGATION_ITERATIONS; i++) {
|
|
if (search.findNext('https://example\\.internal/run/\\d+/details\\?source=terminal-search', { regex: true })) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
if (foundCount !== Constants.NAVIGATION_ITERATIONS) {
|
|
throw new Error(`Expected ${Constants.NAVIGATION_ITERATIONS} matches, got ${foundCount}`);
|
|
}
|
|
return { payloadSize: Constants.NAVIGATION_ITERATIONS, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
|
|
perfContext('findPrevious/incremental typing flow', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
const terms = ['e', 'er', 'err', 'erro', 'error'];
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon();
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.INCREMENTAL_ITERATIONS; i++) {
|
|
for (const term of terms) {
|
|
if (search.findPrevious(term, { incremental: true })) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
}
|
|
return { payloadSize: Constants.INCREMENTAL_ITERATIONS * terms.length, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
|
|
perfContext('findNext/wholeWord dense-punctuation', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon();
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.NAVIGATION_ITERATIONS; i++) {
|
|
if (search.findNext('error', { wholeWord: true })) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
if (foundCount !== Constants.NAVIGATION_ITERATIONS) {
|
|
throw new Error(`Expected ${Constants.NAVIGATION_ITERATIONS} matches, got ${foundCount}`);
|
|
}
|
|
return { payloadSize: Constants.NAVIGATION_ITERATIONS, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
|
|
perfContext('findPrevious/regex wholeWord reverse', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon();
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.NAVIGATION_ITERATIONS; i++) {
|
|
if (search.findPrevious('error', { regex: true, wholeWord: true })) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
if (foundCount !== Constants.NAVIGATION_ITERATIONS) {
|
|
throw new Error(`Expected ${Constants.NAVIGATION_ITERATIONS} matches, got ${foundCount}`);
|
|
}
|
|
return { payloadSize: Constants.NAVIGATION_ITERATIONS, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
|
|
perfContext('findNext/decorations refresh', () => {
|
|
let terminal: TestTerminal;
|
|
let search: SearchAddon;
|
|
before(() => {
|
|
terminal = new TestTerminal({ cols: Constants.COLS, rows: Constants.ROWS, scrollback: Constants.SCROLLBACK });
|
|
search = new SearchAddon({ highlightLimit: 1500 });
|
|
search.activate(terminal);
|
|
terminal.writeSync(bufferContent);
|
|
});
|
|
new RuntimeCase('', () => {
|
|
let foundCount = 0;
|
|
for (let i = 0; i < Constants.DECORATION_REFRESH_ITERATIONS; i++) {
|
|
const term = i % 2 === 0 ? 'WARN' : 'ERROR';
|
|
if (search.findNext(term, DECORATION_OPTIONS)) {
|
|
foundCount++;
|
|
}
|
|
}
|
|
if (foundCount !== Constants.DECORATION_REFRESH_ITERATIONS) {
|
|
throw new Error(`Expected ${Constants.DECORATION_REFRESH_ITERATIONS} matches, got ${foundCount}`);
|
|
}
|
|
return { payloadSize: Constants.DECORATION_REFRESH_ITERATIONS, foundCount };
|
|
}, { fork: false }).showAverageRuntime();
|
|
});
|
|
});
|