From b411bf33fd38942b3afeace1e4f23bef4e60c4f6 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:00:37 -0700 Subject: [PATCH] perf(runtime): reduce terminal ANSI scanning work (#27344) Adapted from outof-place normalization work in #27170. Preserve tail behavior while avoiding repeated CSI scans and newline-density regressions. Co-authored-by: outof-place --- .../runtime/terminal-ansi-normalization.ts | 144 +++++++++++------- .../terminal-vertical-control-scan.test.ts | 25 +++ 2 files changed, 112 insertions(+), 57 deletions(-) diff --git a/src/main/runtime/terminal-ansi-normalization.ts b/src/main/runtime/terminal-ansi-normalization.ts index e1bbe33a79d..6a5cb984351 100644 --- a/src/main/runtime/terminal-ansi-normalization.ts +++ b/src/main/runtime/terminal-ansi-normalization.ts @@ -1,5 +1,8 @@ import { MAX_TAIL_PENDING_ANSI_CHARS } from './terminal-tail-limits' -import { classifyTerminalEscapeIntroducer } from '../../shared/terminal-escape-introducer' +import { + classifyTerminalEscapeIntroducer, + type TerminalEscapeIntroducer +} from '../../shared/terminal-escape-introducer' import { ownRetainedString } from '../../shared/own-retained-string' export function parseAnsiControlSequence( @@ -14,44 +17,56 @@ export function parseAnsiControlSequence( | null { // charCodeAt, not value[i]: indexing mints a one-char string on every escape. const introducer = classifyTerminalEscapeIntroducer(value.charCodeAt(escapeIndex + 1)) + const endIndex = findTerminalEscapeSequenceEnd(value, escapeIndex, introducer) + if (endIndex === -1) { + return null + } + if (introducer !== 'csi') { + return { kind: 'other', endIndex } + } + return parseTerminalCsiAtEnd(value, escapeIndex, endIndex) +} + +function parseTerminalCsiAtEnd(value: string, escapeIndex: number, endIndex: number) { + const params = value.slice(escapeIndex + 2, endIndex) + const firstParamMatch = /^(\d+)/.exec(params) + return { + kind: 'csi' as const, + final: value[endIndex] ?? '', + params, + firstParam: firstParamMatch ? Number(firstParamMatch[1]) : null, + endIndex + } +} + +/** The index of the sequence's last code unit, or -1 when the input ends first. */ +function findTerminalEscapeSequenceEnd( + value: string, + escapeIndex: number, + introducer: TerminalEscapeIntroducer +): number { if (introducer === 'csi') { for (let index = escapeIndex + 2; index < value.length; index += 1) { const code = value.charCodeAt(index) - if (code < 0x40 || code > 0x7e) { - continue - } - const params = value.slice(escapeIndex + 2, index) - const firstParamMatch = /^(\d+)/.exec(params) - return { - kind: 'csi', - final: value[index] ?? '', - params, - firstParam: firstParamMatch ? Number(firstParamMatch[1]) : null, - endIndex: index + if (code >= 0x40 && code <= 0x7e) { + return index } } - return null + return -1 } - if (introducer === 'osc') { + if (introducer === 'osc' || introducer === 'string') { for (let index = escapeIndex + 2; index < value.length; index += 1) { - if (value[index] === '\u0007') { - return { kind: 'other', endIndex: index } + const code = value.charCodeAt(index) + if (code === 0x07 && introducer === 'osc') { + return index } - if (value[index] === '\u001b' && value[index + 1] === '\\') { - return { kind: 'other', endIndex: index + 1 } + if (code === 0x1b && value.charCodeAt(index + 1) === 0x5c) { + return index + 1 } } - return null + return -1 } - if (introducer === 'string') { - for (let index = escapeIndex + 2; index < value.length; index += 1) { - if (value[index] === '\u001b' && value[index + 1] === '\\') { - return { kind: 'other', endIndex: index + 1 } - } - } - return null - } - return { kind: 'other', endIndex: escapeIndex + 1 } + return escapeIndex + 1 } export function hasCanonicalNumericCsiParams(params: string): boolean { @@ -61,18 +76,19 @@ export function hasCanonicalNumericCsiParams(params: string): boolean { export function containsTerminalVerticalLineControl(value: string): boolean { // Only ESC can introduce a vertical control; ordinary output needs no code-unit walk. for (let index = value.indexOf('\x1b'); index !== -1; index = value.indexOf('\x1b', index + 1)) { - const parsed = parseAnsiControlSequence(value, index) - if (!parsed) { + const introducer = classifyTerminalEscapeIntroducer(value.charCodeAt(index + 1)) + const endIndex = findTerminalEscapeSequenceEnd(value, index, introducer) + if (endIndex === -1) { return false } - index = parsed.endIndex if ( - parsed.kind === 'csi' && - parsed.final === 'A' && - hasCanonicalNumericCsiParams(parsed.params) + introducer === 'csi' && + value.charCodeAt(endIndex) === 0x41 && + hasCanonicalNumericCsiParams(value.slice(index + 2, endIndex)) ) { return true } + index = endIndex } return false } @@ -88,42 +104,50 @@ export function normalizeTerminalChunk( const combined = `${pendingAnsi}${chunk}` const parts: string[] = [] let textStart = 0 + // Copy spans and skip parameter parsing for controls the preview discards. for (let index = 0; index < combined.length; index += 1) { - const char = combined[index] - if (char === '\x1b') { + const code = combined.charCodeAt(index) + if (code >= 0x20 && code < 0x7f) { + continue + } + if (code === 0x1b) { appendTerminalNormalizedSpan(parts, combined, textStart, index) if (index + 1 >= combined.length) { return { text: parts.join(''), pendingAnsi: combined.slice(index) } } - const parsed = parseAnsiControlSequence(combined, index) - if (!parsed) { + const introducer = classifyTerminalEscapeIntroducer(combined.charCodeAt(index + 1)) + const endIndex = findTerminalEscapeSequenceEnd(combined, index, introducer) + if (endIndex === -1) { return { text: parts.join(''), // Own the tail so it stops pinning the consumed chunk it was sliced from. pendingAnsi: ownRetainedString(trimPendingAnsiControl(combined.slice(index))) } } - if (parsed.kind === 'csi' && isTerminalPreviewLineControl(parsed)) { - // Why: Codex redraws status text with ANSI controls but no CR; keep them so the tail overwrites the prior frame. - parts.push(combined.slice(index, parsed.endIndex + 1)) + if ( + introducer === 'csi' && + isTerminalPreviewLineControlFinal(combined.charCodeAt(endIndex)) + ) { + const parsed = parseTerminalCsiAtEnd(combined, index, endIndex) + if (isTerminalPreviewLineControl(parsed)) { + // Why: Codex redraws status text with ANSI controls but no CR; keep them so the tail overwrites the prior frame. + parts.push(combined.slice(index, endIndex + 1)) + } } - index = parsed.endIndex + index = endIndex textStart = index + 1 continue } - if (char === '\r' && combined[index + 1] === '\n') { + if (code === 0x0d && combined.charCodeAt(index + 1) === 0x0a) { + // CRLF keeps only its LF, which starts the next span. appendTerminalNormalizedSpan(parts, combined, textStart, index) - parts.push('\n') - index += 1 textStart = index + 1 continue } - const code = combined.charCodeAt(index) if (code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0d) { - appendTerminalNormalizedSpan(parts, combined, textStart, index) - parts.push(char) - textStart = index + 1 - } else if (!isTerminalPreviewPrintableCodeUnit(code)) { + continue + } + if (!isTerminalPreviewPrintableCodeUnit(code)) { appendTerminalNormalizedSpan(parts, combined, textStart, index) textStart = index + 1 } @@ -173,6 +197,18 @@ function trimPendingAnsiControl(value: string): string { return `${introducer}${value.slice(-suffixBudget)}` } +/** CSI controls that affect retained preview rows. */ +function isTerminalPreviewLineControlFinal(code: number): boolean { + return ( + code === 0x4b || + code === 0x41 || + code === 0x47 || + code === 0x60 || + code === 0x44 || + code === 0x43 + ) +} + function isTerminalPreviewLineControl(parsed: { final: string params: string @@ -185,11 +221,5 @@ function isTerminalPreviewLineControl(parsed: { const mode = parsed.firstParam ?? 0 return mode === 0 || mode === 1 || mode === 2 } - return ( - parsed.final === 'A' || - parsed.final === 'G' || - parsed.final === '`' || - parsed.final === 'D' || - parsed.final === 'C' - ) + return isTerminalPreviewLineControlFinal(parsed.final.charCodeAt(0)) } diff --git a/src/main/runtime/terminal-vertical-control-scan.test.ts b/src/main/runtime/terminal-vertical-control-scan.test.ts index b672e28c921..9608b7010ad 100644 --- a/src/main/runtime/terminal-vertical-control-scan.test.ts +++ b/src/main/runtime/terminal-vertical-control-scan.test.ts @@ -6,6 +6,31 @@ import { import { appendNormalizedToTailBuffer } from './terminal-tail-buffer' describe('terminal vertical-control scanning', () => { + it('scans retained CSI parameters once while normalizing', () => { + const input = `\x1b[${'1;'.repeat(2048)}A` + const charCodeAt = vi.spyOn(String.prototype, 'charCodeAt') + let normalized: ReturnType + let inspections: number + try { + normalized = normalizeTerminalChunk(input) + inspections = charCodeAt.mock.calls.length + } finally { + charCodeAt.mockRestore() + } + + expect(normalized).toEqual({ text: input, pendingAnsi: '' }) + expect(inspections).toBeLessThanOrEqual(input.length + 8) + }) + + it('preserves printable spans and carried controls across chunk boundaries', () => { + const first = normalizeTerminalChunk('漢\ud800\x1b[31m字\t\r\n\x00a\x1b[2') + expect(first).toEqual({ text: '漢\ud800字\t\na', pendingAnsi: '\x1b[2' }) + expect(normalizeTerminalChunk('K\udfff😀\b\r\x7f\x9fend', first.pendingAnsi)).toEqual({ + text: '\x1b[2K\udfff😀\b\rend', + pendingAnsi: '' + }) + }) + it.each([ ['plain', 'log output '.repeat(8192), false], ['nonvertical CSI', `\x1b[31m${'log output '.repeat(8192)}\x1b[0m`, false],