perf(daemon): back off the monitor poll when idle

The daemon monitor thread drained output and then slept a fixed short
interval on every tick, waking several times a second for the life of
the node even long after startup, when the node emits almost nothing.
Stopping it also meant waiting out the current sleep before it saw the
stop flag.

- The monitor now waits on a condition variable instead of a plain
  sleep, so a stop request wakes it immediately rather than after the
  interval. The stop flag is set under the monitor's mutex so the wake
  cannot be lost.
- The poll interval stays fast for the first 30 s after the thread
  starts and for 3 s after any output, and otherwise doubles per idle
  tick up to a 500 ms cap. Any new output drops it straight back to
  fast, so responsiveness during startup, rescan and shutdown is
  unchanged while a quiet node is polled far less often. The interval
  choice is a pure function.

Tests cover the interval function across the warmup window, the
recent-data window, the reset on new output, the doubling, and the cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 06:26:13 -05:00
co-authored by Claude Opus 4.8
parent b407b8927a
commit 30f5243dd1
3 changed files with 95 additions and 13 deletions
+18
View File
@@ -14,6 +14,8 @@
#include <atomic>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <set>
#include <vector>
@@ -474,9 +476,23 @@ public:
* reader can tell a new crash from an error it has already reported. */
std::uint64_t getUnexpectedExitCount() const { return unexpected_exits_.load(); }
/** Monitor poll interval: stay `fast` for the first 30 s after the thread starts and for 3 s after any
* output; otherwise double per idle tick up to `cap`. Any output resets to `fast`. Pure. */
static std::chrono::milliseconds nextMonitorInterval(std::chrono::milliseconds cur, bool bytesRead,
std::chrono::milliseconds sinceStart, std::chrono::milliseconds sinceLastData,
std::chrono::milliseconds fast, std::chrono::milliseconds cap)
{
using ms = std::chrono::milliseconds;
if (bytesRead || sinceStart < ms(30000) || sinceLastData < ms(3000)) return fast;
const ms next = cur * 2;
return next > cap ? cap : (next < fast ? fast : next);
}
private:
void setState(State s, const std::string& message = "");
void monitorProcess();
void requestMonitorStop(); // set should_stop_ under monitor_mutex_ and wake the monitor
size_t outputTotalSnapshot() const; // output_total_ under output_mutex_ (monitor idle detection)
void drainOutput(); // read any pending stdout into process_output_
void appendOutput(const char* data, size_t len); // append + trim (caller holds output_mutex_)
bool startProcess(const std::string& binary_path, const std::vector<std::string>& args);
@@ -514,6 +530,8 @@ private:
std::thread monitor_thread_;
std::atomic<bool> should_stop_{false};
std::mutex monitor_mutex_; // pairs with monitor_cv_ so stop wakes the monitor at once
std::condition_variable monitor_cv_;
std::atomic<bool> stop_requested_{false}; // a stop was asked for: the next exit is not a crash
// Serializes start/beginStop/stop/endStop over monitor_thread_, should_stop_ and stop_requested_ (two
// threads joining one std::thread is undefined). Never taken by the monitor thread or the state callback.