Skip to content

perf(state): batch tmux metadata in collect_state — ~13× faster /api/state - #118

Merged
tdody merged 2 commits into
mainfrom
perf/batched-collect-state
Jun 26, 2026
Merged

perf(state): batch tmux metadata in collect_state — ~13× faster /api/state#118
tdody merged 2 commits into
mainfrom
perf/batched-collect-state

Conversation

@tdody

@tdody tdody commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the real cause of the "app gets laggy over time" reports: /api/state was slow, and the dashboard polls it every 500 ms while a terminal modal is open.

collect_state() iterated libtmux objects — srv.sessions, s.windows, w.active_pane, pane.pane_current_command, pane.pane_current_path — and libtmux forks a separate tmux subprocess for each lazy property access (~150 ms per window). At the modal-open cadence that spawned dozens of tmux subprocesses per second, flooding the tmux server so the user's keystrokes — each a tmux send-keysqueued behind the storm. Result: laggy typing + slow modal opens that scaled with pane count and got dramatically worse under system memory pressure (subprocess forks stall on paging → multi-second /api/state spikes).

Change

Replace the per-window/per-pane libtmux fan-out with two batched tmux commands:

  • tmux list-panes -a -f '#{pane_active}' -F <fmt> — all active-pane metadata (session + window + pane) in one subprocess.
  • tmux list-clients -F <fmt> — all clients, grouped onto their session.

Parsed by pure, unit-tested helpers (_parse_pane_rows, _parse_clients_grouped); sessions/windows are derived from the rows. capture-pane stays per-pane (already cached, THI-181 — now keyed by session:window target). The StateResponse schema is unchanged, so the frontend is untouched.

Measured (live, 6 panes)

median max
before 380 ms 810 ms (3.4 s under memory pressure)
after 29 ms 84 ms

~13× faster, and the multi-second spikes are gone. The win grows with pane count (old cost was linear in windows; new cost is ~flat).

Tests (TDD)

  • New pure-parser tests (_parse_pane_rows, _parse_clients_grouped): delimited-field parsing, blank/short-row skipping, spaces in names/paths, client grouping. Watched them fail (helpers didn't exist), then pass.
  • Rewrote the collect_state tests to drive the new batched srv.cmd data source — covering sb-usage filtering, one-card-per-session dedup, capture-pane TTL cache (hit within TTL / re-capture after), client population/attached, malformed-row skip, and graceful degradation when list-panes errors mid-scan (no 500).
  • Full backend suite: 393 passing; ruff (lint + format) + ty clean.

Notes

🤖 Generated with Claude Code

…rocesses)

/api/state's collect_state iterated libtmux objects — srv.sessions,
s.windows, w.active_pane, pane.pane_current_command/path — and libtmux
forks a separate `tmux` subprocess for each lazy property access
(~150ms per window). At the 500ms modal-open poll cadence this spawned
dozens of tmux subprocesses per second, flooding the tmux server so the
user's keystrokes (each a `tmux send-keys`) queued behind it — laggy
typing and slow modal opens that scaled with pane count and were
amplified under system memory pressure.

Replace the per-window/per-pane libtmux fan-out with two batched
commands — one `tmux list-panes -a -f '#{pane_active}' -F <fmt>` for all
active-pane metadata and one `list-clients -F <fmt>` — parsed by pure
helpers (_parse_pane_rows / _parse_clients_grouped). Sessions and windows
are derived from the rows; capture-pane stays per-pane (already cached,
THI-181, now keyed by session:window target).

Measured live (6 panes): /api/state median 380ms → 29ms, max 810ms →
84ms (~13x); eliminates the multi-second spikes seen under load. Response
schema is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tdody
tdody merged commit a9f76e2 into main Jun 26, 2026
3 checks passed
@tdody
tdody deleted the perf/batched-collect-state branch June 26, 2026 00:07
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