Files
orca/tests/e2e/sustained-agent-typing-load-scripts.ts
T
Jinwoo-HandOrca d0b067e7f0 perf(daemon): keep-tail thin hidden panes' stream so agent floods never bury typing (STA multi-workspace lag)
Hidden panes are exempt from pendingData flow control (main gate-drops
their bytes after ingestion), so N background agents ran unbounded ahead
on the one shared daemon->main stream socket (measured 192MB user-space
backlog) and visible-pane echo waited FIFO behind it — typing appeared
seconds late whenever several agents burst on a loaded machine
(8x512KB/s + 12 CPU spinners: p50 293ms fix-off; 12x1MB/s: 6.1s).

Mechanism (replaces producer pacing — no reveal catch-up, ever):
- Shallow socket write gate (128KB) + per-session fairness bypass bounds
  echo latency by construction; kernel-flush refill sentinel keeps held
  bulk draining at full speed (drain-only refill capped at ~8MB/s).
- Backgrounded sessions' queued output is keep-tail dropped (newest
  512KB kept, in-order dataGap replaces the middle); a ~2MB GLOBAL
  budget shrinks per-session keep-tails (floor 64KB) so a worktree
  switch never waits behind the aggregate. Reply-eliciting query bytes
  (DSR/DA/OSC probes) are salvaged from dropped spans.
- Notifications are structurally lossless: the daemon runs the same
  shared scanners main uses (bell/OSC 133/pr-link/2031) over every byte
  BEFORE drop decisions and relays facts in byte order; ordered
  background markers hand scan authority back and forth, seeded with the
  emulator's partial escape tail so a sequence split across the handoff
  neither phantom-fires nor goes missing. Titles/agent-status stay
  main-side (kept-tail convergent).
- Main: background = hidden AND no remote view subscriber (a live
  mobile/web view is never thinned); on dataGap main resets cross-chunk
  parse carries, drops the headless mobile mirror, and reuses the
  hidden-drop model-restore marker.

Wire: three new stream events, tolerated within protocol v19 (old mains
ignore unknown events; old daemons never see the trigger). Kill
switches: ORCA_DAEMON_BACKGROUND_STREAM_DROP=0,
ORCA_DAEMON_SHALLOW_SOCKET_GATE=0.

A/B (pnpm bench:multi-workspace-typing): 8x512KB/s + 12 CPU workers
p50 293ms/p90 647ms -> 15/21ms (= baseline); 12x1MB/s 6,146ms -> 20ms;
light loads unchanged; zero missing echoes. Latin hidden-restore e2e
green (probe-verified aggregate-drain root cause). New deterministic
repro harness: tests/e2e/terminal-multi-workspace-typing-latency.spec.ts
+ CPU pressure workers.

Co-authored-by: Orca <help@stably.ai>
2026-07-07 15:41:05 -04:00

136 lines
4.7 KiB
TypeScript

/**
* Script generators for the multi-workspace sustained typing-latency bench
* (terminal-multi-workspace-typing-latency.spec.ts):
*
* - a paced agent-TUI load generator that replays the deterministic pipeline
* bench fixture through a real PTY at a fixed byte rate, emulating a Claude
* Code-style agent streaming in another workspace, and
* - a typing echo probe that timestamps each keystroke's arrival at the pty
* into a sidecar JSONL, so a key's total latency decomposes into
* input-half (CDP keydown -> pty stdin) and echo-half (pty echo -> screen).
*/
import { mkdirSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
// Why absolute file URL: the generated .mjs scripts run with the disposable
// test repo as cwd, so the fixture builder must be imported by absolute
// specifier (file URL keeps Windows drive-letter paths importable).
const PIPELINE_BENCH_URL = pathToFileURL(
path.resolve(__dirname, '..', '..', 'tools', 'benchmarks', 'terminal-pipeline-bench.mjs')
).href
export function sustainedLoadReadyFilePath(
directory: string,
runId: string,
paneIndex: number
): string {
return path.join(directory, `.orca-mwt-load-ready-${runId}-${paneIndex}`)
}
export function typingProbeReadyMarker(runId: string): string {
return `MWT_TYPING_READY_${runId}`
}
export function typingKeyMarkerPrefix(runId: string): string {
return `MWT_KEY_${runId}_`
}
function sustainedAgentLoadScript(runId: string, readyFileDirectory: string): string {
return `
import { writeFileSync } from 'node:fs'
import { buildFixture } from ${JSON.stringify(PIPELINE_BENCH_URL)}
const paneIndex = Number(process.argv[2] ?? 0)
const rateKbps = Number(process.argv[3] ?? 256)
const durationS = Number(process.argv[4] ?? 60)
const cols = process.stdout.columns ?? 80
const rows = process.stdout.rows ?? 24
// 2MB of deterministic Claude-Code-shaped frames, replayed in a loop.
const fixture = buildFixture('agent-tui', 2 * 1024 * 1024, cols, rows)
const TICK_MS = 50
const chunkChars = Math.max(1, Math.floor((rateKbps * 1024 * TICK_MS) / 1000))
const writeChunk = (data) =>
new Promise((resolve) => {
if (process.stdout.write(data)) {
resolve()
} else {
process.stdout.once('drain', resolve)
}
})
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// Readiness signals via the filesystem, not the terminal buffer: with many
// panes the stream scrolls a READY marker out of the buffer's serialize
// window before the spec's sequential checks reach it.
writeFileSync(
${JSON.stringify(readyFileDirectory)} + '/.orca-mwt-load-ready-${runId}-' + paneIndex,
String(Date.now())
)
process.stdout.write('${'MWT_LOAD_READY_'}${runId}_' + paneIndex + '\\r\\n')
const deadline = Date.now() + durationS * 1000
let offset = 0
while (Date.now() < deadline) {
await writeChunk(fixture.slice(offset, offset + chunkChars))
offset = (offset + chunkChars) % fixture.length
await sleep(TICK_MS)
}
process.stdout.write('\\x1b[0m\\x1b[?2026l\\r\\nMWT_LOAD_DONE_${runId}_' + paneIndex + '\\r\\n')
`
}
function typingEchoProbeScript(runId: string, arrivalSidecarPath: string): string {
return `
import { appendFileSync } from 'node:fs'
process.stdin.setEncoding('utf8')
if (process.stdin.isTTY) process.stdin.setRawMode(true)
process.stdin.resume()
let seq = 0
const interrupt = String.fromCharCode(3)
process.stdout.write('${'MWT_TYPING_READY_'}${runId}\\r\\n')
process.stdin.on('data', (chunk) => {
// One arrival timestamp per chunk: coalesced keystrokes genuinely arrive
// together at the pty, and that coalescing is part of what we measure.
const atMs = Date.now()
if (chunk.includes(interrupt)) {
process.exit(0)
}
for (const char of chunk) {
if (char === '\\r' || char === '\\n') continue
seq += 1
appendFileSync(
${JSON.stringify(arrivalSidecarPath)},
JSON.stringify({ seq, atMs }) + '\\n'
)
process.stdout.write('\\r\\x1b[2Kmwt prompt ' + seq + ': ' + char + ' ${'MWT_KEY_'}${runId}_' + seq + '\\r\\n')
}
})
`
}
export function writeSustainedAgentLoadScript(
scriptPath: string,
runId: string,
readyFileDirectory: string
): void {
mkdirSync(path.dirname(scriptPath), { recursive: true })
// Generated scripts concatenate with '/', which Node's fs accepts on all
// platforms; normalize Windows backslashes out of the baked-in directory.
writeFileSync(
scriptPath,
sustainedAgentLoadScript(runId, readyFileDirectory.replaceAll('\\', '/'))
)
}
export function writeTypingEchoProbeScript(
scriptPath: string,
runId: string,
arrivalSidecarPath: string
): void {
mkdirSync(path.dirname(scriptPath), { recursive: true })
writeFileSync(scriptPath, typingEchoProbeScript(runId, arrivalSidecarPath))
}