Performance and resource-efficiency audit
Performance index · Reproduction playbook
Executive assessment
Rozi is already well optimized for normal interactive use:
- Rendering and layout remain below 0.4 ms at 16 panes.
- Terminal parsing sustains roughly 16–97 MiB/s depending on viewport and escape density.
- Output bursts are coalesced effectively: 128 small messages cost 0.42 ms with lazy snapshot rebuilding versus 15.5 ms with eager rebuilding.
- Protocol framing reaches several GiB/s and is not a bottleneck.
- Transport queues, scrollback, semantic marks, images, and slow-client backlogs are bounded.
- Inactive panes avoid redraws, while background sessions deliberately continue parsing for instant switching.
There are no release-blocking performance problems. The main confirmed concerns are:
- “All panes” scrollback search can block the UI for 68–81 ms at 16 panes × 5,000 lines.
- Idle polling consumes about 0.5–0.7% of one core in the client and 0.8–1.0% in each attached server.
- Workbar command pollers survive config removal and can accumulate across reloads.
The principal scalability limit is retained terminal state: 8 large, full-history panes consumed about 265 MiB application PSS with one client and 372 MiB with two.
Verdict direction: suitable for release, with targeted non-blocking improvements worth considering.
Scope and environment
Measured revision: c07b6bef0bdc9527dc368aaef43ed1b8596e0bed, including the current uncommitted worktree. The render benchmarks were rerun after the latest observed view/sidebar edits.
Environment:
| Item | Value |
|---|---|
| OS | Arch Linux, kernel 7.1.4 |
| CPU | AMD Ryzen 7 5700X3D, 8 cores / 16 threads |
| Rust | rustc 1.93.0 |
| Release profile | optimized, thin LTO, one codegen unit, stripped |
| Release binary | 19,112,248 bytes / 18.23 MiB |
| Debug binary | 353,672,800 bytes / 337.3 MiB |
Sampling profiling was unavailable: Samply was installed, but perf_event_paranoid=2 denied access. No system setting was changed. Valgrind, heaptrack, perf, pidstat, smem, and hyperfine were unavailable, so allocation counts and sampled call-stack percentages could not be collected.
Architecture and hot paths
Rozi uses an always-server architecture:
- A server owns PTYs and parses every pane into a server-side
TerminalScreen. - Raw pane bytes are framed once and broadcast using a shared
Arc<[u8]>. - Each attached client parses the bytes into its own screen.
- The root
AppRootprocesses messages and uses full, layout-only, paint-only, or no-update rendering. - Background attachments stay connected and continue parsing, but do not render.
Performance-sensitive paths:
- PTY output:
SessionServer::handle_event→ broadcast →InboundMailbox→update::session::output→TerminalPane::process_server_output. - Rendering:
view::render, terminal snapshot rebuild, framework paint/diff. - Search:
ops::search::recompute_search→TerminalPane::search_scrollback. - Session server loop:
SessionServer::run_listener. - Runtime metadata:
SessionServer::poll_pane_runtime. - Attach/replay:
enqueue_attach_seedsandTerminalScreen::export_replay_bytes. - Resurrection: synchronous
SessionServer::write_snapshot.
Baseline results
CPU and latency
| Command/workload | Mode | Size | Result |
|---|---|---|---|
cargo bench --bench app_render -- 'app_render/view_layout/(8|16)|sidebar_render' | release | 8 panes, 200×60 | 221.45 µs view/layout |
| Same | release | 16 panes, 200×60 | 385.64 µs |
| Same, Agents sidebar | release | 8 panes, 200×60 | 396.47 µs |
cargo bench --bench snapshot_rebuild | release | 200×60 | 377.64 µs |
| Same | release | 320×90 | 892.11 µs |
| Output burst, 128×64-byte messages | release | one pane | 415.63 µs lazy versus 15.47 ms eager |
| Temporary search diagnostic | release | 1 pane × 5,000 lines, 250×60 | 4.0–4.8 ms |
| Same | release | 8 panes | 34.7–41.0 ms |
| Same | release | 16 panes | 68.3–81.0 ms |
| Temporary picker diagnostic | release | 10,000 entries | 3.17 ms |
| Same search diagnostic | debug | 16 panes | 1.68–1.83 seconds |
Isolated /proc/<pid>/stat sample, 6 s | release | idle client | 0.50–0.67% of one core |
| Same | release | idle server | 0.83–1.00% |
| Scripted detach, five runs | release | 250×60 | 92–93 ms |
The pseudo-terminal startup harness reached an input-ready pane in 791–793 ms, but this is not reliable startup evidence: the non-responsive pseudo-terminal likely forced the host-color query timeout. It is excluded from findings.
Throughput
From cargo bench:
| Operation | Case | Throughput |
|---|---|---|
| Terminal ingest, plain | 200×60 | 23.0 MiB/s |
| Terminal ingest, SGR-heavy | 200×60 | 27.5 MiB/s |
| Terminal ingest, scroll regions | 200×60 | 58.9 MiB/s |
| Terminal ingest, wide Unicode | 200×60 | 48.2 MiB/s |
| Terminal ingest, 1 MiB sparse lines | 200×60 | 69.9 MiB/s |
| Full in-memory session pipeline | 4 KiB | 23.9 MiB/s |
| Full in-memory session pipeline | 64 KiB | 94.6 MiB/s |
| Unix socket pipeline | 4 KiB | 22.0 MiB/s |
| Pane frame round trip | 4 KiB | 7.39 GiB/s |
| Pane frame round trip | 1 MiB | 5.22 GiB/s |
Protocol framing is orders of magnitude faster than parsing/rendering and does not justify optimization.
Memory
Commands:
tools/memory-matrix.sh --case 60 250 1 1 plain 1
tools/memory-matrix.sh --case 60 250 1 5000 styled 1
tools/memory-matrix.sh --case 60 250 8 5000 styled 1
tools/memory-matrix.sh --case 60 250 8 5000 styled 2Median of five PSS samples after a two-second settle:
| Workload | Client PSS | Server PSS | Application PSS | Child PSS |
|---|---|---|---|---|
| 1 pane, minimal history | 15.0 MiB | 7.9 MiB | 22.9 MiB | 1.1 MiB |
| 1 pane, 5,000 styled lines | 49.7 MiB | 39.8 MiB | 89.4 MiB | 1.1 MiB |
| 8 panes, 5,000 styled lines | 136.2 MiB | 128.6 MiB | 264.8 MiB | 5.0 MiB |
| 8 panes, two clients | 245.8 MiB | 125.8 MiB | 371.6 MiB | 5.0 MiB |
Child processes are explicitly excluded from application PSS.
Idle resources:
- Client: 18 threads, 24 file descriptors.
- Server with one pane: 3 threads, 18 descriptors.
- Eight panes and one client: 35 application threads.
- Eight panes and two clients: 52 application threads.
These threads are predominantly blocking transport, PTY, watcher, and command-worker threads; thread count scales, but idle CPU does not scale proportionally.
Confirmed findings
Medium: all-pane scrollback search blocks the UI
Affected workload: large scrollback, especially workspace/all-pane scope.
Evidence:
- One pane: 4–5 ms.
- Eight panes: 35–41 ms.
- Sixteen panes: 68–81 ms.
- Debug builds exceed 1.8 seconds at the largest case.
Relevant code:
src/ops/search.rs:recompute_searchsrc/pane.rs:TerminalPane::search_scrollbacksrc/view/overlays/search.rs: per-render construction of result entries
Root cause: every query synchronously exports all lines, allocates Strings, lowercases every line, finds every match, and retains cloned matching text. Cost is linear in total retained text and match count.
Impact: visible input lag only at high pane/history scope; focused-pane search is acceptable.
Recommendation: add a retained Criterion benchmark first. If 16-pane all-scope search is expected usage, add a streaming grid-search API to TerminalScreen, avoid materializing all lines, and support cancellation/latest-query semantics.
Complexity: medium to high.
Trade-offs: caching lowercase text would reduce CPU but duplicate significant scrollback memory; it is not recommended without measurement.
Verification: require p95 below 16 ms for the chosen supported scale, with sparse and dense matches.
Medium: optional workbar command tasks are not cancellable
Relevant code:
src/pane_lifecycle.rs:spawn_workbar_command_pollerssrc/ops/config.rs:reload_configsrc/update/overlays.rs:workbar_command_output
Evidence: each command owns a permanent loop. Removing it from config does not stop the thread; changing its interval does not update the existing thread; adding unique commands across reloads accumulates threads. Command::output() also has no timeout or output bound.
Impact: normally zero because workbar commands are optional. Repeated live reloads can leave obsolete commands executing indefinitely or retain a hung command thread.
Recommendation: use a generation/cancellation token and the existing bounded shell-command helper. Remove stale output and running-state entries during config reconciliation.
Complexity: medium.
Expected improvement: eliminates orphan threads/process launches; no meaningful common-path cost.
Verification: reload from command A to no command, confirm thread/process count stabilizes and A never runs again.
Low: idle polling has a measurable fixed cost
Evidence:
- Client: 0.50–0.67% of one core.
- Server: 0.83–1.00% per attached server.
Relevant code:
src/session/server/mod.rs:SessionServer::run_listenersleeps 1 ms whenever a client exists.tui-lipan/src/app/runner/animation_ticker.rs: focused terminals force a 16 ms wake interval.
The client interval supports 60 Hz output responsiveness; the server loop polls nonblocking IPC, PTY events, heartbeats, and queues.
Impact: small for one session, but several retained background sessions each keep a server in the 1 ms attached-client loop.
Recommendation: investigate readiness-based wakeups or adaptive server backoff. Preserve bounded input latency and output fairness.
Complexity: high for readiness polling, low/medium for adaptive backoff.
Verification: compare idle CPU and p50/p95 input latency with 1, 5, and 10 parked sessions.
Informational: memory is bounded but intentionally multiplied
The server and every client hold independent terminal parsers and scrollback. Memory therefore scales roughly with:
panes × retained cells × (server + attached clients)The measurements show the consequence, but not a leak. Reducing duplicate parsing would require a substantial protocol/state architecture change and is not currently justified.
Users likely to notice the limit are those combining large viewports, 5,000-line histories, many panes, multiple clients, or many retained background sessions.
Things already done well
- Output uses
TerminalScreenHandleandUpdate::paint, avoiding whole-app view/layout work. - Snapshot rebuilding is lazy and burst-coalesced.
- Inactive workspaces do not redraw from output.
- Raw output is encoded once and shared across clients with
Arc<[u8]>. - Slow clients are disconnected rather than blocking broadcast.
- Client inbound/outbound and server outboxes are byte-bounded at 8 MiB.
- Initial replay has a separate 64 MiB cap.
- PTY ingress is bounded at 4 MiB and applies producer backpressure.
- Adjacent output coalesces up to 64 KiB.
- Protocol frames are limited to 8 MiB.
- Scrollback, semantic marks, image storage, placements, and stderr capture are bounded.
- Resize and layout commits are debounced.
- Process-table scanning is shared across panes and agent detection is change-gated.
- Clock, agent-duration, theme, and sidebar refresh ticks avoid unchanged redraws.
- Framework input, resize, drag, and scroll events are coalesced.
- Locks are short and localized; the application is thread-based rather than async, so there are no locks held across
.await. - Release settings are already sensible. Fat LTO,
panic=abort, oropt-level=zshould remain opt-in size trade-offs.
Queue and backpressure summary
| Queue/buffer | Bound | Overflow behavior |
|---|---|---|
| Server PTY ingress | 4 MiB | producer blocks |
| Server client outbox | 8 MiB | slow client disconnected |
| Attach replay outbox | 64 MiB | client disconnected |
| Client inbound mailbox | 8 MiB | transport fails/reconnects |
| Client outbound queue | 8 MiB | transport fails |
| Output coalescing | 64 KiB | starts another entry |
| Protocol frame | 8 MiB | rejected |
| Orphan output | 256 KiB per pane/generation | oldest bytes dropped |
| Event subscriber | 128 messages | subscriber removed |
| Terminal images | 96 MiB decoded pixels per screen | LRU eviction |
Unverified risks
src/platform/ipc/piped.rshas an unboundedVecDeque<u8>before the bounded client mailbox. Test a remote producer faster than its client for several minutes and assert a memory plateau.SharedSessionState::orphan_outputis bounded per key but not globally; stale pane generations may remain. Simulate repeated follower-side spawn/output/kill races and inspect retained keys.- Resurrection snapshots synchronously export, write, and
fsyncevery pane in the server loop. Measure p99 input/output latency during a 16-pane, 5,000-line snapshot on slow storage. - The server may process up to 4 MiB of queued PTY data before servicing client input. Measure key round-trip latency during maximum output.
- Terminal images permit 96 MiB per parser, multiplied by panes and clients. Add an image-heavy memory matrix case.
- Background attachments have no count limit and continue parsing. Measure CPU/PSS with 1/5/10 active background sessions.
- Actual backend draw/diff time and frame-time percentiles were not measured; TestBackend capture allocates per cell and is unsuitable. Use interactive devtools metrics or Samply after lowering
perf_event_paranoid. - Long-duration leaks, reconnect churn, and memory after pane/session closure need a 1–24 hour soak. The attempted short cleanup run did not establish a valid populated-pane baseline, so no leak conclusion is made.
Prioritized optimization plan
Release-blocking
- None.
High-value
- Make workbar command pollers cancellable and bounded.
- Add a scrollback-search benchmark; optimize the streaming path if broad-scope search is expected to meet one-frame latency.
- Measure server fairness, then consider adaptive or readiness-driven idle waiting.
Useful but non-urgent
- Add remote-pipe and orphan-output global bounds.
- Instrument queue high-water marks and resurrection duration.
- Add configurable terminal-image budgets.
- Add background-session CPU/PSS and cleanup workloads.
Premature or unnecessary
- Optimizing protocol serialization.
- Compressing local output frames.
- Replacing shared
Arcbroadcast frames. - Memoizing terminal snapshots further.
- Splitting every pane into a child component solely for scoped rendering.
- Micro-optimizing clones without allocation/profile evidence.
- Changing the default release profile to fat LTO or size optimization.
Minimal regression suite
Keep the existing five Criterion targets and memory matrix. Add:
scrollback_search: 1/8/16 panes, 5,000 lines, 250×60, sparse and dense matches.server_fairness: input latency while PTY ingress remains saturated.background_sessions: CPU/PSS and switch latency for 1/5/10 sessions.resurrection_snapshot: snapshot duration by pane count and scrollback.remote_backpressure: bounded RSS with a stalled remote consumer.- Memory cleanup cases for pane close, client disconnect, session kill, and reconnect.
Timing benchmarks should remain opt-in on stable hardware; CI should compile them and run deterministic correctness/boundedness tests, not enforce noisy wall-clock thresholds.
No production files were modified by this audit; temporary diagnostics were removed. Generated memory results remain under target/perf-audit/.
Verdict: ready with minor improvements