Repository navigation
fix(perf): take three blocking paths off the UI thread and the thread pool - #160
Merged
Merged
Conversation
… pool Three small, contained fixes from the typing-freeze investigation: - Run commands: output no longer shares InstancesChanged. Each 4KB chunk used to rebuild the chips and re-assign the whole drawer (up to 1MB) through a synchronous Dispatcher.Invoke. Output now drives a drawer-only refresh at most ~4x/s at Background priority, and the drawer appends only the new tail. - State saves: JSON is still snapshotted on the caller's thread, but the temp write and File.Replace now run on the pool. A sequence number keeps an older snapshot from overwriting a newer one, since the gate is not FIFO. - PTY reads: one dedicated thread per PTY instead of a permanently parked pool thread. 55 were measured blocked at ~60 sessions, against a pool minimum of 24, so other pool work queued behind thread injection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 4, 2026
2 tasks done
mortenaslo
added a commit
that referenced
this pull request
Nov 6, 2026
…xed 500ms (#176) A_burst_of_signals_runs_the_action_once failed on main's CI right after #160 merged (Expected: 1, Actual: 0) while passing in #160's own run and locally. Each test slept a fixed 500ms of wall clock for a 50ms timer; xunit runs test classes in parallel, and on a loaded runner the Task.Delay continuation can land later than that. A_signal_after_the_action_ran_schedules_another had a second form of the same race: if the first run was late, the second Signal piggybacked on it and the test saw one run instead of two. Now each test polls until the expected count is reached (10s ceiling, only for a broken implementation), then watches a further 300ms to catch an extra run. ThrottledSignal itself is unchanged. Mutation-checked: removing the coalescing fails the burst test; clearing _pending after the action instead of before fails the during-action test. Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX Co-authored-by: Claude Opus 5.5 <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.
Three small, contained fixes from the investigation into typing still freezing at times. Each one removes a path that either blocks the UI thread or ties up thread-pool threads.
These are fixes chosen by reading the code, not yet confirmed against a captured freeze. A stall recorder is running against the live app. The output path (foreground flush priority, batch size) is deliberately not touched here until that recorder shows it is involved.
1. Run-command output
Before: every 4 KB chunk of run-command output raised
InstancesChangedand made a synchronousDispatcher.Invokecall.After:
SessionRunnerhas a separateOutputChangedevent.InstancesChangednow fires only for structural and state changes, and is marshalled withBeginInvoke, so it never parks the PTY thread.Backgroundpriority (below input) through the newThrottledSignalclass.RunInstance.ReadSince. It falls back to a full snapshot when:2.
state.jsonsavesBefore:
SaveAsynchad noConfigureAwait, andFile.Replaceis synchronous, so the temp-file write and the swap both ran on the UI thread. Antivirus or a synced folder can make that swap slow. Saves are triggered by OSC 9001 prompt hooks, window moves, gitRepoRootupdates and about 30 other call sites.After:
AppStategraph is changed on the UI thread.3. PTY reads
Before:
ReadLoopAsyncblocked a thread-pool thread for the whole life of each session. On the live app I measured 55 pool threads parked inReadwith about 60 sessions, against a pool minimum of 24 on a 24-core machine. Past the minimum, the pool adds threads gradually, so bursts of other pool work queued up. That includes the next read of the active pane, which looks exactly like a keystroke that doesn't echo.After: each PTY gets one dedicated background thread, the same reasoning as the
MonitorExitAsyncchange in #110.Tests
All 621 unit tests pass. New tests:
ThrottledSignalTests: a burst of signals runs the action once; a later signal runs it again; a signal that arrives while the action is running is not lost; the action never runs inline.SessionRunnerTests:OutputChangedand notInstancesChanged;ReadSincereturns only the new tail;ReadSincereturns null once the buffer head has been trimmed past the reader's position.StateServiceTests:SynchronizationContextnever runs work.The second test was checked against the old
StateService: it fails after 20 s, and the save gate the old code leaks then stalls every later save in the run.Manual testing needed
These weren't exercised in a running app:
npm run dev,dotnet watch) with the drawer open, and type in another pane.state.jsonstill updates.🤖 Generated with Claude Code