mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* perf(persistence): add pty-binding fast lane to skip redundant flushes Terminal pane reattachment currently clones the session and serializes the entire 9.2 MB app state even when the binding is already in place and durable. Add an early-return fast path that skips this work when all nine predicates hold: no split, binding matches in-memory and on-disk, incarnation matches, no tombstone, and generation counter proves durability. Includes one-line fix in `writeToDiskSync` to record hash-matched sync flushes as durable, so the fast path doesn't stay parked behind a stale generation. Adds `persistence.pty-binding` observability spans (local NDJSON, unsampled for mutations, budgeted for fast-lane hits) to measure eligibility rates before and after. Includes ratchet test to ensure every binding writer bumps the generation. Diagnostic tools and full investigation notes from September 7, 2026 capture that identified the 59–100 ms no-op binds and measured a real terminal keystroke queued 117 ms behind one such call. * perf(persistence): add pty-binding fast lane to skip redundant flushes Rapid rebinds of already-durable PTY bindings (e.g., remounting panes) were unnecessarily expensive because they cloned and flushed the entire document state every time. Detect when a binding hasn't changed since the last durable write and skip to return immediately, eliminating main-thread cost on that path. * perf(persistence): record binding.origin on the pty-binding span Fresh spawns always flush, so a fast-lane rate over all calls is diluted by however many terminals the user opened. Each caller knows whether it is a spawn, a reattach, a split, or a relay reattach; pass that through as metadata and record it so the reattach hit rate can be read from the trace file. Never branched on. * fix(persistence): keep the tab row on its first pane when a sibling pane binds A tab row names one PTY, but a split tab holds several panes. The renderer keeps the row on the first pane and refuses to let later split-pane spawns steal it, since a remount reattaches the tab to whatever the row says. Main overwrote it with whichever pane was binding, and the renderer's next publish put it back, so every sibling reattach was a state change and could never take the fast lane. On the real profile that is 38% of panes. Rewrite the row only when it names nothing useful: null, the PTY this leaf is replacing, or a PTY no leaf holds. The fast-lane predicate compares against the same rule. * perf(persistence): record durable pty-binding flushes per pane The global write generation is held back by any unrelated dirty state, causing bindings unchanged for minutes to appear unpersisted despite being on disk. Track per-pane durability to skip redundant flushes. * docs(persistence): describe the per-pane durability record The durability section still described the global generation check as the whole story and claimed there was no binding durability cache. Record the measurement that motivated the per-pane record, and why retiring one needs no cooperation from other binding writers. * docs(perf): consolidate every measured Orca performance issue into one register Folds the findings from all related debug sessions into the live lag investigation: the persistence/main-thread work (P1-P11), host contention (H1-H5), git and subprocess load on main (G1-G8), renderer and terminal rendering (R1-R8), the terminal daemon session leak from the deleted debug-orca-perf-issue worktree (D1-D9), and the Cmd-J palette review (C1-C6). Keeps the measurement behind each claim, records what is fixed versus open, and restates what the 117 ms keystroke delay still does not explain. * fix: address performance review findings * fix: satisfy diagnostic probe lint * chore: keep investigation artifacts out of performance PR * fix: run lag probe regression tests with Vitest * perf(persistence): replace pane receipts with global durability check * refactor(persistence): remove redundant binding review machinery * test(persistence): satisfy current assertion-free quality gate --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
185 lines
6.1 KiB
JavaScript
185 lines
6.1 KiB
JavaScript
import { mkdtemp, writeFile } from 'node:fs/promises'
|
||
import { tmpdir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
import {
|
||
startRendererTimingProbe,
|
||
stopRendererTimingProbe
|
||
} from './idle-cpu-renderer-timing-probe.mjs'
|
||
|
||
// Called with an already-attached main Orca page; never launches, focuses, or reloads it.
|
||
export async function captureLiveInputLag(page, durationMs = 30_000) {
|
||
if (!Number.isFinite(durationMs) || durationMs < 1_000 || durationMs > 60_000) {
|
||
throw new Error('Capture duration must be 1–60 seconds')
|
||
}
|
||
const identity = await page.evaluate(async () => {
|
||
if (!window.api?.app?.getIdentity) {
|
||
throw new Error('Target is not the main Orca renderer')
|
||
}
|
||
if (window.__orcaLiveInputLag || window.__orcaIdleCpuTimingProbe) {
|
||
throw new Error('A renderer timing probe already exists; stop it before capturing')
|
||
}
|
||
return window.api.app.getIdentity()
|
||
})
|
||
const directory = await mkdtemp(join(tmpdir(), 'orca-input-lag-'))
|
||
const cdp = await page.context().newCDPSession(page)
|
||
let timingStarted = false
|
||
let inputStarted = false
|
||
let profilingStarted = false
|
||
try {
|
||
await startRendererTimingProbe(page)
|
||
timingStarted = true
|
||
await page.evaluate(() => {
|
||
const events = []
|
||
const frames = []
|
||
const observers = []
|
||
const maxEntries = 3_000
|
||
let dropped = 0
|
||
const retain = (list, value) => {
|
||
if (list.length < maxEntries) {
|
||
list.push(value)
|
||
} else {
|
||
dropped++
|
||
}
|
||
}
|
||
const surface = (target) => {
|
||
if (!(target instanceof Element)) {
|
||
return 'other'
|
||
}
|
||
if (target.closest('.xterm')) {
|
||
return 'terminal'
|
||
}
|
||
if (target.closest('.monaco-editor')) {
|
||
return 'editor'
|
||
}
|
||
if (target.closest('[contenteditable="true"]')) {
|
||
return 'contenteditable'
|
||
}
|
||
return target.matches('input, textarea') ? 'text-input' : 'other'
|
||
}
|
||
const onInput = (event) => {
|
||
retain(events, {
|
||
kind: 'listener',
|
||
type: event.type,
|
||
surface: surface(event.target),
|
||
eventAt: event.timeStamp,
|
||
handlerAt: performance.now(),
|
||
trusted: event.isTrusted
|
||
})
|
||
}
|
||
const types = ['keydown', 'beforeinput', 'input', 'compositionstart', 'compositionend']
|
||
for (const type of types) {
|
||
document.addEventListener(type, onInput, true)
|
||
}
|
||
const supported = PerformanceObserver.supportedEntryTypes ?? []
|
||
if (supported.includes('event')) {
|
||
const observer = new PerformanceObserver((list) => {
|
||
for (const event of list.getEntries()) {
|
||
if (!types.includes(event.name)) {
|
||
continue
|
||
}
|
||
retain(events, {
|
||
kind: 'event-timing',
|
||
type: event.name,
|
||
surface: surface(event.target),
|
||
eventAt: event.startTime,
|
||
processingStart: event.processingStart,
|
||
processingEnd: event.processingEnd,
|
||
duration: event.duration,
|
||
interactionId: event.interactionId
|
||
})
|
||
}
|
||
})
|
||
observer.observe({ type: 'event', durationThreshold: 16 })
|
||
observers.push(observer)
|
||
}
|
||
let last = performance.now()
|
||
let frameId
|
||
const frame = (now) => {
|
||
if (now - last > 32) {
|
||
retain(frames, { at: now, gapMs: now - last })
|
||
}
|
||
last = now
|
||
frameId = requestAnimationFrame(frame)
|
||
}
|
||
frameId = requestAnimationFrame(frame)
|
||
const startedAt = performance.now()
|
||
const startedAtIso = new Date().toISOString()
|
||
window.__orcaLiveInputLag = {
|
||
stop: () => {
|
||
cancelAnimationFrame(frameId)
|
||
for (const type of types) {
|
||
document.removeEventListener(type, onInput, true)
|
||
}
|
||
for (const observer of observers) {
|
||
observer.disconnect()
|
||
}
|
||
delete window.__orcaLiveInputLag
|
||
return {
|
||
startedAt,
|
||
startedAtIso,
|
||
endedAt: performance.now(),
|
||
visibility: document.visibilityState,
|
||
events,
|
||
frames,
|
||
dropped,
|
||
eventTimingSupported: supported.includes('event')
|
||
}
|
||
}
|
||
}
|
||
})
|
||
inputStarted = true
|
||
await cdp.send('Profiler.enable')
|
||
const profileStartWindow = [await page.evaluate(() => performance.now())]
|
||
await cdp.send('Profiler.start')
|
||
profilingStarted = true
|
||
profileStartWindow.push(await page.evaluate(() => performance.now()))
|
||
await new Promise((resolve) => setTimeout(resolve, durationMs))
|
||
const { profile } = await cdp.send('Profiler.stop')
|
||
profilingStarted = false
|
||
const input = await page.evaluate(() => window.__orcaLiveInputLag.stop())
|
||
inputStarted = false
|
||
const timing = await stopRendererTimingProbe(page)
|
||
await page.evaluate(() => {
|
||
delete window.__orcaIdleCpuTimingProbe
|
||
})
|
||
timingStarted = false
|
||
await writeFile(join(directory, 'renderer.cpuprofile'), JSON.stringify(profile), {
|
||
mode: 0o600
|
||
})
|
||
await writeFile(
|
||
join(directory, 'input-timing.json'),
|
||
JSON.stringify(
|
||
{
|
||
identity,
|
||
input,
|
||
timing,
|
||
profileStartWindow,
|
||
limitation:
|
||
'Keyboard dispatch, handlers and frames only; not PTY echo latency. Event Timing omits short events and rounds durations.'
|
||
},
|
||
null,
|
||
2
|
||
),
|
||
{ mode: 0o600 }
|
||
)
|
||
return { directory, eventRecords: input.events.length, slowFrames: input.frames.length, timing }
|
||
} finally {
|
||
if (profilingStarted) {
|
||
await cdp.send('Profiler.stop').catch(() => {})
|
||
}
|
||
if (inputStarted) {
|
||
await page.evaluate(() => window.__orcaLiveInputLag?.stop()).catch(() => {})
|
||
}
|
||
if (timingStarted) {
|
||
await stopRendererTimingProbe(page).catch(() => {})
|
||
await page
|
||
.evaluate(() => {
|
||
delete window.__orcaIdleCpuTimingProbe
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
await cdp.send('Profiler.disable').catch(() => {})
|
||
await cdp.detach().catch(() => {})
|
||
}
|
||
}
|