perf(terminal): bound the reattach payload agent-signal scan (#12681)

hasCursorAgentReattachPayloadScreenSignal built a char-by-char copy of the
entire reattach payload so it could read the last header plus 5000 chars. On a
2MB daemon snapshot that cost 17.5ms of synchronous renderer main-thread work —
~75% of what xterm then spends parsing the same bytes — and the miss case paid
it in full for a result that is always false.

Two changes, both matching existing in-tree precedent: bound the scan to a
256KB tail (as the kitty tracker already bounds its own scan), and strip via
the shared precompiled CSI_SEQUENCE_PATTERN instead of a hand-rolled loop,
which is also faster in V8 because it copies spans rather than building a rope
per character.

  2MB snapshot, header hit   17.5ms -> 0.80ms  (22x)
  2MB snapshot, miss          8.7ms -> 0.52ms  (17x)
  200KB snapshot, header hit  1.5ms -> 0.62ms  (2.4x)

config/scripts/terminal-reattach-payload-scan-benchmark.mjs reproduces this and
asserts every candidate agrees with the baseline before timing it. It also
records a negative result: porting the daemon mouse mirror's includes()
pre-filter to the kitty tracker makes reattach slower, because snapshots always
contain the introducer.

Adds guards for the two behaviours a future shortcut would silently break: a
CSI-split header must still match, and a header behind the tail bound must not.
Also byte-pins POST_REPLAY_REATTACH_RESET_KEEP_MOUSE, which shipped unpinned.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil 2026-08-06 00:23:57 -07:00 committed by GitHub
parent 39ba201ed2
commit a7ffb244e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 373 additions and 22 deletions

View File

@ -0,0 +1,274 @@
#!/usr/bin/env node
// Benchmark: the renderer-side scans that run over a whole reattach payload.
//
// 1. hasCursorAgentReattachPayloadScreenSignal — compares the old hand-rolled char-by-char
// CSI strip against the shipped shape (256KB tail + shared CSI_SEQUENCE_PATTERN). Every
// variant is asserted to agree with the baseline before it is timed.
// 2. TerminalKittyKeyboardModeTracker.scanReplay — measured to justify leaving it alone, and
// to record that porting the daemon mouse mirror's includes() pre-filter makes it slower.
//
// Payloads are generated deterministically (LCG, no Math.random) so runs compare.
//
// Run with: node config/scripts/terminal-reattach-payload-scan-benchmark.mjs
import { performance } from 'node:perf_hooks'
import { CSI_SEQUENCE_PATTERN } from '../../src/shared/ansi-escape-sequences.ts'
import { TerminalKittyKeyboardModeTracker } from '../../src/shared/terminal-kitty-keyboard-mode-tracker.ts'
const ROUNDS = Number(process.env.ORCA_REATTACH_SCAN_BENCH_ROUNDS ?? '7')
const MIN_ITERATION_MS = 120
// --- payload generation -----------------------------------------------------
function makeRng(seed) {
let state = seed >>> 0
return () => {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0
return state / 0x100000000
}
}
const WORDS = [
'src',
'renderer',
'components',
'terminal',
'pane',
'connection',
'reattach',
'payload',
'snapshot',
'daemon',
'passed',
'failed',
'warning',
'building',
'index.ts',
'1.24s',
'ok'
]
// Heavily SGR-colored scrollback, ~120 cols/line — what a serialized daemon snapshot
// of a colorized TUI/build log looks like.
function buildColoredScrollback(targetBytes, seed) {
const rng = makeRng(seed)
const lines = []
let size = 0
while (size < targetBytes) {
let line = ''
let visible = 0
while (visible < 118) {
const word = WORDS[Math.floor(rng() * WORDS.length)]
const color = 16 + Math.floor(rng() * 200)
line += `\x1b[38;5;${color}m${word}\x1b[0m `
visible += word.length + 1
}
lines.push(line)
size += line.length + 1
}
return lines.join('\r\n')
}
// Live TUIs also emit cursor moves / erases; keep some of those in the mix.
function withCursorTraffic(body, seed) {
const rng = makeRng(seed)
return body
.split('\r\n')
.map((line, i) => `\x1b[${(i % 40) + 1};1H\x1b[K${line}${rng() < 0.1 ? '\x1b[?25l' : ''}`)
.join('\r\n')
}
const CURSOR_AGENT_SCREEN = [
'\x1b[38;5;39mCursor Agent\x1b[0m',
'',
' \x1b[2mReady\x1b[0m',
'',
'\x1b[38;5;245m→ \x1b[0m'
].join('\r\n')
function buildPayloads() {
const base200k = buildColoredScrollback(200 * 1024, 1)
const base2m = buildColoredScrollback(2 * 1024 * 1024, 2)
const relayTail = buildColoredScrollback(100 * 1024, 3)
return [
{ name: '200KB snapshot, header hit (tail)', data: `${base200k}\r\n${CURSOR_AGENT_SCREEN}` },
{ name: '200KB snapshot, no header (miss)', data: base200k },
{ name: '2MB snapshot, header hit (tail)', data: `${base2m}\r\n${CURSOR_AGENT_SCREEN}` },
{ name: '2MB snapshot, no header (miss)', data: base2m },
{ name: '100KB relay tail, cursor traffic', data: withCursorTraffic(relayTail, 4) }
]
}
// --- strip variants ---------------------------------------------------------
// Baseline: verbatim copy of pty-connection.ts:448 (not exported).
function stripBaseline(data) {
let normalized = ''
let index = 0
while (index < data.length) {
if (data.charCodeAt(index) === 0x1b && data[index + 1] === '[') {
index += 2
while (index < data.length) {
const code = data.charCodeAt(index)
index += 1
if (code >= 0x40 && code <= 0x7e) {
break
}
}
continue
}
normalized += data[index]
index += 1
}
return normalized
}
const stripRegex = (data) => data.replace(CSI_SEQUENCE_PATTERN, '')
const HEADER = 'Cursor Agent'
const TAIL_CHARS = 5000
// Mirrors CURSOR_AGENT_REATTACH_SCAN_TAIL_LIMIT_CHARS in pty-connection.ts.
const SUFFIX_RAW_CHARS = 256 * 1024
const signalBaseline = (data) => hasSignal(stripBaseline(data))
const signalRegex = (data) => hasSignal(stripRegex(data))
// The shipped shape.
const signalSuffixRegex = (data) =>
hasSignal(stripRegex(data.length > SUFFIX_RAW_CHARS ? data.slice(-SUFFIX_RAW_CHARS) : data))
function hasSignal(normalized) {
const headerIndex = normalized.lastIndexOf(HEADER)
if (headerIndex === -1) {
return false
}
return normalized.slice(headerIndex + HEADER.length, headerIndex + TAIL_CHARS).includes(`${'→'} `)
}
// --- timing -----------------------------------------------------------------
function median(values) {
const sorted = [...values].sort((a, b) => a - b)
const mid = sorted.length >> 1
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
}
function timeMsPerCall(fn, arg) {
let iterations = 1
while (true) {
const start = performance.now()
for (let i = 0; i < iterations; i++) {
globalThis.__sink = fn(arg)
}
const elapsed = performance.now() - start
if (elapsed >= MIN_ITERATION_MS || iterations >= 1 << 22) {
return elapsed / iterations
}
iterations *= Math.max(2, Math.ceil(MIN_ITERATION_MS / Math.max(elapsed, 0.01)))
}
}
function measure(fn, arg) {
timeMsPerCall(fn, arg) // warmup / JIT
const samples = []
for (let round = 0; round < ROUNDS; round++) {
samples.push(timeMsPerCall(fn, arg))
}
return median(samples)
}
const pad = (value, width) => String(value).padStart(width)
const kb = (data) => `${(data.length / 1024).toFixed(0)}KB`
// --- run --------------------------------------------------------------------
const payloads = buildPayloads()
console.log('Reattach payload scans, ms/call (median of %d rounds). Lower is better.\n', ROUNDS)
console.log('== hasCursorAgentReattachPayloadScreenSignal ==')
console.log(
`${pad('payload', 36)} ${pad('size', 8)} ${pad('char-loop', 11)} ${pad('regex', 10)} ${pad('suffix+regex', 13)} ${pad('speedup', 9)}`
)
for (const { name, data } of payloads) {
const expected = signalBaseline(data)
for (const [label, fn] of Object.entries({
regex: signalRegex,
'suffix+regex': signalSuffixRegex
})) {
if (fn(data) !== expected) {
throw new Error(`${label} disagrees with baseline on "${name}"`)
}
}
const base = measure(signalBaseline, data)
const re = measure(signalRegex, data)
const suffix = measure(signalSuffixRegex, data)
console.log(
`${pad(name, 36)} ${pad(kb(data), 8)} ${pad(base.toFixed(3), 11)} ${pad(re.toFixed(3), 10)} ${pad(suffix.toFixed(3), 13)} ${pad(`${(base / suffix).toFixed(0)}x`, 9)}`
)
}
console.log('\n== TerminalKittyKeyboardModeTracker.scanReplay ==')
console.log(
`${pad('payload', 36)} ${pad('size', 8)} ${pad('scanReplay', 11)} ${pad('+includes gate', 15)}`
)
const kittyScan = (data) => {
const tracker = new TerminalKittyKeyboardModeTracker()
tracker.scanReplay(data)
return tracker.flags
}
// Mirrors src/main/daemon/terminal-mouse-mode-mirror.ts:41-47.
const kittyGated = (data) => {
if (!data.includes('\x1b[?') && !data.includes('\x1bc') && !data.includes('\x9b')) {
return 0
}
return kittyScan(data)
}
for (const { name, data } of payloads) {
console.log(
`${pad(name, 36)} ${pad(kb(data), 8)} ${pad(measure(kittyScan, data).toFixed(3), 11)} ${pad(measure(kittyGated, data).toFixed(3), 15)}`
)
}
// The gate can only pay off where no introducer exists: live plain-output chunks, not
// snapshots (which carry ?1049h/?25l by construction).
const liveChunk = buildColoredScrollback(4 * 1024, 5).replaceAll('\x1b', '')
const introducerRows = [
['4KB live chunk, no escapes', liveChunk],
['4KB live chunk, \\x1b[?25l present', `${liveChunk}\x1b[?25l`]
]
console.log(
`\n${pad('payload', 36)} ${pad('size', 8)} ${pad('scanReplay', 11)} ${pad('+includes gate', 15)}`
)
for (const [name, data] of introducerRows) {
console.log(
`${pad(name, 36)} ${pad(kb(data), 8)} ${pad(measure(kittyScan, data).toFixed(3), 11)} ${pad(measure(kittyGated, data).toFixed(3), 15)}`
)
}
console.log(
'\nSnapshot/replay payloads always contain \\x1b[?, so the includes() gate never fires on\nthe reattach path — it only helps the live per-chunk path.'
)
// Reference point: xterm parses the same bytes right after these scans run, so its cost
// is the yardstick for whether the scans are worth cutting.
const xtermHeadless = await import('@xterm/headless')
const Terminal = xtermHeadless.Terminal ?? xtermHeadless.default.Terminal
const writeToXterm = (data) =>
new Promise((resolve) => {
const terminal = new Terminal({ cols: 120, rows: 40, scrollback: 5000, allowProposedApi: true })
const start = performance.now()
terminal.write(data, () => {
const elapsed = performance.now() - start
terminal.dispose()
resolve(elapsed)
})
})
console.log(`\n== xterm headless parse of the same payload (reference) ==`)
console.log(`${pad('payload', 36)} ${pad('size', 8)} ${pad('xterm write', 11)}`)
for (const { name, data } of payloads) {
await writeToXterm(data)
const samples = []
for (let round = 0; round < ROUNDS; round++) {
samples.push(await writeToXterm(data))
}
console.log(`${pad(name, 36)} ${pad(kb(data), 8)} ${pad(median(samples).toFixed(3), 11)}`)
}

View File

@ -16427,6 +16427,81 @@ describe('connectPanePty', () => {
disposable.dispose()
})
// Why pinned: the classifier strips CSI precisely so a styled header still matches. A future
// "just lastIndexOf the raw bytes" shortcut would pass every other test and silently break this.
it('still detects the Cursor Agent screen when CSI styling splits the header and the marker', async () => {
const { connectPanePty } = await import('./pty-connection')
enableActiveRuntimeEnvironment()
const transport = createMockTransport('remote:env-1@@terminal-1')
const capturedReplayCallback: {
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
} = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedReplayCallback.current = callbacks.onReplayData ?? null
return { id: 'remote:env-1@@terminal-1', replay: '' }
})
transportFactoryQueue.push(transport)
setReattachPaneTitle('renamed shell')
const pane = createPane(1)
const textarea = {} as HTMLTextAreaElement
configureTerminalFocusMode(pane, textarea)
const manager = createManager(1)
const deps = createDeps()
const disposable = await withMockedDocumentActiveElement(textarea, async () => {
const connection = connectPanePty(pane as never, manager as never, deps as never)
await flushAsyncTicks(6)
capturedReplayCallback.current?.(
'\x1b[4;3HCursor \x1b[1mAgent\x1b[0m\x1b[9;3H→\x1b[0m Plan, search, build anything'
)
await flushAsyncTicks(12)
expect(pane.terminal.write).toHaveBeenCalledWith(
POST_REPLAY_LIVE_AGENT_REATTACH_RESET,
expect.any(Function)
)
return connection
})
disposable.dispose()
})
// Why pinned: the scan reads only a bounded tail, so a header buried behind megabytes of
// scrollback describes a finished run, not the current screen.
it('ignores a Cursor Agent screen buried beyond the payload scan tail limit', async () => {
const { connectPanePty } = await import('./pty-connection')
enableActiveRuntimeEnvironment()
const transport = createMockTransport('remote:env-1@@terminal-1')
const capturedReplayCallback: {
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
} = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedReplayCallback.current = callbacks.onReplayData ?? null
return { id: 'remote:env-1@@terminal-1', replay: '' }
})
transportFactoryQueue.push(transport)
setReattachPaneTitle('renamed shell')
const pane = createPane(1)
const textarea = {} as HTMLTextAreaElement
configureTerminalFocusMode(pane, textarea)
const manager = createManager(1)
const deps = createDeps()
const disposable = await withMockedDocumentActiveElement(textarea, async () => {
const connection = connectPanePty(pane as never, manager as never, deps as never)
await flushAsyncTicks(6)
capturedReplayCallback.current?.(
`${ANSI_POSITIONED_CURSOR_AGENT_REATTACH_SCREEN}${'shell scrollback\r\n'.repeat(20_000)}`
)
await flushAsyncTicks(12)
expect(transport.sendInput).not.toHaveBeenCalledWith('\x1b[I')
return connection
})
disposable.dispose()
})
it('downgrades a scrollback-only Cursor Agent signal when the parsed viewport shows a shell', async () => {
const { connectPanePty } = await import('./pty-connection')
enableActiveRuntimeEnvironment()

View File

@ -115,6 +115,7 @@ import {
queuePanePtyResizeIfHeld,
type PanePtyResizeHoldFlushDetail
} from '@/lib/pane-manager/pane-pty-resize-hold'
import { CSI_SEQUENCE_PATTERN } from '../../../../shared/ansi-escape-sequences'
import {
buildPostReplayLiveAgentReattachReset,
POST_REPLAY_LIVE_AGENT_SNAPSHOT_RESET,
@ -469,33 +470,22 @@ function terminalOwnsDomFocus(terminal: TerminalWithFocusMode): boolean {
return document.activeElement === terminal.textarea
}
function stripAnsiCsiSequences(data: string): string {
let normalized = ''
let index = 0
while (index < data.length) {
if (data.charCodeAt(index) === 0x1b && data[index + 1] === '[') {
index += 2
while (index < data.length) {
const code = data.charCodeAt(index)
index += 1
if (code >= 0x40 && code <= 0x7e) {
break
}
}
continue
}
normalized += data[index]
index += 1
}
return normalized
}
const CURSOR_AGENT_REATTACH_HEADER = 'Cursor Agent'
const CURSOR_AGENT_REATTACH_INPUT_MARKER = '→'
const CURSOR_AGENT_REATTACH_SCREEN_SIGNAL_MAX_CHARS = 5000
// Why bounded: reattach payloads reach multiple MB, but every replay puts the current screen last
// and only the header nearest the end matters. 256KB clears even a fully SGR-styled frame by ~2x,
// so the cut only ever drops stale scrollback — which would have been rejected anyway.
const CURSOR_AGENT_REATTACH_SCAN_TAIL_LIMIT_CHARS = 256 * 1024
function hasCursorAgentReattachPayloadScreenSignal(data: string): boolean {
const normalized = stripAnsiCsiSequences(data)
const tail =
data.length > CURSOR_AGENT_REATTACH_SCAN_TAIL_LIMIT_CHARS
? data.slice(-CURSOR_AGENT_REATTACH_SCAN_TAIL_LIMIT_CHARS)
: data
// Why CSI only: OSC-carried titles must keep counting as a header occurrence, as they did when
// this stripped CSI by hand.
const normalized = tail.replace(CSI_SEQUENCE_PATTERN, '')
// Why: anchor on the LAST header occurrence — replay buffers keep scrollback,
// and an earlier finished run must not classify the current screen.
const headerIndex = normalized.lastIndexOf(CURSOR_AGENT_REATTACH_HEADER)

View File

@ -6,6 +6,7 @@ import {
POST_REPLAY_LIVE_SNAPSHOT_RESET,
POST_REPLAY_MODE_RESET,
POST_REPLAY_REATTACH_RESET,
POST_REPLAY_REATTACH_RESET_KEEP_MOUSE,
RESET_MOUSE_REPORTING,
buildPostReplayLiveAgentReattachReset,
replayPayloadEndsWithCursorHidden
@ -33,6 +34,17 @@ describe('terminal mode reset profiles', () => {
expect(POST_REPLAY_REATTACH_RESET).not.toContain('\x1b[?2004l')
})
// Why ?1004l stays: #944 — a hard-killed TUI leaves the daemon emulator on the alternate buffer,
// so this profile can reach a plain shell, where armed focus reporting rings BEL on every pane
// switch. Dropping it would also make this byte-identical to the live-agent profile.
it('pins the live alternate-screen profile, which keeps mouse reporting but not focus', () => {
expect(POST_REPLAY_REATTACH_RESET_KEEP_MOUSE).toBe(
'\x1b[0 q\x1b[<99u\x1b[=0u\x1b[?25h\x1b[?1004l'
)
expect(POST_REPLAY_REATTACH_RESET_KEEP_MOUSE).not.toContain(RESET_MOUSE_REPORTING)
expect(POST_REPLAY_REATTACH_RESET_KEEP_MOUSE).not.toBe(POST_REPLAY_LIVE_AGENT_REATTACH_RESET)
})
// Why: #12101 — a cold-restored seed re-arms mouse reporting for a dead TUI.
it('disarms mouse reporting on the cold-restore seed', () => {
expect(COLD_RESTORE_SEED_MODE_RESET).toBe(RESET_MOUSE_REPORTING)