Skip to content

[Windows desktop] CrBrowserMain consumes one CPU core while watching a delete-pending directory; restart clears it #44960

Open

Description

@catthememoriser

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

Windows package version: 26.908.4834.0 (OpenAI.Codex, x64; observed from the installed package, not the About dialog).
The unified desktop app executable is ChatGPT.exe. Bundled Chromium version observed: 152.0.7977.83.

What subscription do you have?

Not disclosed for privacy.

What platform is your computer?

Windows x64, NTFS.

What issue are you seeing?

The ChatGPT/Codex desktop app recurrently consumes approximately one full CPU core even when no question or task is running.

One affected instance was investigated before restarting. The CPU hotspot was the main process's CrBrowserMain thread. Repeated execution-position samples landed in directory-change notification and file-information calls, with the relevant call contexts repeatedly containing the same handle to a delete-pending directory.

After restarting the app, I confirmed that the high CPU symptom was resolved. No numerical post-restart CPU benchmark was captured.

CPU measurements before restart

All percentages below use one logical CPU = 100%, not whole-machine percentages.

Component Observed CPU
Main ChatGPT.exe process Approximately 102.1% initially; 101.5% on a later check
CrBrowserMain thread, 10-second sample 99.3% total: 27.0% user time + 72.4% kernel time
GPU child process, later check 8.4%
Primary renderer child process, later check 6.2%
codex.exe app-server, later check 0.4%

The symptom existed before diagnosis. Measurements were collected during the diagnostic session, not a controlled benchmark with all diagnostic activity stopped.

Sampled execution locations

In one recorded batch of 400 short thread-context samples:

Execution location Samples
ntdll!ZwNotifyChangeDirectoryFileEx+0x14 191
ntdll!ZwQueryInformationFile+0x14 76

Together these accounted for 267/400 (66.75%) samples. An earlier independent batch gave 281/400 (70.25%) for the same two locations. These are execution-position sample fractions, not precise per-function CPU-time percentages.

Stack-top return addresses mapped into KERNELBASE's ReadDirectoryChangesExW and GetFileInformationByHandleEx. Additional stack code pointers included ReadDirectoryChangesW and chrome.dll. This was not a fully symbolicated stack unwind.

Directory-handle evidence

Further targeted sampling repeatedly resolved the same directory handle in both hot call contexts. Its normalized path was under the NTFS $Extend$Deleted namespace; the volume, unique entry name, handle value, and original local paths are intentionally omitted.

Independent metadata queries on a duplicate of that handle returned:

DeletePending=True
Directory=True
NumberOfLinks=0

This establishes that the application still held a handle to an unlinked, delete-pending directory during the high CPU episode.

What steps can reproduce the bug?

A deterministic minimal reproduction has not been established. The observed sequence was:

  1. Use the Windows desktop app normally and leave it running.
  2. The app enters a sustained high CPU state that continues during periods without active questions/tasks.
  3. During one occurrence, inspect CPU by process and thread: CrBrowserMain consumes approximately one full core.
  4. Sample that thread and inspect the directory handles in the hot call contexts: repeated directory notification/information calls refer to the same delete-pending directory.
  5. Restart the desktop app.
  6. The high CPU symptom disappears.

The original directory path and the process or operation that deleted it were not identified. Directory deletion was observed through handle metadata; it was not deliberately triggered as a reproduction test.

What is the expected behavior?

The desktop app should return to low CPU when idle. If a watched directory is deleted or unlinked, its watcher should be retired, rebuilt, or handled with bounded retry/backoff, without a persistent full-core loop in the main thread. Recovery should not require restarting the application.

Additional information

Working hypothesis: a directory-watcher lifecycle or repeated-processing problem involving a directory that became delete-pending. The hotspot and directory state are measured evidence; the exact faulty code path and causal trigger remain unproven.

Diagnostic limits:

  • No application private symbols or full symbolicated call stack were available.
  • System-call boundary register snapshots were not treated as completed-call return codes; no particular failure code is asserted.
  • The current instance's desktop log files were empty when inspected, so they did not establish the trigger.
  • Windows Performance Recorder could not start because profiling privileges were unavailable. The alternative used brief thread-context sampling with immediate resume, plus directory-handle metadata queries.
  • Restart recovery is confirmed for this occurrence. It does not establish that every historical occurrence has the same cause or that the problem is permanently fixed.

Related reports: #37372 (Windows main-process idle CPU) and #38109 (continued activity against deleted worktrees). This report adds native directory-notification hotspots and independently verified delete-pending handle state. Whether these reports share a cause is unknown.

Privacy: This issue contains only a sanitized technical summary. No raw logs, memory dumps, screenshots, local files, usernames, machine names, account details, workspace paths, conversation content, session IDs, process/thread IDs, handle values, unique filesystem entry identifiers, absolute memory addresses, or precise local timestamps are included.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    windows-osIssues related to Codex on Windows systems
    on Sep 12, 2026
  2. 84dnnvbdvp-debug commented on Sep 12, 2026

    @84dnnvbdvp-debug

    Fresh public-source review suggests a useful process-boundary discriminator here rather than assuming this is the same loop as #38109.

    Current codex-rs/file-watcher is built on notify::RecommendedWatcher, tracks requested vs. actual watched paths, and can move the OS watch to the nearest existing ancestor when path existence changes. app-server/src/fs_watch.rs also owns those watches explicitly by connection/watch ID and removes them on fs/unwatch or connection close. So there is public watcher lifecycle machinery for missing paths; I would not infer from #38109 alone that the Rust/app-server live-query path is the exact source of this incident.

    The measured process split in this report is the stronger clue: CrBrowserMain in the main ChatGPT.exe process was ~one core while codex.exe app-server was ~0.4%. That differs materially from #38109's app-server/Git churn signature. A high-value next discriminator would be to reproduce the delete-pending-handle state while simultaneously checking whether app-server fs/watch / live-query activity is quiet. If it is quiet while CrBrowserMain continues issuing ReadDirectoryChanges* / file-information calls on the same delete-pending handle, this points to a Desktop/Chromium-side watcher lifecycle rather than the public Rust watcher.

    For regression coverage I would keep the process boundary explicit: create a watched directory, unlink/delete it while the app remains open, verify the main-process watcher releases or rebinds the old handle and returns to idle CPU, and separately assert no app-server subscription keeps polling the removed path. That should also help avoid fixing the already-visible #38109 retry class while leaving the main-process handle loop intact.

    Source checked on current main (c4017a87):

    • codex-rs/file-watcher/src/lib.rs
    • codex-rs/app-server/src/fs_watch.rs

    This does not identify the private Desktop/Chromium call site; it only narrows where the observed process-level evidence says to look.

  3. natqe commented on Oct 14, 2026

    @natqe

    Same hot loop, with a concrete trigger, the root cause, and a deterministic repro (Windows 11 Pro 25H2, build 26200; Codex desktop 26.1007.2314.0)

    Trigger: archiving a thread that owns a managed worktree. Archive cleanup removed the worktree directory while the desktop app still had fs.watch directory handles open on it and on its .git\worktrees\<name> admin directory.

    What I measured:

    • In the main ChatGPT.exe process, a Node worker thread named git (from GetThreadDescription) ran at about 100% of one core for about 11 hours (about 40,000 CPU-seconds).
    • About 37% of that was kernel time, with about 51,000 non-read/write I/O operations per second.
    • The process held three directory handles (access 0x100081, which is the libuv fs_event signature) that resolved to \\?\C:\$Extend\$Deleted\..., with DeletePending=True.
    • Only a full app restart stopped it.
    • Side effect: git status calls on the deleted worktree failed about 1.75 times per second. The watcher events probably re-triggered them; compare Review live query keeps polling deleted managed worktrees until app restart #38109.

    Root cause (libuv bundled with Node/Electron):

    • In libuv 1.52.x and earlier, src/win/fs-event.c → uv__process_fs_event_req handles a watched directory that is pending deletion like this: ReadDirectoryChangesW fails with ERROR_ACCESS_DENIED, libuv emits rename for the watched root, and then immediately queues ReadDirectoryChangesW again.
    • The new request fails at once, so this repeats until the application closes the watcher.

    Minimal repro (Node v24.5.0 / libuv 1.51.0, the same Windows machine):

    const fs = require('fs'), os = require('os'), path = require('path');
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'watch-repro-'));
    let n = 0;
    const w = fs.watch(dir, () => n++);
    setTimeout(() => {
      fs.rmdirSync(dir);
      const t = process.cpuUsage(), s = Date.now(), n0 = n;
      setTimeout(() => {
        const c = process.cpuUsage(t), ms = Date.now() - s;
        console.log({ eventsPerSec: Math.round((n - n0) * 1000 / ms), cores: ((c.user + c.system) / 1000 / ms).toFixed(2) });
        w.close();
      }, 2000);
    }, 300);

    Output: { eventsPerSec: 155398, cores: '0.97' }, mostly kernel time. Every event is a rename that carries the full \\?\C:\... path of the deleted root.

    Upstream status:

    Suggested fixes in the app (these do not depend on the libuv update):

    1. Close the fs.watch handles on a worktree root and on its .git\worktrees\<name> before archive cleanup or git worktree remove.
    2. Close a watcher when it receives rename for its own root, or an error. Even with libuv 1.53.0, the watcher stays open until the app closes it.
    3. Move to libuv 1.53.0 or later through Electron/Node when that becomes available.

    Related: #38109 (same trigger: live-query polling of deleted managed worktrees), #33401 (handles left on archived worktree directories).

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 workingperformancewindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions