Skip to content

perf: cut per-frame copies and idle polling in the frame pipeline - #23

Merged
marcrdgz merged 4 commits into
mainfrom
perf/frame-pipeline
Oct 19, 2026
Merged

marcrdgz merged 4 commits into
mainfrom
perf/frame-pipeline

Conversation

@marcrdgz

Copy link
Copy Markdown
Owner

Performance items 2.3–2.6 of the 0.8.0 plan: fewer per-frame copies in the daemon and client, cheaper link detection, and a quieter idle loop.

Changes

  • Daemon frames (2.3)
    • The daemon diffs directly against the pane cache. detach_buffer, which cloned the whole grid every frame, is gone.
    • The previous-frame buffer is updated by copying only the rows that were sent.
    • Each frame is built once per tick and shared by every client as an Arc<DaemonEvent>, instead of being deep-cloned per client.
    • WireCell.text is now CompactString (re-exported from kumo-protocol). Short text is stored inline, so cells no longer each cost a heap allocation.
  • Links (2.4)
    • The per-column OSC 8 lookup only runs when ghostty's per-row hyperlink flag is set (new Terminal::row_may_have_hyperlink).
    • The plain-text URL scan is skipped for rows without ://.
  • Client (2.5)
    • Grid::apply moves frame rows into the grid instead of cloning them.
    • The blit copies each row as one contiguous buffer slice.
    • Blitting only the touched rows is not possible, because ratatui clears its back buffer on every draw.
    • Removed the write-only Grid::dirty_rows.
  • Idle (2.6)
    • New ConfigFingerprint::refresh() re-reads a config file only when its size or modification time changed. Files modified in the last 3 s are always re-read, so a same-length rewrite on a filesystem with coarse timestamps is still caught.
    • Idle park goes from 10 → 50 ms. New connections are accepted on their own thread, which wakes the loop, so CLI/attach latency doesn't regress.
    • A client that is behind and waiting for a retry keeps the loop on the 2 ms cadence.
  • Hardened daemon_is_command_driven so it ignores a ConfigReloaded event. That event can fire when concurrent tests change the config env, and the failure was intermittent.

Wire protocol

Byte-identical, with a test comparing against the old String cell layout. The only change is in the Rust API: WireCell.text is a CompactString, so construct it with "x".into().

Benchmarks

Release builds, A/B against the pre-change base, 3 interleaved rounds on a loaded machine. The new benchmark is benchmark_pane_frame_pipeline_120x40.

before after
Frame work in daemon loop, full 120×40 190–260 µs 50–66 µs
Frame work in daemon loop, one-row diff 66–87 µs 24–33 µs
Link scans per full frame +75–100 µs +6–9 µs
Pane write → frame, end-to-end mean 592–671 µs 485–567 µs

Risks

  • Per-client encoding in the writer threads measured about 5–10% slower; this could be noise. Total CPU per frame is still lower.
  • Child exits and signals can now take up to 50 ms to be noticed (was 10 ms).
  • A config file rewritten after it settled, with the same size and the same modification time, is missed. A normal edit always changes the modification time.

Test plan

  • cargo build --workspace && cargo test --workspace && cargo clippy --workspace (run twice)
  • Manual: vim/htop, seq 1 100000, resizes and splits while output is scrolling, two viewers on one session, wide characters/emoji, OSC 8 and plain URLs with the link modifier held, selection, config hot reload, kumo session list latency, idle CPU

@marcrdgz
marcrdgz force-pushed the perf/frame-pipeline branch from f75c9be to c7805af Compare October 19, 2026 20:27
@marcrdgz marcrdgz self-assigned this Oct 19, 2026
@marcrdgz
marcrdgz merged commit a9da15a into main Oct 19, 2026
2 checks passed
@marcrdgz
marcrdgz deleted the perf/frame-pipeline branch October 19, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant