Diff surface polls git diff HEAD --numstat unthrottled and is silently auto-recreated, saturating the main process and causing keystroke latency
wmux version: 0.41.0
OS: Windows 11 Pro 10.0.26200 (20 cores, 63.7 GB RAM)
Git: 2.41.0.windows.3
Shell in pane: powershell.exe
Summary
Two behaviours combine into a persistent, hard-to-diagnose typing lag:
-
A diff surface polls git diff HEAD --numstat with no throttling — measured at
10–42 invocations per second while the session is completely idle. There is no debounce and
no back-pressure from how long the previous invocation took.
-
The diff surface is silently auto-recreated whenever a new surface is opened in the workspace
(and on session restore). Closing it is not durable, which is why the problem appears to "come
back on its own" and why restarting the app does not help.
In my repo git diff HEAD --numstat takes ~0.8–1.0 s, so invocations overlap heavily — roughly
10–30 git.exe processes alive at any moment. The wmux main process burns 79–106% of a CPU
core while completely idle (no typing, no terminal output, no agent activity).
Because Electron's main process is a single-threaded Node event loop, and keystrokes travel
renderer → IPC → main → PTY, this manifests directly as typing lag in every terminal pane.
Closing the diff surface drops git spawns to zero and main-process CPU to ~1.7% immediately.
Reproduced four times.
Note: this is not a resurrection of the GitPoller from #100 — that class was deleted in
ca837b7 / v0.27.0, and the command being spawned here (git diff HEAD --numstat) is tied to the
lifetime of a diff surface, not to a cwd watcher. It is also unrelated to #139, which concerns
orphaned MCP node.exe children; every process in my case is git.exe parented directly to the
wmux main process.
The auto-recreation (this is the part that makes it persistent)
Observed sequence, all verified with wmux tree:
- Closed the diff surface.
wmux tree confirmed the pane contained only a terminal surface.
Measured: git=0, main CPU 1.8%.
- Ran
wmux markdown <file> — a command that has nothing to do with git or diffs.
wmux tree now showed three surfaces: the terminal, the markdown surface I asked for, and
a brand-new diff surface I never requested (surf-b8a1da2b…, a different ID from the one
I had closed).
- Measured immediately after:
git=119 in 8 s (~15/s), main CPU 57.8%.
- Closed the new diff surface again:
git=0, main CPU 1.7%, markdown surface unaffected.
The same thing happens on session restore, which is why restarting wmux does not fix the lag —
the diff surface comes back with the restored session and polling resumes.
It is not limited to creating surfaces. Running wmux markdown set <existing-surface-id> --file <path>,
which only replaces the content of a surface that already exists, also created a fresh diff surface
(surf-f14fbbab…). Read-only commands such as wmux tree do not trigger it. So the trigger appears
to be any operation that mutates surface state, which makes the diff surface very difficult to keep
closed in practice — I had to close it three separate times while writing this report.
Measurements
Sampling method: poll Win32_Process for git.exe, filter on ParentProcessId == <wmux main PID>,
count distinct PIDs. CPU measured as delta of Process.CPU over the window, expressed as % of one core.
All measurements taken with the session idle — no typing, no commands running in the pane.
Diff surface open
| Window (10 s, idle) |
distinct git.exe spawned by wmux main |
wmux main CPU |
| 1 |
284 (~28/s) |
79.2% |
| 2 |
326 (~33/s) |
82.2% |
| 3 |
425 (~42/s) |
91.4% |
| 4 |
314 (~31/s) |
94.1% |
An earlier run, on the diff surface restored from the saved session, showed a lower but still
pathological rate: 159 spawns in ~15 s (~10/s), main process avg 32.1% / max 106% of one core
over an 80-sample × 500 ms idle run.
I could not confirm what accounts for ~10/s versus ~28–42/s. The most likely explanation is that
more than one diff surface was alive during the faster runs — by then three workspaces existed, two
sharing the same cwd, and the CLI made it impossible to enumerate surfaces across all of them
(see issue 2). If the poll is per-surface, the rate would scale with the number of live diff
surfaces. Flagging this as a hypothesis, not a finding.
Child process breakdown
Every spawned process is the same command:
PPID <wmux main> -> 245 git procs in 10 s
git diff HEAD --numstat
git diff HEAD --numstat
...
Per-process CPU within the wmux process tree during an idle window:
| Process |
avg CPU (% of one core) |
| main |
32.1 (max 106) |
| renderer |
4.8 |
| gpu-process |
3.8 |
| utility (network) |
0.0 |
The load is entirely on the main process, not the renderer.
Diff surface closed
12 s idle: git=0 main cpu=1.8%
10 s idle: git=0 main cpu=1.7%
CPU drops by roughly an order of magnitude and the user-visible typing lag disappears.
Repository characteristics
This matters because the bug is invisible on small repos — git diff HEAD --numstat returns in a
few ms there, so even a 40 Hz poll goes unnoticed.
| Metric |
Value |
| Working tree size |
14 GB |
Files on disk (excl. .git) |
187,645 |
| Tracked files |
2,543 |
.git size |
491 MB |
git diff HEAD --numstat wall time |
1.03 s |
| …after adding a Defender exclusion for the repo path |
0.79 s |
Git is already tuned — core.fsmonitor=true and core.untrackedCache=true are set, and the
fsmonitor daemon is confirmed running. The remaining ~0.8 s is the cost of the working tree itself.
Notably, reducing untracked file count does not help. Before filing this I added the bulk data
directories to .gitignore, taking untracked files from 115,665 down to 435. The lag was completely
unaffected, because git diff HEAD only compares tracked files against HEAD and never walks
untracked paths. The bottleneck is the poll rate, not the file count.
Steps to reproduce
- Open a workspace whose
cwd is a git repo large enough that git diff HEAD --numstat takes
several hundred ms. (Mine is 14 GB / 187k files; a synthetic large working tree should do.)
- Ensure a diff surface is open (
wmux diff).
- Leave the session completely idle — do not type, do not run anything.
- Count
git.exe processes whose parent is the wmux main process. On Windows:
Get-CimInstance Win32_Process -Filter "Name='git.exe'" |
Select-Object ProcessId, ParentProcessId, CommandLine
Poll in a loop and count distinct PIDs over a fixed window.
- Observe main-process CPU while idle, and type into a terminal pane — input is visibly delayed.
- Close the diff surface (verify with
wmux tree, not list-surfaces — see issue 2).
Spawns and CPU drop to near zero; typing returns to normal.
- Now open any unrelated surface, e.g.
wmux markdown <some-file>. Run wmux tree again:
a new diff surface has been created without being requested, and the polling has resumed.
Suggested fix
For the polling itself — any one of these would resolve the pathological case:
- Do not re-invoke while a previous invocation is still in flight. This alone caps concurrency
at 1 and would have prevented this entirely.
- Debounce and drive refreshes from file-change events rather than a timer. A
git diff refresh
only needs to run when the working tree actually changed, and there is already a filesystem
watcher in play for other features.
- Adaptive interval: measure how long the last invocation took and set the next poll to some
multiple of it. A repo where the command costs 1 s should never be polled at 10–40 Hz.
- Stop polling when the diff surface is not visible (background workspace, hidden tab).
For the auto-recreation:
- A diff surface the user explicitly closed should stay closed for that session, and opening an
unrelated surface should not resurrect it.
Given that the main process is also the keystroke relay, it may be worth moving git invocation and
output parsing off the main thread regardless of poll rate.
Issue 2 (secondary): CLI state commands disagree, making cleanup silently fail
Found while diagnosing the above. Possibly separate bugs, but they made this one much harder to pin
down and they broke a scripted cleanup.
-
wmux tree --workspace <id> ignores the flag. Passing three different workspace IDs returned
the byte-identical tree (same paneId, same surface IDs) for all three. There appears to be no
working way to enumerate surfaces in a non-active workspace.
-
wmux list-surfaces and wmux tree disagree about current state. At the same moment:
wmux tree reported pane pane-c36f0826… containing a terminal and a diff surface.
wmux list-surfaces reported pane pane-48d68481… containing only a terminal.
$WMUX_SURFACE_ID in the running shell matched the surface reported by tree, not the one
reported by list-surfaces.
Consequence: a script doing "list surfaces → find type == diff → close it" found nothing to
close and exited reporting success, while the diff surface was still open and still polling.
This sent me chasing a phantom "polling restarts by itself" behaviour that was really a close
that never happened.
-
wmux diff appears to create a new workspace. Before the call there were 2 workspaces;
after, 3 — the new one auto-named 工作区 3. Note the locale inconsistency: the existing
workspaces were named Session 1 / Workspace 3, so default names are being generated in
different languages within one session. The new workspace also duplicated an existing cwd.
I expected wmux diff to add a surface to the current workspace, not create another one.
Workaround for other users
In a large repo, close the diff surface and verify with wmux tree (not list-surfaces) that it
actually went away:
wmux tree # find the surface with "type": "diff"
wmux close-surface <that-id>
wmux tree # confirm it is gone
Restarting wmux does not help. And because the surface is auto-recreated whenever a new surface is
opened, this has to be repeated — check wmux tree again any time typing starts to feel sluggish.
Diff surface polls
git diff HEAD --numstatunthrottled and is silently auto-recreated, saturating the main process and causing keystroke latencywmux version: 0.41.0
OS: Windows 11 Pro 10.0.26200 (20 cores, 63.7 GB RAM)
Git: 2.41.0.windows.3
Shell in pane: powershell.exe
Summary
Two behaviours combine into a persistent, hard-to-diagnose typing lag:
A
diffsurface pollsgit diff HEAD --numstatwith no throttling — measured at10–42 invocations per second while the session is completely idle. There is no debounce and
no back-pressure from how long the previous invocation took.
The diff surface is silently auto-recreated whenever a new surface is opened in the workspace
(and on session restore). Closing it is not durable, which is why the problem appears to "come
back on its own" and why restarting the app does not help.
In my repo
git diff HEAD --numstattakes ~0.8–1.0 s, so invocations overlap heavily — roughly10–30
git.exeprocesses alive at any moment. The wmux main process burns 79–106% of a CPUcore while completely idle (no typing, no terminal output, no agent activity).
Because Electron's main process is a single-threaded Node event loop, and keystrokes travel
renderer → IPC → main → PTY, this manifests directly as typing lag in every terminal pane.Closing the diff surface drops git spawns to zero and main-process CPU to ~1.7% immediately.
Reproduced four times.
Note: this is not a resurrection of the
GitPollerfrom #100 — that class was deleted inca837b7 / v0.27.0, and the command being spawned here (
git diff HEAD --numstat) is tied to thelifetime of a diff surface, not to a cwd watcher. It is also unrelated to #139, which concerns
orphaned MCP
node.exechildren; every process in my case isgit.exeparented directly to thewmux main process.
The auto-recreation (this is the part that makes it persistent)
Observed sequence, all verified with
wmux tree:wmux treeconfirmed the pane contained only a terminal surface.Measured:
git=0, main CPU1.8%.wmux markdown <file>— a command that has nothing to do with git or diffs.wmux treenow showed three surfaces: the terminal, the markdown surface I asked for, anda brand-new
diffsurface I never requested (surf-b8a1da2b…, a different ID from the oneI had closed).
git=119 in 8 s (~15/s), main CPU57.8%.git=0, main CPU1.7%, markdown surface unaffected.The same thing happens on session restore, which is why restarting wmux does not fix the lag —
the diff surface comes back with the restored session and polling resumes.
It is not limited to creating surfaces. Running
wmux markdown set <existing-surface-id> --file <path>,which only replaces the content of a surface that already exists, also created a fresh diff surface
(
surf-f14fbbab…). Read-only commands such aswmux treedo not trigger it. So the trigger appearsto be any operation that mutates surface state, which makes the diff surface very difficult to keep
closed in practice — I had to close it three separate times while writing this report.
Measurements
Sampling method: poll
Win32_Processforgit.exe, filter onParentProcessId == <wmux main PID>,count distinct PIDs. CPU measured as delta of
Process.CPUover the window, expressed as % of one core.All measurements taken with the session idle — no typing, no commands running in the pane.
Diff surface open
git.exespawned by wmux mainAn earlier run, on the diff surface restored from the saved session, showed a lower but still
pathological rate: 159 spawns in ~15 s (~10/s), main process avg 32.1% / max 106% of one core
over an 80-sample × 500 ms idle run.
I could not confirm what accounts for ~10/s versus ~28–42/s. The most likely explanation is that
more than one diff surface was alive during the faster runs — by then three workspaces existed, two
sharing the same
cwd, and the CLI made it impossible to enumerate surfaces across all of them(see issue 2). If the poll is per-surface, the rate would scale with the number of live diff
surfaces. Flagging this as a hypothesis, not a finding.
Child process breakdown
Every spawned process is the same command:
Per-process CPU within the wmux process tree during an idle window:
The load is entirely on the main process, not the renderer.
Diff surface closed
CPU drops by roughly an order of magnitude and the user-visible typing lag disappears.
Repository characteristics
This matters because the bug is invisible on small repos —
git diff HEAD --numstatreturns in afew ms there, so even a 40 Hz poll goes unnoticed.
.git).gitsizegit diff HEAD --numstatwall timeGit is already tuned —
core.fsmonitor=trueandcore.untrackedCache=trueare set, and thefsmonitor daemon is confirmed running. The remaining ~0.8 s is the cost of the working tree itself.
Notably, reducing untracked file count does not help. Before filing this I added the bulk data
directories to
.gitignore, taking untracked files from 115,665 down to 435. The lag was completelyunaffected, because
git diff HEADonly compares tracked files against HEAD and never walksuntracked paths. The bottleneck is the poll rate, not the file count.
Steps to reproduce
cwdis a git repo large enough thatgit diff HEAD --numstattakesseveral hundred ms. (Mine is 14 GB / 187k files; a synthetic large working tree should do.)
wmux diff).git.exeprocesses whose parent is the wmux main process. On Windows:wmux tree, notlist-surfaces— see issue 2).Spawns and CPU drop to near zero; typing returns to normal.
wmux markdown <some-file>. Runwmux treeagain:a new diff surface has been created without being requested, and the polling has resumed.
Suggested fix
For the polling itself — any one of these would resolve the pathological case:
at 1 and would have prevented this entirely.
git diffrefreshonly needs to run when the working tree actually changed, and there is already a filesystem
watcher in play for other features.
multiple of it. A repo where the command costs 1 s should never be polled at 10–40 Hz.
For the auto-recreation:
unrelated surface should not resurrect it.
Given that the main process is also the keystroke relay, it may be worth moving git invocation and
output parsing off the main thread regardless of poll rate.
Issue 2 (secondary): CLI state commands disagree, making cleanup silently fail
Found while diagnosing the above. Possibly separate bugs, but they made this one much harder to pin
down and they broke a scripted cleanup.
wmux tree --workspace <id>ignores the flag. Passing three different workspace IDs returnedthe byte-identical tree (same
paneId, same surface IDs) for all three. There appears to be noworking way to enumerate surfaces in a non-active workspace.
wmux list-surfacesandwmux treedisagree about current state. At the same moment:wmux treereported panepane-c36f0826…containing a terminal and a diff surface.wmux list-surfacesreported panepane-48d68481…containing only a terminal.$WMUX_SURFACE_IDin the running shell matched the surface reported bytree, not the onereported by
list-surfaces.Consequence: a script doing "list surfaces → find
type == diff→ close it" found nothing toclose and exited reporting success, while the diff surface was still open and still polling.
This sent me chasing a phantom "polling restarts by itself" behaviour that was really a close
that never happened.
wmux diffappears to create a new workspace. Before the call there were 2 workspaces;after, 3 — the new one auto-named
工作区 3. Note the locale inconsistency: the existingworkspaces were named
Session 1/Workspace 3, so default names are being generated indifferent languages within one session. The new workspace also duplicated an existing
cwd.I expected
wmux diffto add a surface to the current workspace, not create another one.Workaround for other users
In a large repo, close the diff surface and verify with
wmux tree(notlist-surfaces) that itactually went away:
Restarting wmux does not help. And because the surface is auto-recreated whenever a new surface is
opened, this has to be repeated — check
wmux treeagain any time typing starts to feel sluggish.