Performance and resource-efficiency audit
Performance index · Reproduction playbook
Historical context: this report records the measured 2026-08-03 worktree. The current tree resolves
tui-lipanandrelswapfrom crates.io, so source and dependency details in this report are not current guidance. The measurements remain evidence for the recorded revision only.
Executive assessment
This dated audit found no performance problems in normal interactive use:
- Rendering and layout stayed below 0.4 ms at 16 panes.
- Terminal parsing sustained roughly 16–97 MiB/s depending on viewport and escape density.
- For 128 small messages, lazy snapshot rebuilding cost 0.42 ms versus 15.5 ms for eager rebuilding.
- Protocol framing reached several GiB/s and was not a bottleneck.
- Transport queues, scrollback, semantic marks, images, and slow-client backlogs had bounds.
- Inactive panes avoided redraws. Background sessions continued parsing for instant switching.
The audit found no release-blocking performance problems. It confirmed these concerns:
- "All panes" scrollback search blocked the UI for 68–81 ms at 16 panes × 5,000 lines.
- Idle polling consumed about 0.5–0.7% of one core in the client and 0.8–1.0% in each attached server.
- Workbar command pollers survived config removal and could accumulate across reloads.
Retained terminal state set the measured scalability limit. Eight large, full-history panes consumed about 265 MiB application PSS with one client and 372 MiB with two.
Verdict: suitable for release, with specific non-blocking improvements to consider.
Scope and environment
Measured revision: c07b6bef0bdc9527dc368aaef43ed1b8596e0bed, including the uncommitted audit 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, and perf_event_paranoid=2 denied access. No system setting was changed. Valgrind, heaptrack, perf, pidstat, smem, and hyperfine were unavailable. The audit therefore did not collect allocation counts or sampled call-stack percentages.
Architecture and hot paths
At the measured revision, Rozi used 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. They 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. This was not reliable startup evidence because the non-responsive pseudo-terminal appeared to force the host-color query timeout. The result is excluded from the 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 was much faster than parsing and rendering. The measurement did not justify optimizing it.
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 |
Application PSS excludes child processes.
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.
Most were blocking transport, PTY, watcher, and command-worker threads. Thread count scaled without a proportional increase in idle CPU.
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. It 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 appears 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 and duplicate significant scrollback memory. Measure both effects before adopting it.
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 running 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 and process launches with no measured 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. Each retained background session keeps 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: each parser adds bounded memory
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 cost of duplicate parsing. They do not show a leak. Reducing that duplication would require a large protocol and state architecture change, which the measurements did not justify.
The limit affects workloads that combine large viewports, 5,000-line histories, many panes, multiple clients, or many retained background sessions.
Existing efficient paths
- 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 before they can block 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 thread-based application holds no locks across
.await. - The release settings fit the measured workload. Fat LTO,
panic=abort, andopt-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 and has no global bound. 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. 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, 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 and boundedness tests. It should 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