Skip to content

perf: stop the 5s active-sessions poll from blocking the event loop - #289

Open
vakovalskii wants to merge 1 commit into
mainfrom
perf/unblock-active-poll
Open

perf: stop the 5s active-sessions poll from blocking the event loop#289
vakovalskii wants to merge 1 commit into
mainfrom
perf/unblock-active-poll

Conversation

@vakovalskii

Copy link
Copy Markdown
Owner

Второй из трёх источников заморозки терминала, которые мы замерили. #287 (в RC #284) снял индекс поиска — этот про опрос активных сессий.

Проблема

getActiveSessions() обслуживает GET /api/active, который дашборд опрашивает каждые 5 секунд. Для Qwen-агентов он звал findQwenSessionByPid(), а тот выполнял

execSync(`lsof -a -p ${pid} -Fn 2>/dev/null`, { timeout: 2000 })

— синхронно, на каждый pid, прямо на этом пути.

Сервер, HTTP API и WebSocket браузерного терминала живут в одном процессе и одном event loop. Значит ввод в терминале замирал до двух секунд на каждого запущенного агента, каждые пять секунд. В 7.15.0 остальные опросы ps/lsof вынесли с цикла, а этот пропустили.

Решение

lsof сохранён — это единственный сигнал, который отличает две сессии в одной папке (сопоставление по cwd тут бессильно). Убрана именно блокировка:

  • вызов идёт через _execFileAsync вне цикла;
  • вызывающий код читает кеш по pid с TTL 30 с (агент держит свой транскрипт открытым, чаще незачем);
  • до первого обновления работает уже существующий запасной путь по cwd — тот же ответ, что дал бы lsof для одиночной сессии в папке;
  • параллельные обновления по одному pid схлопываются, кеш ограничен по размеру (pid'ы переиспользуются);
  • заодно argv-форма вместо строки для шелла — pid больше не подставляется в команду.

Про измерения — важная оговорка

Замерять это временем бесполезно: на машине без запущенного Qwen-агента горячий цикл не выполняется вообще, и любой замер выходит зелёным независимо от кода. Я сам на этом обжёгся раньше в этой же функции.

Поэтому тест проверяет свойство структурно, и я убедился, что он не пустой: временно вернул execSync — тест упал (4 passed / 1 failed), откатил — снова 5/5.

Проверка

  • node --test test/*.test.js261 passed / 0 failed / 2 skipped
  • Новый test/active-poll-nonblocking.test.js — 5 проверок: нет синхронных вызовов ни в findQwenSessionByPid, ни в getActiveSessions; lsof асинхронный и в argv-форме; дедупликация обновлений; ограниченность кеша

Что остаётся

Третий источник — loadSessions() (648 мс с прогретым кешем, 11.9 с на холодную). Его правку намеренно не тащу сюда: она требует поэтапной пересборки в духе того, что #287 сделал для индекса поиска, и опирается на его помощники. Сделаю после того, как RC #284 вольётся, чтобы не разводить конфликты в data.js.

🤖 Generated with Claude Code

getActiveSessions() backs GET /api/active, which the dashboard polls every 5s.
For Qwen agents it called findQwenSessionByPid(), which ran
execSync('lsof -a -p <pid> -Fn') — with a 2000ms timeout, once per pid — inline.

The server, the HTTP API and the browser-terminal WebSocket share one process
and one event loop, so that stalled the pty data pump: typing in the terminal
froze for up to two seconds per running agent, every five seconds. (7.15.0 moved
the other ps/lsof polls off the loop; this one was missed.)

lsof is kept — it is the only signal that can tell two sessions in the same
folder apart — but it now runs through _execFileAsync off the loop and callers
read a 30s per-pid cache, falling back to the existing cwd match until the first
refresh lands. Refreshes are de-duplicated per pid and the cache is bounded.

Also switches to argv form, so the pid is no longer interpolated into a shell
command string.

Note the measurement trap: on a machine with no running Qwen agent the hot loop
never executes, so timing this looks green either way. The added test asserts
the property structurally instead, and was verified to fail when execSync is
reintroduced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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