mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-16 04:22:36 +00:00
perf(tui): instrument stdout drain — rule out terminal parse bottleneck
Adds four fields to FrameEvent.phases and the matching profile
summary:
optimizedPatches post-optimize patch count (what's actually
written to stdout; the .patches field is
pre-optimize)
writeBytes UTF-8 byte count of the write this frame
backpressure true when Node's stdout.write returned false
(Writable buffer full — outer terminal can't
keep up)
prevFrameDrainMs end-to-end drain time of the PREVIOUS frame's
write, captured from stdout.write's 2-arg
callback. Reported on the next frame so the
measurement reflects "time until OS flushed
the bytes to the terminal fd", not "time until
queued in Node".
writeDiffToTerminal() now returns { bytes, backpressure } and
accepts an optional onDrain callback. Only attached on TTY with
diff; piped/non-TTY stdout bypasses flow control so the callback
would fire synchronously anyway.
Initial measurements under hold-wheel_up against 1106-msg session
(30Hz for 6s):
patches total 28,888
optimized total 16,700 (ratio 0.58 — optimizer cuts ~42%)
writeBytes 42 KB / 10s = 4.2 KB/s throughput
drainMs p50 0.14 ms terminal accepts bytes instantly
drainMs p99 0.85 ms
backpressure 0% of frames
This rules out the terminal-parse hypothesis — Cursor's xterm.js
drains our output in sub-millisecond time at only 4 KB/s. The
remaining lag has to be in the render pipeline, not the wire.
Profile output now includes the bytes+drain+backpressure lines to
keep this visible on every subsequent iteration.
This commit is contained in:
parent
d3dedf10aa
commit
f823535db2
6 changed files with 126 additions and 4 deletions
|
|
@ -165,6 +165,15 @@ export default class Ink {
|
|||
private backFrame: Frame
|
||||
private lastPoolResetTime = performance.now()
|
||||
private drainTimer: ReturnType<typeof setTimeout> | null = null
|
||||
// Write-drain telemetry: pendingWriteStart is the performance.now() of
|
||||
// the most recent stdout.write waiting for its drain callback. Set to
|
||||
// null when the callback fires (drained). Read on the NEXT frame and
|
||||
// reported as prevFrameDrainMs so the FrameEvent records how long the
|
||||
// previous write took to actually hit the terminal — distinguishes
|
||||
// "queued in Node" (write returned true) from "terminal accepted bytes"
|
||||
// (callback fired).
|
||||
private pendingWriteStart: number | null = null
|
||||
private lastDrainMs = 0
|
||||
private lastYogaCounters: {
|
||||
ms: number
|
||||
visited: number
|
||||
|
|
@ -970,7 +979,43 @@ export default class Ink {
|
|||
}
|
||||
|
||||
const tWrite = performance.now()
|
||||
writeDiffToTerminal(this.terminal, optimized, this.altScreenActive && !SYNC_OUTPUT_SUPPORTED)
|
||||
// Capture any stale pending write BEFORE starting this frame's write —
|
||||
// if the callback already fired, pendingWriteStart is null and lastDrainMs
|
||||
// already reflects the previous frame's drain. If it hasn't fired, we
|
||||
// report "still pending" via a non-zero duration based on now-then so
|
||||
// backpressure shows up even if Node never flushes this session.
|
||||
const staleDrain =
|
||||
this.pendingWriteStart !== null
|
||||
? performance.now() - this.pendingWriteStart
|
||||
: this.lastDrainMs
|
||||
|
||||
const prevFrameDrainMs = Math.round(staleDrain * 100) / 100
|
||||
this.lastDrainMs = 0
|
||||
|
||||
// Only track drain on TTY. Piped/non-TTY stdout bypasses flow control.
|
||||
const trackDrain = this.options.stdout.isTTY && hasDiff
|
||||
const drainStart = trackDrain ? tWrite : 0
|
||||
|
||||
if (trackDrain) {
|
||||
this.pendingWriteStart = drainStart
|
||||
}
|
||||
|
||||
const { bytes: writeBytes, backpressure } = writeDiffToTerminal(
|
||||
this.terminal,
|
||||
optimized,
|
||||
this.altScreenActive && !SYNC_OUTPUT_SUPPORTED,
|
||||
trackDrain
|
||||
? () => {
|
||||
// Callback fires once Node has flushed the chunk to the OS.
|
||||
// Capture the drain time and clear pending so the NEXT frame's
|
||||
// staleDrain = the real end-to-end flush time.
|
||||
if (this.pendingWriteStart === drainStart) {
|
||||
this.lastDrainMs = performance.now() - drainStart
|
||||
this.pendingWriteStart = null
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
const writeMs = performance.now() - tWrite
|
||||
|
||||
// Update blit safety for the NEXT frame. The frame just rendered
|
||||
|
|
@ -1008,6 +1053,10 @@ export default class Ink {
|
|||
optimize: optimizeMs,
|
||||
write: writeMs,
|
||||
patches: diff.length,
|
||||
optimizedPatches: optimized.length,
|
||||
writeBytes,
|
||||
backpressure,
|
||||
prevFrameDrainMs,
|
||||
yoga: yogaMs,
|
||||
commit: commitMs,
|
||||
yogaVisited: yc.visited,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue