Skip to content

macOS Desktop: main thread spawns git ~50/s — uncached core.fsmonitor probe + per-file untracked diffs in repos with large untracked dirs #34005

Open

Description

@StarryKira

What version of the Codex App are you using (From "About Codex" dialog)?

26.715.31925 (5551) — Codex Framework 150.0.7871.124

What subscription do you have?

ChatGPT Pro 20x

What platform is your computer?

macOS 26.5.2 (25F84), Darwin 25.5.0, arm64 (Apple Silicon)

What issue are you seeing?

The Electron main process sustains 35–80% CPU indefinitely because the bundled VS Code git extension spawns git roughly 45–50 times per second, synchronously, on the main thread.

I traced this end-to-end. Root cause is a large untracked directory inside an opened repo — in my case a stray pnpm store (.pnpm-store/, 54,872 files, 649 MB) that was neither tracked nor gitignored.

1. sample shows the main thread blocked in process creation. ~21% of all main-thread samples sit directly in posix_spawn:

2145 __CFRunLoopRun
  ... node::worker::MessagePort::OnMessage
      ... _register_external_reference_process_wrap
          689 uv_spawn + 3108
            689 posix_spawn + 496
              660 __posix_spawn        <-- main thread, synchronous

Because the main thread is stuck in posix_spawn, it also reaps children late — ps shows a steady trail of <defunct> git processes.

2. What it actually runs. ps cannot read the argv (these processes live <1 ms), so I wrote a small native poller using proc_listpids() + sysctl(KERN_PROCARGS2). Over a 15 s capture, sampled by duty cycle:

share of git process time command
~94% git config --null --get core.fsmonitor
~4% git -c ... diff --no-ext-diff --no-textconv --no-index --raw --numstat -z -- /dev/null <file>
~2% git -c ... hash-object -- <file>

All with CWD=<repo root>.

Two separate defects compound here:

  • (a) git config --null --get core.fsmonitor is re-forked before every single git invocation, uncached. It dominates by a wide margin — the app spends ~94% of its git time re-reading one static config key. This alone triples the process count of every git operation.
  • (b) Untracked files are diffed one file per process. For each untracked file the extension runs hash-object + diff --no-index against /dev/null to compute added-line stats. With 54,872 untracked files that is ~165,000 git process spawns queued up — hours of work at the observed rate.

3. System-wide collateral damage. Process creation at ~50/s saturates the macOS daemons that service it. Concurrent ps aux during the incident:

opendirectoryd   27.9%
WindowServer     19.0%
runningboardd     7.1%
launchd           6.5%
notifyd           6.5%
logd              6.3%
load average: 5.19 6.01 5.08

The system PID counter wrapped through its entire range (to 99999) during a routine observation window. So the true cost is well above what Activity Monitor attributes to Codex.

4. Newly added ignore rules do not take effect until restart. After adding .pnpm-store/ to .git/info/exclude, git status --porcelain -uall dropped from 54,897 → 24 — but CPU and spawn rate were unchanged (39.5% CPU, ~40 spawns/s), because the already-enumerated file list is held in memory and keeps draining. Only a full app restart clears it.

What steps can reproduce the bug?

  1. Open any git repo in Codex Desktop on macOS.
  2. Create a large untracked directory inside it that is not in .gitignore. A pnpm store reproduces it exactly:
    cd <repo>
    pnpm install --store-dir .pnpm-store   # ~50k files
    Any directory with tens of thousands of untracked files works.
  3. Leave the app open — no prompt or task required, it happens while idle.
  4. Observe:
    # spawn rate (replace <PID> with the ChatGPT/Codex main process)
    end=$((SECONDS+10)); while [ $SECONDS -lt $end ]; do \
      ps -eo pid,ppid,comm | awk -v p=<PID> '$2==p && $3 ~ /git/ {print $1}'; \
    done | sort -u | wc -l
    I measured 400–479 distinct git PIDs per 10 s window, consistently across many runs.
  5. sample <PID> 5 confirms the main thread sitting in posix_spawn.

What is the expected behavior?

  • core.fsmonitor should be read once per repo and cached, not re-forked before every git call. This is the single highest-leverage fix — it removes ~94% of git process time on its own.
  • Untracked-file diff stats should be batched (one git invocation covering many paths) rather than 2 processes per file, and should be capped/skipped past a threshold (e.g. >5,000 untracked files), or deferred until the user actually expands that part of the SCM tree.
  • Process spawning must not happen synchronously on the Electron main thread — this blocks UI and delays child reaping.
  • Changes to .gitignore / .git/info/exclude should invalidate the pending scan queue without requiring an app restart.

Additional information

Workaround (both steps required): add the directory to .gitignore and restart the app — the ignore rule alone does not stop the in-flight queue. In my case I deleted the stray store outright, which dropped untracked files to 24.

Possibly related, but I believe none identify this mechanism:

Happy to supply the native argv-capture source or raw sample output if useful.

Activity

  1. kkkzbh commented on Jul 19, 2026

    @kkkzbh

    Additional Linux reproduction on the same Desktop build, with cancellation-path evidence.

    Environment

    • Codex Desktop: 26.715.31925
    • Linux x86_64, Wayland/Electron
    • Valid local Git repository containing 2,380 non-ignored untracked files
    • 1,468 files were under a generated output tree (about 447 MiB); most remaining files were generated/project artifacts
    • No nested repository was required for this reproduction

    Observed behavior

    • The Electron main process sustained roughly 55–68% CPU while the scan was active.
    • An 8-second strace -f -c -p <main-pid> recorded 2,100 execve, 350 clone, and 1,406 wait4 calls. The execve total includes failed PATH probes; there were 350 actual child commands.
    • A focused 4-second trace recorded 153 successful /usr/bin/git executions, about 38.7 Git commands/second.
    • Command shapes matched this issue:
      • git ... diff --no-index --raw --numstat -z -- /dev/null <untracked-path>
      • git ... hash-object -- <untracked-path>
    • Desktop logs attributed the work to requestKind=review-summary source=review_model.
    • The scan continued after switching away from the affected task. After the queue eventually drained, a later 5-second trace recorded zero Git executions.

    I also inspected .vite/build/worker.js from the packaged app.asar. In this build, the review-summary live-query path calls the core scan function (V8(...) in the minified bundle) without threading the live query's AbortSignal into it. The outer cancellation helper can reject the consumer while the already-running file scan continues.

    This aligns with the cancellation defect described in #27570. That report uses nested repositories to trigger an unbounded hash fallback; this reproduction uses ordinary untracked generated files and reaches the per-file diff/hash path described here.

    Confirmed workaround

    Git tracking was unused for this directory, so I disabled repository detection by moving .git aside. A 6-second post-change trace recorded zero successful Git executions. Existing in-flight work still had to drain or be cleared by restarting the app, consistent with this report.

    Suggested fix boundary

    • Propagate AbortSignal through the core scan and every child Git operation.
    • Cancel and reap pending children when the review subscription is disposed.
    • Batch/cache untracked metadata instead of spawning per-file diff/hash processes.
    • Invalidate an already-enumerated untracked queue when ignore or repository state changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appbugSomething isn't workingperformance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions