Files
orca/tests/e2e/terminal-split-activation-latency-report.ts
Neil c558d7e083 Activate terminal splits before inherited CWD resolution (#17601)
* perf(terminal): activate splits before cwd resolution

* test(terminal): prove split focus before cwd publish

* fix(terminal): release stale split cwd fence

* test(terminal): add visible split activation latency benchmark

* docs(reliability): clarify split benchmark provenance

* fix: preserve deferred split handoffs across remounts

* fix: fence late deferred split closes

* docs(reliability): record exact split benchmark runs

* test(reliability): fail benchmark on artifact write errors

* test(reliability): attribute split activation phases

* docs(reliability): record schema-v2 split benchmark

* refactor(terminal): collapse duplicated split-handoff and write-queue paths

- Drop the discardDeferredSplitPaneHandoff alias for its identical clear twin.
- Fold the deferred-cwd resolve/reject settle handlers into one applier.
- Extract settlePaneCwdDeferredSpawn for the repeated read-clear-write pattern.
- Share one head-index FIFO primitive between the ordinary and reply queues.

* fix(terminal): stop retaining a promise reaction per acknowledged write

Racing every accepted write against one queue-lifetime cancel promise kept a
reaction record alive until that promise settled: 200k acknowledged writes
retained 88.6MB, now 0.1MB. Give each in-flight write its own cancel, and
split the shared FIFO primitive into its own module.

Also sanitize the split-latency benchmark report at its single serialization
point so shared artifacts no longer carry the machine-local repo path or
unbounded cleanup error text.

* fix(terminal): settle deferred split input when the spawn is abandoned

An abandoned deferred spawn returns before transport.connect(), so nothing
drained the pre-connect buffer: sendInputAccepted's promise never settled and
a paste into that pane hung forever. Clear the buffer on the abandon fence.

Also re-derive the pre-connect retention cap from the clipboard-paste ceiling
rather than the 16MB single-write ceiling; it is held twice per pane across up
to 64 deferred splits, so 5.59M code units guarded the wrong thing.

* fix(terminal): release the deferred cwd fence on a rejected reattach

A daemon createOrAttach can turn an apparent fresh spawn into a reattach; when
that reattach is refused the spawn ends with deferredSplitSpawn/pendingCwd
still set, permanently arming the pre-bind detach refusal. The release no-ops
when a PTY did bind, so it only fires where the fence would otherwise leak.

The stale-generation return above is deliberately left alone: a newer connect
already owns the pane there, and the fence is not generation-scoped.
2026-08-31 16:45:36 -07:00

207 lines
7.9 KiB
TypeScript

import { summarizeLatencies, type LatencyDistribution } from './codex-composer-echo-latency-probe'
import type { SplitLatencySample } from './terminal-split-activation-latency-phases'
export type BenchmarkRevisionIdentity = {
headSha: string
dirty: boolean
}
export type SampleSummary = {
counts: {
requested: number
attempted: number
success: number
missing: number
unattempted: number
missingEvents: {
keydown: number
focus: number
cwdRequest: number
cwdSettled: number
ptySpawnRequest: number
ptySpawnResult: number
ptyBind: number
fixtureUnlockRequest: number
fixtureUnlockIpcWrite: number
fixtureReadyParse: number
input: number
firstEcho: number
paneCount: number
ptyIdentity: number
ptyExit: number
cleanup: number
}
}
distributions: {
shortcutToFocusMs: LatencyDistribution
shortcutToCwdRequestMs: LatencyDistribution
cwdLookupMs: LatencyDistribution
cwdSettleToPtySpawnRequestMs: LatencyDistribution
ptySpawnRequestToResultMs: LatencyDistribution
ptySpawnResultToBindMs: LatencyDistribution
shortcutToPtyBindMs: LatencyDistribution
ptyBindToFixtureUnlockRequestMs: LatencyDistribution
fixtureUnlockRequestToIpcWriteMs: LatencyDistribution
fixtureUnlockIpcWriteToReadyParseMs: LatencyDistribution
fixtureReadyParseToInputMs: LatencyDistribution
shortcutToFirstEchoMs: LatencyDistribution
ptyBindToFirstEchoMs: LatencyDistribution
inputToFirstEchoMs: LatencyDistribution
}
}
export type BrowserWindowState = {
browserWindowVisible: boolean
windowCount: number
}
export type TerminalSplitLatencyReportConfig = {
warmupCycles: number
measuredCycles: number
maxMeasuredCycles: number
testTimeoutMs: number
splitChord: string
closeChord: string
sampleTimeoutMs: number
cleanupTimeoutMs: number
processCwdCacheExpiryWaitMs: number
}
export type BenchmarkReportResult = {
report: Record<string, unknown>
warmupSummary: SampleSummary
measuredSummary: SampleSummary
}
function valuesFor(
samples: SplitLatencySample[],
key: keyof SampleSummary['distributions']
): number[] {
return samples.flatMap((sample) => {
const value = sample[key]
return value === null ? [] : [value]
})
}
export function summarizeSamples(samples: SplitLatencySample[], requested: number): SampleSummary {
const missingEvents = {
keydown: samples.filter((sample) => sample.keydownAtMs === null).length,
focus: samples.filter((sample) => sample.focusAtMs === null).length,
cwdRequest: samples.filter((sample) => sample.cwdRequestAtMs === null).length,
cwdSettled: samples.filter((sample) => sample.cwdSettledAtMs === null).length,
ptySpawnRequest: samples.filter((sample) => sample.ptySpawnRequestAtMs === null).length,
ptySpawnResult: samples.filter((sample) => sample.ptySpawnResultAtMs === null).length,
ptyBind: samples.filter((sample) => sample.ptyBoundAtMs === null).length,
fixtureUnlockRequest: samples.filter((sample) => sample.fixtureUnlockRequestedAtMs === null)
.length,
fixtureUnlockIpcWrite: samples.filter((sample) => sample.fixtureUnlockIpcWriteAtMs === null)
.length,
fixtureReadyParse: samples.filter((sample) => sample.fixtureReadyParsedAtMs === null).length,
input: samples.filter((sample) => sample.inputAtMs === null).length,
firstEcho: samples.filter((sample) => sample.firstEchoAtMs === null).length,
paneCount: samples.filter((sample) => sample.paneCountAfterProbe !== 2).length,
ptyIdentity: samples.filter((sample) => sample.newPtyId === sample.sourcePtyId).length,
ptyExit: samples.filter((sample) => !sample.ptyExitObserved).length,
cleanup: samples.filter((sample) => sample.cleanupError !== null).length
}
const success = samples.filter((sample) => sample.success).length
return {
counts: {
requested,
attempted: samples.length,
success,
missing: samples.length - success,
unattempted: Math.max(0, requested - samples.length),
missingEvents
},
distributions: {
shortcutToFocusMs: summarizeLatencies(valuesFor(samples, 'shortcutToFocusMs')),
shortcutToCwdRequestMs: summarizeLatencies(valuesFor(samples, 'shortcutToCwdRequestMs')),
cwdLookupMs: summarizeLatencies(valuesFor(samples, 'cwdLookupMs')),
cwdSettleToPtySpawnRequestMs: summarizeLatencies(
valuesFor(samples, 'cwdSettleToPtySpawnRequestMs')
),
ptySpawnRequestToResultMs: summarizeLatencies(
valuesFor(samples, 'ptySpawnRequestToResultMs')
),
ptySpawnResultToBindMs: summarizeLatencies(valuesFor(samples, 'ptySpawnResultToBindMs')),
shortcutToPtyBindMs: summarizeLatencies(valuesFor(samples, 'shortcutToPtyBindMs')),
ptyBindToFixtureUnlockRequestMs: summarizeLatencies(
valuesFor(samples, 'ptyBindToFixtureUnlockRequestMs')
),
fixtureUnlockRequestToIpcWriteMs: summarizeLatencies(
valuesFor(samples, 'fixtureUnlockRequestToIpcWriteMs')
),
fixtureUnlockIpcWriteToReadyParseMs: summarizeLatencies(
valuesFor(samples, 'fixtureUnlockIpcWriteToReadyParseMs')
),
fixtureReadyParseToInputMs: summarizeLatencies(
valuesFor(samples, 'fixtureReadyParseToInputMs')
),
shortcutToFirstEchoMs: summarizeLatencies(valuesFor(samples, 'shortcutToFirstEchoMs')),
ptyBindToFirstEchoMs: summarizeLatencies(valuesFor(samples, 'ptyBindToFirstEchoMs')),
inputToFirstEchoMs: summarizeLatencies(valuesFor(samples, 'inputToFirstEchoMs'))
}
}
}
export function buildBenchmarkReport(args: {
label: string
revision: BenchmarkRevisionIdentity
headfulRun: boolean
windowState: BrowserWindowState
documentVisibility: string
testRepoPath: string
warmupSamples: SplitLatencySample[]
measuredSamples: SplitLatencySample[]
abortError: Error | null
config: TerminalSplitLatencyReportConfig
}): BenchmarkReportResult {
const warmupSummary = summarizeSamples(args.warmupSamples, args.config.warmupCycles)
const measuredSummary = summarizeSamples(args.measuredSamples, args.config.measuredCycles)
const runComplete =
warmupSummary.counts.success === args.config.warmupCycles &&
measuredSummary.counts.success === args.config.measuredCycles &&
args.abortError === null
const headlineMs = runComplete
? {
shortcutToFocusP50: measuredSummary.distributions.shortcutToFocusMs.p50,
shortcutToFocusP95: measuredSummary.distributions.shortcutToFocusMs.p95,
shortcutToFocusMax: measuredSummary.distributions.shortcutToFocusMs.max,
shortcutToPtyBindP50: measuredSummary.distributions.shortcutToPtyBindMs.p50,
shortcutToPtyBindP95: measuredSummary.distributions.shortcutToPtyBindMs.p95,
shortcutToPtyBindMax: measuredSummary.distributions.shortcutToPtyBindMs.max,
shortcutToFirstEchoP50: measuredSummary.distributions.shortcutToFirstEchoMs.p50,
shortcutToFirstEchoP95: measuredSummary.distributions.shortcutToFirstEchoMs.p95,
shortcutToFirstEchoMax: measuredSummary.distributions.shortcutToFirstEchoMs.max
}
: null
return {
report: {
schemaVersion: 2,
benchmark: 'terminal-split-activation-latency',
label: args.label,
revision: args.revision,
status: runComplete ? 'passed' : 'failed',
valid: runComplete,
abortReason: args.abortError?.message ?? null,
timestamp: new Date().toISOString(),
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
headful: args.headfulRun,
browserWindowVisible: args.windowState.browserWindowVisible,
documentVisibility: args.documentVisibility,
testRepoPath: args.testRepoPath,
config: args.config,
headlineMs,
warmupSummary,
measuredSummary,
warmupSamples: args.warmupSamples,
measuredSamples: args.measuredSamples
},
warmupSummary,
measuredSummary
}
}