fix: bounded frame channel to preserve fast typing#225
Merged
Conversation
…fast typing Commit 694156e replaced the unbounded mpsc::channel per client with a single-slot Arc<Mutex<Option<String>>> to fix a memory leak during rapid copy-mode scrolling. The single-slot design overwrites any unconsumed frame, which drops intermediate frames during fast typing — the cursor advances but characters are not rendered (psmux#224). Replace the single-slot with a bounded sync_channel (capacity 16). This preserves intermediate frames during normal typing bursts while still bounding memory to O(FRAME_CHANNEL_CAPACITY * clients) instead of the original unbounded O(frames * clients). When the channel is full under sustained high-throughput (e.g. rapid scroll), the frame is skipped rather than blocking, so the server is never stalled. The client already has 16 frames queued to drain, so it won't miss meaningful content. The writer thread now drains all queued frames per loop iteration via try_recv(), ensuring batched delivery without the 5ms polling delay between frames. Fixes psmux#224 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When remain-on-exit is on, prune_exited() marks panes as dead but keeps them in the tree. The hook-firing logic only checked whether the tree leaf count decreased (any_pruned), which never happens in the remain-on-exit case since dead panes are still counted as leaves. Fix: prune_exited() now returns a newly_dead_count tracking panes that transitioned alive→dead in each call. reap_children() propagates this as a separate any_newly_dead flag alongside any_pruned. The server event loop fires pane-died/pane-exited hooks when either flag is true, but only resizes the layout when panes were actually removed. This matches tmux semantics: hooks fire when the child process exits, regardless of whether the pane is retained or removed. The dead flag on the pane prevents hooks from firing repeatedly on subsequent reap ticks — has_any_exited() skips already-dead panes. Fixes psmux#227 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Owner
|
Hey @tarikguney, this is merged now. Really appreciate you taking the time to not only identify the root cause but also implement the fix yourself. Your analysis of the single slot overwrite behavior was spot on. The bounded I also noticed you included the pane died/pane exited hooks fix for remain on exit in the second commit, which is a nice bonus. Thanks again for the solid contribution! |
This was referenced Apr 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Arc<Mutex<Option<String>>>frame push (from 694156e) with a boundedsync_channel(capacity 16)O(FRAME_CHANNEL_CAPACITY * clients)— still prevents the original unbounded growth during rapid copy-mode scrollingWhat changed
src/types.rs:FrameSlot→FrameChannelbacked bympsc::sync_channel(16).push_frame()usestry_send()— if the channel is full (sustained scroll burst), the frame is skipped rather than blocking, so the server is never stalled. Dead channels are pruned viaDisconnected.src/server/connection.rs: Writer thread now locks the channel receiver once and drains all queued frames per loop iteration viatry_recv(), delivering batched frames without the 5ms polling delay between them.Why this approach
Fixes #224
Test plan
test_scroll_memory.ps1— memory should stay bounded (regression test from 694156e)