Skip to content

Wait for a window's panes concurrently to speed up loads#1056

Open
tony wants to merge 1 commit into
masterfrom
tmuxp-lag
Open

Wait for a window's panes concurrently to speed up loads#1056
tony wants to merge 1 commit into
masterfrom
tmuxp-lag

Conversation

@tony

@tony tony commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

  • Fix slow workspace loads (Very slow performance opening layouts/workspaces #1053): the per-pane readiness wait added in fix(workspace/builder): Wait for shell prompt before layout and commands #1018 ran serially, so a window's load time grew with its pane count even though tmux warms every pane's shell concurrently.
  • Replace the serial per-pane wait with a single concurrent barrier (_wait_for_panes_ready) that polls all of a window's default-shell panes together under one shared timeout.
  • Preserve config-order semantics: each window is still built and finished before the next begins, so a later window can rely on a directory or side effect created by an earlier window's commands.
  • Preserve the Outputs % / percent sign after every command in some panes #365 zsh partial-line % fix: no pane is resized before its shell has drawn its prompt; select_layout runs once per window, after that window's barrier.
  • Update the load progress display and before_script handling to match the new build-event ordering (see Follow-on changes).
  • No config or YAML changes — same workspaces, faster loads.

Motivation

#1018 fixed the long-standing zsh % marker (#365) by waiting for each pane's shell to draw its prompt before applying its layout. That wait ran serially: pane N+1 wasn't split until pane N was ready. Because tmux starts a pane's shell the moment the pane is split, the shells in a window already warm up concurrently — only the observation of readiness was serial. With a heavy interactive rc file, a window's load cost became panes × shell-init, the regression reported in #1053.

Design decisions

Concurrent barrier per window. A window's panes are all split first, so their shells initialize together; a single barrier then waits for every default-shell pane in that window under one shared timeout. Readiness becomes one observation pass over all the panes instead of one blocking wait per pane.

Per-window, not whole-session. Windows are built one at a time: create panes → barrier → lay out → dispatch commands → finish → next window. A window's commands run before the next window is created, so a later window can depend on side effects of an earlier one (e.g. a start_directory whose directory an earlier command created). The readiness wait is collapsed within a window; window-to-window ordering is unchanged.

Split the newest pane; defer layout until after the barrier. A split resizes the pane it targets, and the build only ever splits the newest pane — created microseconds earlier and still sourcing its rc, which is safe to resize. The window's single select_layout is deferred until after its barrier, when every shell in the window is past its prompt. Why not keep a per-split select_layout? That call also resizes the older panes; if one is mid-prompt, the resize races zsh's prompt redraw and resurrects the % marker.

Adaptive space reclaim. Without a per-split layout, a window with many panes can run out of room for the next split. _split_pane_reclaiming_space handles this: on a no-space failure it first waits for the window's existing panes (so they are safe to resize), runs select_layout to redistribute, then retries. Reclaim never resizes a not-ready pane, and a genuinely-too-small window still raises as before (#800).

Internal and always-on. No new YAML keys or tunables; the barrier just makes the existing readiness behavior cheap.

Follow-on changes

Building a window's panes together (rather than one-ready-then-next) changes the grouping and ordering of build events, so two adjacent areas needed updates:

  • Progress display (src/tmuxp/cli/_progress.py): window_done events now carry window identity, and the default progress bar tracks created vs. finished windows so the bar and the window label stay in sync as panes are created.
  • before_script streaming (src/tmuxp/cli/load.py, src/tmuxp/util.py): before_script inherits the terminal's stdio by default so TTY-aware tools run interactively, the spinner starts only after before_script finishes, and its output is captured into the panel only when a nonzero --progress-lines is requested.
  • synchronize-panes restore (src/tmuxp/workspace/builder.py): the temporary synchronize-panes disable is isolated to pane-command dispatch and skips targets for panes that exited during startup.

Before / After

Readiness wait for a window with N default-shell panes:

Before:  N serial waits  (pane N+1 not split until pane N is ready)
After:   1 concurrent barrier over all N panes (shells already warming together)

Test plan

Builder / readiness:

  • test_wait_for_panes_ready_all_ready, test_wait_for_panes_ready_mixed — the barrier reports each pane ready and times out only non-prompt panes
  • test_wait_for_pane_ready_returns_true, test_wait_for_pane_ready_timeout — the one-pane wrapper
  • test_pane_readiness_waits_for_default_shell_panes — only default-shell panes enter the barrier (custom shell/window_shell skipped)
  • test_build_waits_for_each_window_before_dispatch — a window's panes are awaited together before its commands run
  • test_select_layout_called_once_per_window, test_layout_runs_after_readiness_barrier — the Outputs % / percent sign after every command in some panes #365 invariant: select_layout runs once per window, after that window's barrier
  • test_split_target_refreshes_without_readiness_wait — splitting no longer blocks on a per-pane prompt wait
  • test_build_dispatches_window_commands_before_later_start_directory — a later window can depend on an earlier window's command side effects
  • test_issue_800_default_size_many_windows — reclaim still re-raises when a window is genuinely too small (no space for new pane with main-horizontal and a few panes #800)
  • test_synchronize_panes_disabled_during_pane_commands, test_synchronize_panes_ignores_exited_targets — sync isolation and skipping exited targets
  • test_plugin_hook_orderon_window_create / after_window_finished order across windows

Progress / before_script:

  • test_builder_window_done_events_include_window_identity — completion events carry window identity
  • test_spinner_default_progress_tracks_started_window_in_phase_one, test_spinner_default_progress_tracks_completed_window_in_phase_two — bar and label stay in sync
  • test_load_workspace_pauses_spinner_for_before_script — spinner starts after before_script
  • test_load_workspace_handles_explicit_before_script_progress_linesbefore_script output captured into the panel only on explicit --progress-lines

Suite / quality:

  • uv run py.test — full suite green
  • uv run mypy — strict, clean
  • uv run ruff check . && uv run ruff format . — clean
  • uv run py.test --doctest-modules src/tmuxp/workspace/builder.py — doctests for the readiness helpers

@tony

tony commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.04651% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.81%. Comparing base (91ac851) to head (c2b70b1).

Files with missing lines Patch % Lines
src/tmuxp/workspace/builder/classic.py 88.20% 17 Missing and 4 partials ⚠️
src/tmuxp/util.py 47.05% 8 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1056      +/-   ##
==========================================
- Coverage   82.56%   81.81%   -0.76%     
==========================================
  Files          31       31              
  Lines        2770     2925     +155     
  Branches      518      546      +28     
==========================================
+ Hits         2287     2393     +106     
- Misses        346      392      +46     
- Partials      137      140       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony force-pushed the tmuxp-lag branch 2 times, most recently from cd2526a to ed3063a Compare June 27, 2026 10:45
@tony

tony commented Jun 27, 2026

Copy link
Copy Markdown
Member Author

Code review

Found 1 issue:

  1. The build() docstring says plugin hooks "fire by phase" — that on_window_create runs for every window "before any after_window_finished" — but the build loop is per-window sequential (create → finish → next window). For a multi-window session, on_window_create for a later window runs after the earlier window's after_window_finished, so there is no global create-phase. The PR's own test_plugin_hook_order asserts this per-window order (on_window_create:one, after_window_finished:one, on_window_create:two, after_window_finished:two), which contradicts the docstring. This reads like a leftover from the earlier whole-session two-phase build before it was reverted to per-window ordering.

passed in on initialization to create a new Session object.
Plugin hooks fire by phase: ``on_window_create`` runs for every window
as it is created, before any ``after_window_finished``. Each window's
``after_window_finished`` still runs once that window has been laid out
and its pane commands dispatched.

Contradicted by the PR's own test:

),
expected_order=[
"before_workspace_builder",
"on_window_create:one",
"after_window_finished:one",
"on_window_create:two",
"after_window_finished:two",
],
),

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

why: Loading a workspace waited for each pane's shell prompt one pane at
a time, so a window's load cost scaled with its pane count. Preparing a
window's panes together lets their shells warm up in parallel and
collapses the per-window wait into a single readiness barrier.

what:
- Create a window's panes up front, then wait for every default-shell
  pane in one barrier before applying the layout and sending commands.
- Honor the pane_readiness policy (auto/always/never) resolved in
  build(): auto waits only for zsh, so non-zsh shells skip the wait;
  always forces it; never disables it.
- Fall back to sequential per-pane setup when later panes depend on an
  earlier pane's start_directory side effects.
- Run before_script attached to the terminal by default so interactive
  prompts and TTY-aware tools behave normally. Restore panel capture
  with --progress-lines / TMUXP_PROGRESS_LINES.
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