perf(terminal): coalesce PTY output posts and unblock the keystroke path - #83
Merged
Conversation
Typing into a session could lag by minutes with many live sessions. Three compounding causes, all on the shared UI dispatcher (issue #70): 1. TerminalBridge.OnPtyData did one Dispatcher.BeginInvoke per PTY chunk, so chatty background sessions flooded the queue. Introduce OutputCoalescer: chunks arriving before the dispatcher gets a turn collapse into a single post. The scheduler and emitter are injected so the state machine is unit-testable without a dispatcher or WebView2. 2. Keystrokes were starved by that same queue, not just rendering. WebView2 raises WebMessageReceived on the UI thread, so an input event waits behind the backlog, and the echo waits again to render - two queue traversals per visible character. Background sessions now post at DispatcherPriority .Background and only the active pane posts at Normal, via the new TerminalBridge.IsForeground flag driven from UpdateActiveTerminalHighlight. 3. Every keystroke synchronously recomputed AlertCount (an O(N) scan over all sessions) twice and invalidated WPF bindings both times. AlertCleared fires unconditionally from NotifyUserInteracted, so the second raise in the UserInput handler was redundant - removed. The remaining raise goes through RaiseAlertCountIfChanged, which suppresses no-op raises. AlertRaised and AlertCleared switch from blocking Invoke to BeginInvoke; neither caller consumes a result and AlertRaised arrives on a threadpool timer callback. The pre-ready output buffer now drains through the coalescer too, so load-time output and post-ready chunks share one buffer and keep arrival order. MainViewModel's GroupsChanged Invoke is deliberately left blocking: it is low-frequency group CRUD, not on the hot path, and deferring it could reorder sidebar rebuilds. Verified: 213/213 unit tests pass (7 new for the coalescer); app starts clean. The latency improvement itself is NOT yet measured under load - that needs a real multi-session run with DebugTerminalTrace enabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Fixes the typing-freeze reported from live use: with many live sessions, typing into a pane could lag by minutes before the characters appeared.
Closes #70.
Root cause
Three compounding causes, all on the shared WPF UI dispatcher. Full trace in #70; summary:
TerminalBridge.OnPtyDatadid aDispatcher.BeginInvokefor every chunk from every session. Chatty background sessions (Claude spinners, status-line repaints) flooded the queue.CoreWebView2.WebMessageReceivedis raised on the UI thread, so a keystroke waits in the same backlog it's competing with — then the echo waits again to render. Two full queue traversals per visible character, which is how hundreds of ms becomes minutes.AlertCount(Sessions.Count(s => s.NeedsAttention)) was recomputed twice per keystroke with WPF binding invalidation each time.Scale context: this was originally measured at 23 sessions; the reporter's
state.jsonhas 39.Changes
OutputCoalescer(Terminal/OutputCoalescer.cs) — chunks arriving before the dispatcher gets a turn collapse into a single post. Scheduler and emitter are injected, so the state machine is unit-testable with no dispatcher and no WebView2.TerminalBridge.IsForeground, driven fromUpdateActiveTerminalHighlight. Background sessions post atDispatcherPriority.Background; only the active pane posts atNormal.AlertCountraise in theUserInputhandler (AlertClearedfires unconditionally fromNotifyUserInteractedand already raises it). The remaining raise goes throughRaiseAlertCountIfChanged, which suppresses no-op raises.AlertRaised/AlertClearedmove from blockingInvoketoBeginInvoke.MainViewModel'sGroupsChangedInvokeis deliberately left blocking — low-frequency group CRUD, not on the hot path, and deferring it could reorder sidebar rebuilds.Verification
Not yet verified: the latency improvement itself is unmeasured under real load. That needs a multi-session run with
Settings > Diagnostics > Trace terminal input/outputon, comparingOUTPUTdispatcher latency before/after. Worth doing before this is assumed fixed.Follow-ups (deliberately not in this PR)
git status --porcelainevery 10s forever regardless of visibility (SessionViewModel.cs:109). At 39 sessions that's ~8 process spawns/sec of full working-tree scans. Steps 4-5 in perf: coalesce per-session PTY output dispatcher posts to prevent UI starvation #70.AlertDetector.Feedreallocates aSystem.Threading.Timerper output chunk.🤖 Generated with Claude Code