7.7 KiB
slim-forge: xterm.js parser perf
An experiment to autonomously speed up xterm.js's EscapeSequenceParser, the terminal escape sequence parser inside VS Code, GitHub Codespaces, and most web-based terminals.
Setup
- Confirm the workspace: fresh clone of
xtermjs/xterm.js. Branch should beautoresearch/xterm-<date>. Verify:git status,git log --oneline -5. - Read the in-scope files:
src/common/parser/EscapeSequenceParser.ts— ~845 LOC. THIS is what you optimize. The mainparse()method is the hot loop. Read the whole file.src/common/parser/Params.ts— parameter accumulation. Called from the PARAM action in the main loop. Secondary surface.src/common/parser/Constants.ts—ParserStateandParserActionenums.src/common/parser/EscapeSequenceParser.test.ts— ~2236 LOC of tests. Read before making changes.bench/workload.js— the benchmark. Do NOT modify.run.py— the harness. Do NOT modify.program.md— this file.
- Verify baseline exists:
cat results.tsv. One row,baseline, with ambpsvalue andtests_pass: 1. - Confirm and go.
Mission
Primary metric: mbps — weighted throughput in MB/s across 5 representative terminal workloads (plain ASCII print, SGR-heavy colored output, cursor movement sequences, parameter-heavy truecolor, mixed realistic git-log-style output). Higher is better.
Hard gates:
npm run test-unitpasses (2261 mocha tests, ~6s).- The harness enforces
mbps > best_so_far * 1.05for a row to be labeledkeep.
Important: build pipeline
xterm.js is TypeScript. Your edits go in src/. The build pipeline is:
npm run build # tsc: src/**/*.ts -> out/**/*.js
npm run esbuild # esbuild: out/ -> out-esbuild/ (what tests and bench consume)
run.py handles this automatically. But if you want to test manually:
npm run build && npm run esbuild && npm run test-unit && node bench/workload.js
The agent edits .ts source. The bench runs against compiled .js in out-esbuild/. If your edit has a TS type error, npm run build fails and the experiment is discard.
What you CAN do
- Edit
src/common/parser/EscapeSequenceParser.ts— the main target. - Edit
src/common/parser/Params.ts— if parameter handling is the bottleneck. - Edit
src/common/parser/OscParser.ts,DcsParser.ts,ApcParser.ts— if those sub-parsers show up in the bench. - Add internal helper functions or restructure the parse loop.
What you CANNOT do
- Modify
src/common/parser/EscapeSequenceParser.test.tsor any test file. - Modify
bench/workload.jsorrun.py. - Modify
program.md. - Modify the
TransitionTableinitialization — the VT500 table is the spec-correct state machine. Changing transitions changes which sequences are recognized, which is a correctness violation even if tests pass. - Cache parse results across calls. The parser is stateful (it maintains
currentState), but eachparse()call must process every byte of input. No memoization. - Remove or weaken handler dispatch. The bench registers real handlers; skipping dispatch would be a benchmark exploit.
What the parser already does well
Before you start, understand what's already optimized so you don't waste time re-discovering it:
- Transition table:
Uint16Arraylookup — state × charCode → action + nextState. One array access per byte. This is already near-optimal for the state machine itself. - PRINT action loop unrolling: 4× unrolled inner loop for consecutive printable characters. This is why
print_asciihits ~2500 MB/s. - PARAM inner loop: do-while that stays in the PARAM action for consecutive digit/semicolon/colon bytes without re-entering the main switch.
Where to look for wins
The bench breakdown shows where time is actually spent:
sgr_heavy: ~84 MB/s — 30× slower than print. Every SGR sequence goes through: state transitions forESC [ params m, param parsing, CSI handler dispatch. The handler dispatch involves array lookups and function calls per sequence.params_heavy: ~92 MB/s — similar. Truecolor sequences have many params (e.g.38;2;0;255;128).cursor_move: ~127 MB/s — CSI sequences with fewer params but more state transitions per byte.mixed_realistic: ~158 MB/s — blend of print runs and SGR.
Likely sources of real speedups:
- Reduce handler dispatch overhead. The CSI dispatch does
this._csiHandlers[this._collect << 8 | code]then iterates the handler array in reverse. For the common case of a single registered handler, the array iteration +instanceof Promisecheck is overhead. A fast-path for single-handler case could help. - Inline the PARAM parsing more aggressively. The current do-while re-enters
this._params.addDigit()/addParam()/addSubParam()per byte. IfParamsmethods are not inlined by V8, each is a virtual dispatch. - Reduce property access in the hot loop.
this._transitions,this.currentState,this._collect,this._params— eachthis.access goes through the object's hidden class. Caching in locals at the top ofparse()and writing back at the end could help. - Specialize the common case in the main switch. PRINT and PARAM are the most common actions. If the switch has many cases, V8 may generate a jump table; if it has few hot cases, a nested if-else could be faster.
- The
code < 0xa0 ? code : NON_ASCII_PRINTABLEternary runs per byte. For ASCII-only input (the overwhelming common case), the branch is always taken. Consider a fast-path that skips the check when all input is < 0x80.
Anti-patterns:
- Don't try to change the transition table layout (e.g. making it a 2D array). The
Uint16Arraywith bit shifting is already the most cache-friendly layout. - Don't try to replace the state machine with regex. The state machine handles streaming input correctly; regex can't.
- Don't optimize the async handler resume path (top of
parse()). It's cold code.
Output format
run.py prints:
---
mbps: 693.3
tests_passed: 1
build_ok: 1
install_seconds: 5.0
build_seconds: 15.0
test_seconds: 6.0
best_so_far: 693.3
threshold: 728.0 (best_so_far * 1.05)
auto-status: keep
Read the headline: grep "^mbps:\|^tests_passed:\|^auto-status:" run.log
Per-case breakdown is on stderr from the bench: grep "per-case" -A 10 run.log (or check the bench stderr output).
Logging results
python run.py --record --description "<one line>" appends a row. The harness auto-labels status.
Schema: commit mbps tests_pass build_ok status description
The experiment loop
LOOP FOREVER:
git log --oneline -10andtail -10 results.tsv. What's the current best? What has been tried?- Pick one optimization idea. Be specific.
- Edit
src/common/parser/EscapeSequenceParser.ts(or Params.ts). One change per experiment. git add -A && git commit -m "try: <description>"(commit BEFORE the record run).python run.py --record --description "<description>" > run.log 2>&1grep "^mbps:\|^tests_passed:\|^auto-status:" run.log.- Decision:
auto-status: keep→ the branch advances.auto-status: discard→git reset --hard HEAD~1.
- Goto 1.
Build failures: if npm run build fails, it's a TS type error from your edit. Read the tsc output in run.log, fix the type error or revert.
Verification for large wins: if auto-status: keep on a >15% improvement, run python run.py > /tmp/verify.log 2>&1 (no --record). If verify mbps is also clearly above threshold, it's real. If verify is close to baseline, it was noise — git reset --hard HEAD~1.
NEVER STOP: once the loop has begun, do NOT pause. Run indefinitely until manually stopped.