Skip to content

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:

  1. “All panes” scrollback search can block the UI for 68–81 ms at 16 panes × 5,000 lines.
  2. Idle polling consumes about 0.5–0.7% of one core in the client and 0.8–1.0% in each attached server.
  3. 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:

ItemValue
OSArch Linux, kernel 7.1.4
CPUAMD Ryzen 7 5700X3D, 8 cores / 16 threads
Rustrustc 1.93.0
Release profileoptimized, thin LTO, one codegen unit, stripped
Release binary19,112,248 bytes / 18.23 MiB
Debug binary353,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:

  1. A server owns PTYs and parses every pane into a server-side TerminalScreen.
  2. Raw pane bytes are framed once and broadcast using a shared Arc<[u8]>.
  3. Each attached client parses the bytes into its own screen.
  4. The root AppRoot processes messages and uses full, layout-only, paint-only, or no-update rendering.
  5. Background attachments stay connected and continue parsing, but do not render.

Performance-sensitive paths:

  • PTY output: SessionServer::handle_event → broadcast → InboundMailboxupdate::session::outputTerminalPane::process_server_output.
  • Rendering: view::render, terminal snapshot rebuild, framework paint/diff.
  • Search: ops::search::recompute_searchTerminalPane::search_scrollback.
  • Session server loop: SessionServer::run_listener.
  • Runtime metadata: SessionServer::poll_pane_runtime.
  • Attach/replay: enqueue_attach_seeds and TerminalScreen::export_replay_bytes.
  • Resurrection: synchronous SessionServer::write_snapshot.

Baseline results

CPU and latency

Command/workloadModeSizeResult
cargo bench --bench app_render -- 'app_render/view_layout/(8|16)|sidebar_render'release8 panes, 200×60221.45 µs view/layout
Samerelease16 panes, 200×60385.64 µs
Same, Agents sidebarrelease8 panes, 200×60396.47 µs
cargo bench --bench snapshot_rebuildrelease200×60377.64 µs
Samerelease320×90892.11 µs
Output burst, 128×64-byte messagesreleaseone pane415.63 µs lazy versus 15.47 ms eager
Temporary search diagnosticrelease1 pane × 5,000 lines, 250×604.0–4.8 ms
Samerelease8 panes34.7–41.0 ms
Samerelease16 panes68.3–81.0 ms
Temporary picker diagnosticrelease10,000 entries3.17 ms
Same search diagnosticdebug16 panes1.68–1.83 seconds
Isolated /proc/<pid>/stat sample, 6 sreleaseidle client0.50–0.67% of one core
Samereleaseidle server0.83–1.00%
Scripted detach, five runsrelease250×6092–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:

OperationCaseThroughput
Terminal ingest, plain200×6023.0 MiB/s
Terminal ingest, SGR-heavy200×6027.5 MiB/s
Terminal ingest, scroll regions200×6058.9 MiB/s
Terminal ingest, wide Unicode200×6048.2 MiB/s
Terminal ingest, 1 MiB sparse lines200×6069.9 MiB/s
Full in-memory session pipeline4 KiB23.9 MiB/s
Full in-memory session pipeline64 KiB94.6 MiB/s
Unix socket pipeline4 KiB22.0 MiB/s
Pane frame round trip4 KiB7.39 GiB/s
Pane frame round trip1 MiB5.22 GiB/s

Protocol framing is orders of magnitude faster than parsing/rendering and does not justify optimization.

Memory

Commands:

bash
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 2

Median of five PSS samples after a two-second settle:

WorkloadClient PSSServer PSSApplication PSSChild PSS
1 pane, minimal history15.0 MiB7.9 MiB22.9 MiB1.1 MiB
1 pane, 5,000 styled lines49.7 MiB39.8 MiB89.4 MiB1.1 MiB
8 panes, 5,000 styled lines136.2 MiB128.6 MiB264.8 MiB5.0 MiB
8 panes, two clients245.8 MiB125.8 MiB371.6 MiB5.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_search
  • src/pane.rs: TerminalPane::search_scrollback
  • src/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_pollers
  • src/ops/config.rs: reload_config
  • src/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_listener sleeps 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:

text
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 TerminalScreenHandle and Update::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, or opt-level=z should remain opt-in size trade-offs.

Queue and backpressure summary

Queue/bufferBoundOverflow behavior
Server PTY ingress4 MiBproducer blocks
Server client outbox8 MiBslow client disconnected
Attach replay outbox64 MiBclient disconnected
Client inbound mailbox8 MiBtransport fails/reconnects
Client outbound queue8 MiBtransport fails
Output coalescing64 KiBstarts another entry
Protocol frame8 MiBrejected
Orphan output256 KiB per pane/generationoldest bytes dropped
Event subscriber128 messagessubscriber removed
Terminal images96 MiB decoded pixels per screenLRU eviction

Unverified risks

  1. src/platform/ipc/piped.rs has an unbounded VecDeque<u8> before the bounded client mailbox. Test a remote producer faster than its client for several minutes and assert a memory plateau.
  2. SharedSessionState::orphan_output is bounded per key but not globally; stale pane generations may remain. Simulate repeated follower-side spawn/output/kill races and inspect retained keys.
  3. Resurrection snapshots synchronously export, write, and fsync every pane in the server loop. Measure p99 input/output latency during a 16-pane, 5,000-line snapshot on slow storage.
  4. The server may process up to 4 MiB of queued PTY data before servicing client input. Measure key round-trip latency during maximum output.
  5. Terminal images permit 96 MiB per parser, multiplied by panes and clients. Add an image-heavy memory matrix case.
  6. Background attachments have no count limit and continue parsing. Measure CPU/PSS with 1/5/10 active background sessions.
  7. 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.
  8. 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

  1. Release-blocking

    • None.
  2. 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.
  3. 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.
  4. Premature or unnecessary

    • Optimizing protocol serialization.
    • Compressing local output frames.
    • Replacing shared Arc broadcast 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

MIT OR Apache-2.0