Skip to content

0.41.0: diff surface polls git diff HEAD --numstat at 10-42/s and is silently auto-recreated — main process saturated, typing lag #141

Description

@Ray0483

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:

  1. 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.

  2. 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:

  1. Closed the diff surface. wmux tree confirmed the pane contained only a terminal surface.
    Measured: git=0, main CPU 1.8%.
  2. Ran wmux markdown <file> — a command that has nothing to do with git or diffs.
  3. 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).
  4. Measured immediately after: git=119 in 8 s (~15/s), main CPU 57.8%.
  5. 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

  1. 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.)
  2. Ensure a diff surface is open (wmux diff).
  3. Leave the session completely idle — do not type, do not run anything.
  4. 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.
  5. Observe main-process CPU while idle, and type into a terminal pane — input is visibly delayed.
  6. 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.
  7. 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.

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions