Skip to content

fix: reduce CPU usage from PTY URL parsing and file watchers#566

Open
EricBlanquer wants to merge 1 commit intositeboon:mainfrom
EricBlanquer:fix/cpu-leak-watchers-url-parsing
Open

fix: reduce CPU usage from PTY URL parsing and file watchers#566
EricBlanquer wants to merge 1 commit intositeboon:mainfrom
EricBlanquer:fix/cpu-leak-watchers-url-parsing

Conversation

@EricBlanquer
Copy link
Copy Markdown
Contributor

@EricBlanquer EricBlanquer commented Mar 21, 2026

Summary

  • Throttle URL detection in PTY onData handler: extractUrlsFromText() (O(n²) regex on large buffer) was called on every data chunk — now throttled to once every 2s, with immediate execution preserved for auto-open triggers
  • Remove chokidar awaitWriteFinish polling: was stat()-ing every 50ms on ~2000 watched files; reduce depth from 10 to 3
  • Increase watcher debounce from 300ms to 2s to reduce heavy getProjects() scan frequency
  • Reduce URL buffer from 32KB to 8KB

Root cause

After running for several hours, node server/index.js reached 94% CPU / 574MB RAM. Three compounding causes:

  1. extractUrlsFromText() ran a nested O(n²) loop + regex on a 32KB buffer on every PTY chunk (hundreds/sec during active Claude sessions)
  2. chokidar with awaitWriteFinish: { pollInterval: 50 } polled ~2000 files every 50ms
  3. getProjects() (heavy directory scan) triggered every 300ms by file changes

After fix: ~12% CPU / 100MB stable after 5 min, trending to idle.

Summary by CodeRabbit

  • Performance Adjustments
    • Less frequent file-change refreshes and reduced directory scanning to cut CPU and UI flapping.
    • URL detection now batches and throttles parsing to lower resource use while still promptly opening important links.
  • Reliability Fixes
    • More robust URL auto-open logic to avoid duplicate detections and prefer the best link.
    • Session messages are sent only to active, ready connections to reduce delivery errors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

Increased file-watcher debounce and reduced chokidar scan depth; tightened shell URL parse buffer and added a per-shell throttled URL detection flow that immediately handles auto-open cases, deduplicates/normalizes URLs, and resolves the active WebSocket from ptySessionsMap before sending auth_url.

Changes

Cohort / File(s) Summary
File watcher config
server/index.js
Changed WATCHER_DEBOUNCE_MS 300 → 2000; reduced chokidar depth 10 → 3; removed awaitWriteFinish options.
Shell URL detection & emit flow
server/index.js
Reduced SHELL_URL_PARSE_BUFFER_LIMIT 32768 → 8192; added URL_PARSE_THROTTLE_MS = 2000. Replaced announcedAuthUrls Set with a Map storing per-URL autoOpen. Introduced per-shell urlParseTimer/urlParsePending, runUrlDetection(forceAutoOpen) to normalize/dedupe (prefer longest) and emit auth_url, immediately on auto-open triggers or throttled otherwise. Changed sends to look up current socket via ptySessionsMap and require WebSocket.OPEN before sending.

Sequence Diagram(s)

sequenceDiagram
  participant Shell as Shell Process
  participant Detector as URL Detector
  participant Timer as Throttle Timer
  participant Map as ptySessionsMap
  participant WS as WebSocket (client)

  Shell->>Detector: output chunk
  Detector->>Detector: append to urlDetectionBuffer
  alt shouldAutoOpenUrlFromOutput == true
    Detector->>Detector: runUrlDetection(forceAutoOpen=true)
  else not auto-open
    Detector->>Timer: start/coalesce urlParseTimer (2000ms)
    Timer-->>Detector: timer fires
    Detector->>Detector: runUrlDetection(forceAutoOpen=false)
  end

  Detector->>Detector: normalize & dedupe URLs (prefer longest)
  Detector->>Map: lookup session by ptySessionKey
  Map-->>Detector: session (may be undefined)
  alt session exists & WebSocket.OPEN
    Detector->>WS: send `auth_url` (autoOpen true/false)
  else no open socket
    Detector->>Detector: skip send
  end
Loading

Poem

🐰 I nibble logs and watch the stream,
I buffer hops and guard each beam.
Some links spring forth and open fast,
Others wait — I throttle last.
Sockets ready — then I leap!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: reducing CPU usage through PTY URL parsing and file watcher optimizations, which is precisely what the changeset addresses.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/index.js`:
- Around line 1863-1889: The code currently adds only the URL string to
announcedAuthUrls so calling emitAuthUrl(url, true) after emitAuthUrl(url,
false) is skipped; change the logic so autoOpen can override a prior
non-autoOpen announcement: update announcedAuthUrls to track per-URL whether
autoOpen has been sent (e.g., map URL -> boolean) and modify emitAuthUrl to send
if the URL is new OR if autoOpen is true and the stored value is false, then set
the stored value to true after sending; reference emitAuthUrl,
announcedAuthUrls, dedupedDetectedUrls and forceAutoOpen when locating the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f692ce5f-9a04-4b05-a8ca-a528a965bd07

📥 Commits

Reviewing files that changed from the base of the PR and between 08a6653 and 46fb3ac.

📒 Files selected for processing (1)
  • server/index.js

@EricBlanquer EricBlanquer force-pushed the fix/cpu-leak-watchers-url-parsing branch from 46fb3ac to a81eb1d Compare March 21, 2026 04:35
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/index.js`:
- Around line 1894-1907: The throttle timer is not cleared when
shouldAutoOpenUrlFromOutput(cleanChunk) causes runUrlDetection(true) to run
immediately, so any existing urlParseTimer can still fire later; update the
branch where runUrlDetection(true) is called to clear the existing timer
(clearTimeout(urlParseTimer); urlParseTimer = null) and reset urlParsePending as
appropriate so the pending callback won't run again after URL_PARSE_THROTTLE_MS.
Modify the logic around shouldAutoOpenUrlFromOutput, runUrlDetection,
urlParseTimer, urlParsePending (and related emitAuthUrl behavior) to ensure the
pending timer is cancelled before immediate execution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d16d3500-b3a8-4ec2-be8b-9d168b8b1ddd

📥 Commits

Reviewing files that changed from the base of the PR and between 46fb3ac and a81eb1d.

📒 Files selected for processing (1)
  • server/index.js

- Throttle extractUrlsFromText() to run at most every 2s instead of on
  every PTY data chunk (O(n²) regex on 32KB buffer was called hundreds
  of times per second during active sessions)
- Remove chokidar awaitWriteFinish polling (stat every 50ms on ~2000
  files), reduce watch depth from 10 to 3
- Increase watcher debounce from 300ms to 2s to reduce getProjects()
  scan frequency
- Reduce URL detection buffer from 32KB to 8KB

Before: 94% CPU / 574MB after ~8h. After: ~12% CPU / 100MB stable.
@EricBlanquer EricBlanquer force-pushed the fix/cpu-leak-watchers-url-parsing branch from a81eb1d to ac904c5 Compare March 21, 2026 04:56
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/index.js (1)

1976-1993: Consider clearing urlParseTimer on WebSocket close.

When the shell client disconnects, urlParseTimer may still be pending. While the timer callback safely skips sending (due to ws.readyState check), clearing it explicitly would be slightly cleaner.

🔧 Optional cleanup
     ws.on('close', () => {
         console.log('🔌 Shell client disconnected');
+
+        if (urlParseTimer) {
+            clearTimeout(urlParseTimer);
+            urlParseTimer = null;
+        }

         if (ptySessionKey) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/index.js` around lines 1976 - 1993, On WebSocket close, clear any
pending urlParseTimer to avoid leaving a dangling timer: inside the
ws.on('close' ...) handler (the block that references ptySessionKey,
ptySessionsMap, session.timeoutId and PTY_SESSION_TIMEOUT) check if
urlParseTimer is set, call clearTimeout(urlParseTimer) and set urlParseTimer =
null so the timer is explicitly cancelled when the shell client disconnects.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/index.js`:
- Around line 1976-1993: On WebSocket close, clear any pending urlParseTimer to
avoid leaving a dangling timer: inside the ws.on('close' ...) handler (the block
that references ptySessionKey, ptySessionsMap, session.timeoutId and
PTY_SESSION_TIMEOUT) check if urlParseTimer is set, call
clearTimeout(urlParseTimer) and set urlParseTimer = null so the timer is
explicitly cancelled when the shell client disconnects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7cfb99ea-3e46-4485-a1d6-e07422aeb241

📥 Commits

Reviewing files that changed from the base of the PR and between a81eb1d and ac904c5.

📒 Files selected for processing (1)
  • server/index.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant