You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When i start wmux and it is loading the sessions or when i manually load the sessions it takes very long until all the gray dots ( i have 26 sessions open) becoming green.
What would you like wmux to do?
Whatever is done when loading sessions, i suggest to do that in parallel (extra threads) - obviously currently it goes through time in a simple for loop and thus loads them one by one.
Fixed in 1.3.1. You were right that something was serialised, and wrong about where — which turned out to be the useful part, because the actual culprit was work that did not need doing at all.
Where it wasn't
There is no for-loop over sessions. Restore is a single replaceAllWorkspaces call — one store update, all 26 workspaces at once. React then mounts them all (wmux keeps every workspace mounted so PTYs survive session switching), and each pane fires its own pty.create IPC independently. From the renderer's side that is already concurrent.
Where it was
The queue was in the main process, and it was this, running on every pane create:
which ends in execFileSync('where', [shell]) — a synchronous child process, on the Electron main thread, with no memoization. On the miss path it runs twice per pane, because resolveShell's fallback resolves the same spec again.
I measured it rather than eyeballing it:
per call
where lookup
~51 ms
pty.spawn (the actual ConPTY)
~30 ms
So wmux was spending roughly twice as long asking Windows where your shell is as it spent starting your shell — and asking 26 times for the same answer.
For your 26 workspaces that is 1.3 s if the shell resolves and ~2.7 s if it does not. And because execFileSync blocks the main event loop, during that window nothing else in wmux ran either — not the remaining pane creates, and not the pipe server that receives the report_shell_state messages your dots are waiting for. That is the direct link to the symptom you described: the dots are gray until each shell reports its first prompt, and main was too busy to listen.
The fix, and why it isn't threads
Your instinct was extra threads. That does not work here and, more usefully, is not needed: PTY handles have to live in the process that owns the IPC, so moving pty.spawn to a worker would mean marshalling every keystroke and every byte of output across a thread boundary — a large change for a modest win.
The where lookups are a different matter. They are pure, and 26 of them were computing the same value. So they are now memoized, and the answer is simply not recomputed.
The precedent was already in the same file, three functions below the code that needed it:
// `hasWsl` is cached because it shells out to `where`, and this runs on every pane create.letcachedWsl: boolean|null=null;
Someone had already worked out exactly this problem — and cached it for wsl.exe only. The reasoning was always equally true of the shell you actually use.
Measured on the shipped function, 26 resolutions:
pwsh.exe 1232ms -> 44ms (28x)
powershell.exe 1425ms -> 48ms (30x)
a shell that doesn't exist 1325ms -> 53ms (25x)
That is ~1.2–1.4 s of blocked main thread removed on a hit, and around 2.5 s on the miss path since create() resolves twice there. It also folds in a readdirSync over C:\Program Files\WindowsApps that the Store-PowerShell fallback was repeating — an ACL-heavy directory that is slow to enumerate.
Two details, since caching is where bugs hide. Negative results are cached too, deliberately: a miss is the expensive branch (where scans the whole PATH before failing) and it is what an uninstalled or mistyped shell hits on every single pane. The cost is that installing a shell mid-session is not noticed until wmux restarts, which is already how the existing default-shell cache behaves. And a cached hit is re-validated with an existsSync before being handed back, so a shell uninstalled while wmux is running re-probes instead of feeding a dead path to pty.spawn and killing the pane with node-pty's opaque File not found:.
What is still slow, stated plainly
This does not make 26 workspaces instant, and I would rather set the expectation than have you discover it:
~30 ms × 26 ≈ 0.8 s of ConPTY creation remains. That is real work and it has to happen.
Your shells' own startup — PowerShell profile, oh-my-posh, module imports — dominates after that. wmux does not control it, but those 26 shells now initialise concurrently, which they could not do while main was blocked. If your profile takes two seconds, 26 of them still take about two seconds, not fifty.
So the ceiling should now be roughly "one shell's startup plus a second", instead of that plus several seconds of wmux blocking itself.
Please tell me what you actually see. If 26 workspaces still take much longer than a single one, there is a second bottleneck and I would like the issue reopened — a rough count of seconds before and after is enough, and it beats anything I can infer from my own machine, where I do not have 26 sessions to restore.
Thanks for filing it. The wrong mechanism pointed straight at the right symptom, and "obviously it goes through them one by one" was correct about the behaviour even though the loop was in a place neither of us would have guessed from the outside.
Activity
perf(pty): memoize shell path resolution (#176)
amirlehmam commentedon Aug 17, 2026
Fixed in 1.3.1. You were right that something was serialised, and wrong about where — which turned out to be the useful part, because the actual culprit was work that did not need doing at all.
Where it wasn't
There is no for-loop over sessions. Restore is a single
replaceAllWorkspacescall — one store update, all 26 workspaces at once. React then mounts them all (wmux keeps every workspace mounted so PTYs survive session switching), and each pane fires its ownpty.createIPC independently. From the renderer's side that is already concurrent.Where it was
The queue was in the main process, and it was this, running on every pane create:
which ends in
execFileSync('where', [shell])— a synchronous child process, on the Electron main thread, with no memoization. On the miss path it runs twice per pane, becauseresolveShell's fallback resolves the same spec again.I measured it rather than eyeballing it:
wherelookuppty.spawn(the actual ConPTY)So wmux was spending roughly twice as long asking Windows where your shell is as it spent starting your shell — and asking 26 times for the same answer.
For your 26 workspaces that is 1.3 s if the shell resolves and ~2.7 s if it does not. And because
execFileSyncblocks the main event loop, during that window nothing else in wmux ran either — not the remaining pane creates, and not the pipe server that receives thereport_shell_statemessages your dots are waiting for. That is the direct link to the symptom you described: the dots are gray until each shell reports its first prompt, and main was too busy to listen.The fix, and why it isn't threads
Your instinct was extra threads. That does not work here and, more usefully, is not needed: PTY handles have to live in the process that owns the IPC, so moving
pty.spawnto a worker would mean marshalling every keystroke and every byte of output across a thread boundary — a large change for a modest win.The
wherelookups are a different matter. They are pure, and 26 of them were computing the same value. So they are now memoized, and the answer is simply not recomputed.The precedent was already in the same file, three functions below the code that needed it:
Someone had already worked out exactly this problem — and cached it for
wsl.exeonly. The reasoning was always equally true of the shell you actually use.Measured on the shipped function, 26 resolutions:
That is ~1.2–1.4 s of blocked main thread removed on a hit, and around 2.5 s on the miss path since
create()resolves twice there. It also folds in areaddirSyncoverC:\Program Files\WindowsAppsthat the Store-PowerShell fallback was repeating — an ACL-heavy directory that is slow to enumerate.Two details, since caching is where bugs hide. Negative results are cached too, deliberately: a miss is the expensive branch (
wherescans the whole PATH before failing) and it is what an uninstalled or mistyped shell hits on every single pane. The cost is that installing a shell mid-session is not noticed until wmux restarts, which is already how the existing default-shell cache behaves. And a cached hit is re-validated with anexistsSyncbefore being handed back, so a shell uninstalled while wmux is running re-probes instead of feeding a dead path topty.spawnand killing the pane with node-pty's opaqueFile not found:.What is still slow, stated plainly
This does not make 26 workspaces instant, and I would rather set the expectation than have you discover it:
So the ceiling should now be roughly "one shell's startup plus a second", instead of that plus several seconds of wmux blocking itself.
Please tell me what you actually see. If 26 workspaces still take much longer than a single one, there is a second bottleneck and I would like the issue reopened — a rough count of seconds before and after is enough, and it beats anything I can infer from my own machine, where I do not have 26 sessions to restore.
Thanks for filing it. The wrong mechanism pointed straight at the right symptom, and "obviously it goes through them one by one" was correct about the behaviour even though the loop was in a place neither of us would have guessed from the outside.
1.3.1 is out — https://github.com/amirlehmam/wmux/releases/tag/v1.3.1
feat(agents): identify which agent runs in each pane
feat(agents): screen detection for agents that report no state
fix(codex): restore is fully inert when off, never hands PowerShell t…