mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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 <contact@outofplace.space>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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<typeof normalizeTerminalChunk>
|
||||
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],
|
||||
|
||||
Reference in New Issue
Block a user