Skip to content

perf: cut idle CPU from leaked watchers and unconditional polling - #35

Merged
JeanBaptisteRenard merged 1 commit into
mainfrom
perf/idle-cpu-watchers-polling
Jun 2, 2026
Merged

perf: cut idle CPU from leaked watchers and unconditional polling#35
JeanBaptisteRenard merged 1 commit into
mainfrom
perf/idle-cpu-watchers-polling

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown
Collaborator

Problem

The main process consumed ~53% CPU continuously while idle (observed after a 5h+ session), with renderer processes adding ~33%/~25%.

Root causes & fixes

1. main.jsfs.watchFile stat-polling + watcher leak (main fix)

Each subagent live-tail watcher used fs.watchFile, which stat-polls the file once per second, per watcher, for the lifetime of the process. Teardown via fs.unwatchFile(path) was fragile and watchers could accumulate across a long session.

  • → event-driven fs.watch + 300ms debounce (mirrors the existing projects watcher)
  • → per-watcher teardown() closure called from both stop-subagent-watch and the window-closed handler — watchers can no longer leak
  • → 10s poll fallback only if fs.watch fails to attach

2. public/app.js — unconditional 3s polling

pollActiveSessions ran every 3s (IPC + full-sidebar querySelectorAll) even with zero running sessions.

  • → adaptive cadence: 3s while sessions run, 30s when idle; in-renderer starts re-arm the fast poll immediately, 30s floor still catches externally-started sessions
  • → 30s timeago interval now no-ops when nothing is active

3. schedule-runner.js — re-reading all JSONLs every 60s

scanSchedules read 4KB of every project JSONL every minute just to extract projectPath.

  • → resolve from the cache_meta SQLite cache (getAllFolderMeta), falling back to the JSONL read only when a folder is genuinely uncached

Testing

  • eslint clean (no new errors)
  • existing node --test suite: 119 pass / 0 fail, unchanged
  • behavior-preserving: same session detection, same schedules/triggers

The main process pegged ~53% CPU continuously while idle. Three causes:

- main.js: each subagent live-tail watcher used fs.watchFile, which
  stat-polls the file once per second per watcher, forever. Teardown
  via fs.unwatchFile(path) was fragile and watchers could accumulate
  across a long app session. Switch to event-driven fs.watch + 300ms
  debounce, and store a per-watcher teardown() closure called from both
  stop-subagent-watch and the window-closed handler so nothing leaks.
  Falls back to a 10s poll only if fs.watch fails to attach.

- public/app.js: pollActiveSessions ran every 3s unconditionally (IPC +
  full-sidebar querySelectorAll) even with zero running sessions. Poll
  adaptively: 3s while sessions run, 30s when idle. In-renderer session
  starts re-arm the fast cadence immediately; the 30s floor still catches
  externally-started sessions. The 30s timeago interval now no-ops when
  nothing is active.

- schedule-runner.js: scanSchedules re-read 4KB of every project JSONL
  every 60s just to extract projectPath. Resolve it from the cache_meta
  SQLite cache (getAllFolderMeta) instead, falling back to the JSONL read
  only when a folder is genuinely uncached.

Lint clean; existing 119-test suite passes unchanged.
@JeanBaptisteRenard
JeanBaptisteRenard merged commit 4724527 into main Jun 2, 2026
7 checks passed
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