perf(mining): sample daemon memory off the UI thread
The daemon's RSS was read on the UI thread: a scan of every /proc entry on Linux (1-7 ms), a forked ps on macOS (up to ~30 ms). The mining poll probed every 1-5 s on every tab, and the Mining tab's memory bar probed again every 1.5 s. util::BackgroundSampler now runs the probe on one lazily started thread and caches the result. getDaemonMemoryUsageMB() returns the last sample and asks for a new one at most every 3 s, and only while the Mining tab memory bar or the Peers tab Memory cell is drawn: nothing on other tabs or while minimized. EmbeddedDaemon::memoryProbe() hands the sampler a pid snapshot (on Windows the value read from the owned process handle), so nothing off-thread touches daemon state. The mining poll does no I/O any more; it carries the last sample. Both shutdown paths stop the sampler. Fixed along the way: an unreadable /proc made the directory iterator throw on the UI thread; the scan now just stops. User-visible: - the Peers tab Memory cell keeps updating during sync and shows an external daemon's memory (it showed the mining poll's value, which pauses during sync and covers only the embedded daemon); - the Mining tab memory bar refreshes every ~3 s (was 1.5 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -711,6 +711,7 @@ set(APP_HEADERS
|
||||
src/util/async_task_manager.h
|
||||
src/util/fetch_backoff.h
|
||||
src/util/coalescing_writer.h
|
||||
src/util/background_sampler.h
|
||||
src/util/amount_format.h
|
||||
src/util/base64.h
|
||||
src/util/single_instance.h
|
||||
|
||||
+23
-19
@@ -6178,26 +6178,28 @@ bool App::stopDaemonForBootstrap()
|
||||
|
||||
double App::getDaemonMemoryUsageMB() const
|
||||
{
|
||||
// The Mining tab reads this every frame, but the probe is expensive (Linux embedded/external paths
|
||||
// scan /proc; macOS forks a shell). Throttle to ~1.5s so a 60Hz caller doesn't hammer the OS.
|
||||
const double now = ImGui::GetTime();
|
||||
if (daemon_mem_probe_at_ > 0.0 && now - daemon_mem_probe_at_ < 1.5)
|
||||
return daemon_mem_cached_mb_;
|
||||
daemon_mem_probe_at_ = now;
|
||||
|
||||
double result = 0.0;
|
||||
// If we have an embedded daemon with a tracked process handle, use it
|
||||
// directly — more reliable than a process scan since we own the handle.
|
||||
if (daemon_controller_ && daemon_controller_->isRunning()) {
|
||||
double mb = daemon_controller_->memoryUsageMB();
|
||||
daemon_mem_diag_ = "embedded";
|
||||
result = (mb > 0.0) ? mb : util::Platform::getDaemonMemoryUsageMB();
|
||||
} else {
|
||||
daemon_mem_diag_ = "process scan";
|
||||
result = util::Platform::getDaemonMemoryUsageMB(); // external daemon
|
||||
// Never probes on the calling (UI) thread. The probe (a /proc scan on Linux, a forked `ps` on macOS, a
|
||||
// process snapshot for an external daemon) runs on daemon_mem_sampler_'s thread. This returns the last
|
||||
// completed sample (0 until the first lands, tens of ms after the first ask) and asks for a fresh one
|
||||
// when the last request is older than 3 s.
|
||||
constexpr auto kMaxAge = std::chrono::milliseconds(3000);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (daemon_mem_sampler_.due(now, kMaxAge)) {
|
||||
util::BackgroundSampler::Probe probe;
|
||||
if (daemon_controller_ && daemon_controller_->isRunning()) {
|
||||
// The embedded daemon we own: its tracked process group first, a process scan if that reads 0.
|
||||
daemon_mem_diag_ = "embedded";
|
||||
probe = [own = daemon_controller_->memoryProbe()]() {
|
||||
const double mb = own ? own() : 0.0;
|
||||
return mb > 0.0 ? mb : util::Platform::getDaemonMemoryUsageMB();
|
||||
};
|
||||
} else {
|
||||
daemon_mem_diag_ = "process scan";
|
||||
probe = []() { return util::Platform::getDaemonMemoryUsageMB(); }; // external daemon
|
||||
}
|
||||
daemon_mem_sampler_.requestIfStale(std::move(probe), now, kMaxAge);
|
||||
}
|
||||
daemon_mem_cached_mb_ = result;
|
||||
return result;
|
||||
return daemon_mem_sampler_.value();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -7401,6 +7403,7 @@ void App::shutdown()
|
||||
if (settings_) {
|
||||
settings_->save();
|
||||
}
|
||||
daemon_mem_sampler_.stop();
|
||||
if (daemon_controller_) {
|
||||
stopEmbeddedDaemon(StopMode::Fallback); // on the UI thread: bounded, never killed
|
||||
}
|
||||
@@ -7428,6 +7431,7 @@ void App::shutdown()
|
||||
if (http_worker_) {
|
||||
http_worker_->stop(); // http_cancel_ was set in beginShutdown(): an in-flight GET aborts within ~1 s
|
||||
}
|
||||
daemon_mem_sampler_.stop(); // joins a probe in flight (tens of ms at most)
|
||||
// Disconnect RPC after worker is fully stopped (safe — no curl in flight)
|
||||
if (rpc_) {
|
||||
rpc_->disconnect();
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "services/wallet_security_controller.h"
|
||||
#include "services/wallet_security_workflow.h"
|
||||
#include "util/async_task_manager.h"
|
||||
#include "util/background_sampler.h"
|
||||
#include "util/coalescing_writer.h"
|
||||
#include "util/fetch_backoff.h"
|
||||
#include "util/pool_stats_service.h"
|
||||
@@ -957,10 +958,11 @@ private:
|
||||
std::int64_t sb_unconf_key_ts_ = -1;
|
||||
std::size_t sb_unconf_key_n_ = 0;
|
||||
int sb_unconf_count_ = 0;
|
||||
// Daemon-memory probe is expensive (/proc scan on Linux, popen on macOS); throttle it to ~1.5s so the
|
||||
// Mining tab's per-frame read doesn't hammer the OS. See App::getDaemonMemoryUsageMB().
|
||||
mutable double daemon_mem_cached_mb_ = 0.0;
|
||||
mutable double daemon_mem_probe_at_ = 0.0;
|
||||
// Daemon-memory probe is expensive (/proc scan on Linux, popen on macOS): it runs on this sampler's own
|
||||
// thread, at most once per 3 s and only while a reader asks (Mining tab memory bar, Peers tab Memory cell).
|
||||
// See App::getDaemonMemoryUsageMB(). Mutable: that accessor is const. Probes capture values only, so any
|
||||
// member order is safe; shutdown() stops it.
|
||||
mutable util::BackgroundSampler daemon_mem_sampler_;
|
||||
|
||||
// ── Chat note buffer (BOTH variants) ────────────────────────────────────────────────────────
|
||||
// Each chat message is a shielded tx that spends a note; its change needs a few confirmations before
|
||||
|
||||
+3
-6
@@ -3021,12 +3021,9 @@ void App::refreshMiningInfo()
|
||||
if (state_.sync.syncing && !includeLocalHashrate) return;
|
||||
ui::NavPage tracePage = current_page_;
|
||||
|
||||
// Capture daemon memory outside (may be accessed on main thread). Only for a poll that runs: the probe
|
||||
// reads /proc (or the Windows process table).
|
||||
double daemonMemMb = 0.0;
|
||||
if (daemon_controller_) {
|
||||
daemonMemMb = daemon_controller_->memoryUsageMB();
|
||||
}
|
||||
// Daemon memory is sampled off the UI thread by getDaemonMemoryUsageMB() (Mining / Peers tab); the poll
|
||||
// only carries the last sample into state.mining.daemon_memory_mb (no I/O here).
|
||||
const double daemonMemMb = daemon_mem_sampler_.value();
|
||||
|
||||
auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Mining, *w,
|
||||
[this, rpc, daemonMemMb, doSlowRefresh, includeLocalHashrate, tracePage]() -> rpc::RPCWorker::MainCb {
|
||||
|
||||
@@ -99,6 +99,11 @@ double DaemonController::memoryUsageMB() const
|
||||
return daemon_ ? daemon_->getMemoryUsageMB() : 0.0;
|
||||
}
|
||||
|
||||
std::function<double()> DaemonController::memoryProbe() const
|
||||
{
|
||||
return daemon_ ? daemon_->memoryProbe() : std::function<double()>{};
|
||||
}
|
||||
|
||||
std::vector<std::string> DaemonController::recentLines(std::size_t count) const
|
||||
{
|
||||
return daemon_ ? daemon_->getRecentLines(count) : std::vector<std::string>{};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -106,6 +107,7 @@ public:
|
||||
int crashCount() const;
|
||||
int lastBlockHeight() const;
|
||||
double memoryUsageMB() const;
|
||||
std::function<double()> memoryProbe() const; // EmbeddedDaemon::memoryProbe(); empty without a daemon
|
||||
std::vector<std::string> recentLines(std::size_t count) const;
|
||||
std::string outputSince(std::size_t& offset) const;
|
||||
|
||||
|
||||
@@ -839,7 +839,7 @@ static DWORD findProcessByName(const char* name)
|
||||
double EmbeddedDaemon::getMemoryUsageMB() const
|
||||
{
|
||||
if (process_handle_ == nullptr) return 0.0;
|
||||
|
||||
|
||||
PROCESS_MEMORY_COUNTERS pmc;
|
||||
ZeroMemory(&pmc, sizeof(pmc));
|
||||
pmc.cb = sizeof(pmc);
|
||||
@@ -849,6 +849,16 @@ double EmbeddedDaemon::getMemoryUsageMB() const
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::function<double()> EmbeddedDaemon::memoryProbe() const
|
||||
{
|
||||
if (process_handle_ == nullptr) return {};
|
||||
// GetProcessMemoryInfo on the handle we own is one cheap call: read it here and hand back the value, so the
|
||||
// callable never touches process_handle_ (the monitor can close it). The expensive part on Windows, the
|
||||
// Toolhelp process scan App falls back to when this reads 0, runs on the sampler thread.
|
||||
const double mb = getMemoryUsageMB();
|
||||
return [mb]() { return mb; };
|
||||
}
|
||||
|
||||
void EmbeddedDaemon::drainOutput()
|
||||
{
|
||||
// Read new content from debug.log (tail-follow approach)
|
||||
@@ -1321,15 +1331,17 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
||||
return true;
|
||||
}
|
||||
|
||||
double EmbeddedDaemon::getMemoryUsageMB() const
|
||||
namespace {
|
||||
// Sum of the RSS (MB) of every process in process group `pgid`. The tracked pid is often a bash wrapper and the
|
||||
// real dragonxd a child in the same group (setpgid in startProcess). Runs on a sampler thread
|
||||
// (util::BackgroundSampler), so the /proc scan / `ps` fork no longer costs the UI anything.
|
||||
double sampleProcessGroupRssMB(pid_t pgid)
|
||||
{
|
||||
if (process_pid_ <= 0) return 0.0;
|
||||
|
||||
#ifdef __APPLE__
|
||||
// macOS: use ps to read RSS for the daemon process and its children
|
||||
// in the same process group.
|
||||
char cmd[128];
|
||||
snprintf(cmd, sizeof(cmd), "ps -o rss= -g %d 2>/dev/null", process_pid_);
|
||||
snprintf(cmd, sizeof(cmd), "ps -o rss= -g %d 2>/dev/null", static_cast<int>(pgid));
|
||||
FILE* fp = popen(cmd, "r");
|
||||
if (!fp) return 0.0;
|
||||
double total_rss_mb = 0.0;
|
||||
@@ -1341,20 +1353,17 @@ double EmbeddedDaemon::getMemoryUsageMB() const
|
||||
pclose(fp);
|
||||
return total_rss_mb;
|
||||
#else
|
||||
// Linux: The tracked PID is often a bash wrapper script; the real daemon
|
||||
// (dragonxd) is a child in the same process group. Sum VmRSS for every
|
||||
// process whose PGID matches our tracked PID.
|
||||
// Linux: sum VmRSS for every /proc/<pid> whose process group is pgid. The error_code overloads: an
|
||||
// unreadable /proc ends the scan instead of throwing (the old range-for constructor threw).
|
||||
double total_rss_mb = 0.0;
|
||||
|
||||
// Iterate /proc/<pid>/stat for all numeric entries
|
||||
for (const auto& entry : fs::directory_iterator("/proc")) {
|
||||
if (!entry.is_directory()) continue;
|
||||
const std::string name = entry.path().filename().string();
|
||||
if (name.empty() || !std::isdigit(name[0])) continue;
|
||||
std::error_code ec;
|
||||
for (fs::directory_iterator it("/proc", ec), end; !ec && it != end; it.increment(ec)) {
|
||||
const std::string name = it->path().filename().string();
|
||||
if (name.empty() || !std::isdigit(static_cast<unsigned char>(name[0]))) continue;
|
||||
|
||||
// Read process group from /proc/<pid>/stat (field 5 is pgrp)
|
||||
std::string statPath = entry.path().string() + "/stat";
|
||||
FILE* sf = fopen(statPath.c_str(), "r");
|
||||
const std::string base = it->path().string();
|
||||
FILE* sf = fopen((base + "/stat").c_str(), "r");
|
||||
if (!sf) continue;
|
||||
|
||||
char statLine[512];
|
||||
@@ -1372,11 +1381,10 @@ double EmbeddedDaemon::getMemoryUsageMB() const
|
||||
char state;
|
||||
int ppid;
|
||||
if (sscanf(p, " %c %d %d", &state, &ppid, &pgrp) != 3) continue;
|
||||
if (pgrp != process_pid_) continue;
|
||||
if (pgrp != pgid) continue;
|
||||
|
||||
// This process belongs to our group — read its VmRSS
|
||||
std::string statusPath = entry.path().string() + "/status";
|
||||
FILE* f = fopen(statusPath.c_str(), "r");
|
||||
FILE* f = fopen((base + "/status").c_str(), "r");
|
||||
if (!f) continue;
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
@@ -1389,10 +1397,23 @@ double EmbeddedDaemon::getMemoryUsageMB() const
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
return total_rss_mb;
|
||||
#endif // __APPLE__ / Linux
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::function<double()> EmbeddedDaemon::memoryProbe() const
|
||||
{
|
||||
const pid_t pid = process_pid_; // snapshot on the calling thread; the callable carries only this
|
||||
if (pid <= 0) return {};
|
||||
return [pid]() { return sampleProcessGroupRssMB(pid); };
|
||||
}
|
||||
|
||||
double EmbeddedDaemon::getMemoryUsageMB() const
|
||||
{
|
||||
const auto probe = memoryProbe();
|
||||
return probe ? probe() : 0.0;
|
||||
}
|
||||
|
||||
bool EmbeddedDaemon::isRunning() const
|
||||
{
|
||||
|
||||
@@ -128,8 +128,18 @@ public:
|
||||
/**
|
||||
* @brief Get daemon process memory usage (RSS) in MB
|
||||
* @return Memory usage in MB, or 0 if unavailable
|
||||
*
|
||||
* Synchronous (Linux scans /proc, macOS forks `ps`): UI code uses memoryProbe() through a
|
||||
* util::BackgroundSampler instead.
|
||||
*/
|
||||
double getMemoryUsageMB() const;
|
||||
|
||||
/**
|
||||
* @brief A self-contained RSS probe (MB) for another thread. Snapshots what it needs on the calling
|
||||
* thread (the pid; on Windows the value itself: one cheap call on the owned handle); the callable
|
||||
* touches no EmbeddedDaemon state. Empty when there is no process.
|
||||
*/
|
||||
std::function<double()> memoryProbe() const;
|
||||
|
||||
/**
|
||||
* @brief Get last error message
|
||||
|
||||
@@ -276,7 +276,9 @@ void RenderPeersTab(App* app)
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_version"),
|
||||
state.daemon_version > 0 ? buf : nullptr, OnSurface());
|
||||
|
||||
double memMb = state.mining.daemon_memory_mb;
|
||||
// The cached off-thread sample (refreshed every ~3 s while this tab or the Mining tab asks). Also
|
||||
// keeps updating during sync and shows an external daemon's memory (process scan).
|
||||
double memMb = app->getDaemonMemoryUsageMB();
|
||||
if (memMb > 0) {
|
||||
if (memMb >= 1024.0)
|
||||
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Runs a slow probe (a /proc scan, a forked `ps`) off the UI thread and caches its latest result as an atomic.
|
||||
// One lazily started thread, latest-wins, no main-thread callback. due / requestIfStale / reset / stop are for
|
||||
// one owner thread (the UI); value / hasSample are safe from anywhere. Header-only.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
class BackgroundSampler {
|
||||
public:
|
||||
using Probe = std::function<double()>;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
BackgroundSampler() = default;
|
||||
~BackgroundSampler() { stop(); }
|
||||
BackgroundSampler(const BackgroundSampler&) = delete;
|
||||
BackgroundSampler& operator=(const BackgroundSampler&) = delete;
|
||||
|
||||
// True when requestIfStale would accept a probe now (none pending/running, last request >= maxAge ago).
|
||||
bool due(Clock::time_point now, std::chrono::milliseconds maxAge) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_);
|
||||
return !stopping_ && !busyLocked() && (!requestedOnce_ || now - lastRequestAt_ >= maxAge);
|
||||
}
|
||||
|
||||
// Schedules `probe` unless one is pending/running or the previous request is younger than maxAge. The probe
|
||||
// runs on the sampler thread: it must capture everything it needs by value (no owner state).
|
||||
bool requestIfStale(Probe probe, Clock::time_point now, std::chrono::milliseconds maxAge)
|
||||
{
|
||||
if (!probe) return false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_);
|
||||
if (stopping_ || busyLocked()) return false;
|
||||
if (requestedOnce_ && now - lastRequestAt_ < maxAge) return false;
|
||||
lastRequestAt_ = now; // before starting the thread: a failed start is retried after maxAge
|
||||
requestedOnce_ = true;
|
||||
if (!thread_.joinable()) {
|
||||
try {
|
||||
thread_ = std::thread(&BackgroundSampler::run, this);
|
||||
} catch (...) {
|
||||
return false; // no thread (resource exhaustion): keep the cached value
|
||||
}
|
||||
}
|
||||
pending_ = std::move(probe);
|
||||
}
|
||||
cv_.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
double value() const { return value_.load(std::memory_order_relaxed); } // last completed sample (0 before)
|
||||
bool hasSample() const { return have_.load(std::memory_order_relaxed); }
|
||||
|
||||
// Forget the cached sample (the process changed): value()==0 and the next request is accepted at once.
|
||||
// A probe already running is discarded when it finishes.
|
||||
void reset()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_);
|
||||
++generation_;
|
||||
requestedOnce_ = false;
|
||||
value_.store(0.0, std::memory_order_relaxed);
|
||||
have_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Idempotent. Drops a pending probe and joins the thread (waits for one in flight: tens of ms).
|
||||
void stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_);
|
||||
stopping_ = true;
|
||||
pending_ = nullptr;
|
||||
}
|
||||
cv_.notify_all();
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
private:
|
||||
bool busyLocked() const { return running_ || static_cast<bool>(pending_); }
|
||||
|
||||
void run()
|
||||
{
|
||||
for (;;) {
|
||||
Probe probe;
|
||||
std::uint64_t gen = 0;
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(mtx_);
|
||||
cv_.wait(lk, [this] { return stopping_ || static_cast<bool>(pending_); });
|
||||
if (stopping_) return;
|
||||
probe = std::move(pending_);
|
||||
pending_ = nullptr;
|
||||
running_ = true;
|
||||
gen = generation_;
|
||||
}
|
||||
double v = 0.0;
|
||||
bool ok = false;
|
||||
try { v = probe(); ok = true; } catch (...) {}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx_);
|
||||
running_ = false;
|
||||
if (ok && gen == generation_) {
|
||||
value_.store(v, std::memory_order_relaxed);
|
||||
have_.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutable std::mutex mtx_;
|
||||
std::condition_variable cv_;
|
||||
std::thread thread_;
|
||||
Probe pending_;
|
||||
bool running_ = false;
|
||||
bool stopping_ = false;
|
||||
bool requestedOnce_ = false;
|
||||
Clock::time_point lastRequestAt_{};
|
||||
std::uint64_t generation_ = 0;
|
||||
std::atomic<double> value_{0.0};
|
||||
std::atomic<bool> have_{false};
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
@@ -71,6 +71,7 @@
|
||||
#include "util/pool_registry.h"
|
||||
#include "util/daemon_updater.h"
|
||||
#include "util/http_download.h"
|
||||
#include "util/background_sampler.h"
|
||||
#include "util/lite_server_probe.h"
|
||||
#include "util/wallet_file_probe.h"
|
||||
#include "wallet/lite_connection_service.h"
|
||||
@@ -3488,6 +3489,30 @@ void testPosixRequestedStopIsNotACrash()
|
||||
std::error_code ec; std::filesystem::remove(script, ec);
|
||||
}
|
||||
|
||||
// C103: EmbeddedDaemon::memoryProbe (POSIX: place after testPosixRequestedStopIsNotACrash, it uses
|
||||
// writeFakeDaemonScript).
|
||||
void testEmbeddedDaemonMemoryProbe()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
{
|
||||
EmbeddedDaemon idle;
|
||||
EXPECT_FALSE(static_cast<bool>(idle.memoryProbe())); // no process: empty probe
|
||||
EXPECT_NEAR(idle.getMemoryUsageMB(), 0.0, 1e-9);
|
||||
}
|
||||
const auto script = writeFakeDaemonScript("od_fake_daemon_mem.sh", "sleep 2\nexit 0\n");
|
||||
EmbeddedDaemon d;
|
||||
d.setSkipPortCheck(true);
|
||||
EXPECT_TRUE(d.start(script.string()));
|
||||
auto probe = d.memoryProbe();
|
||||
EXPECT_TRUE(static_cast<bool>(probe));
|
||||
double mb = 0.0;
|
||||
std::thread t([&probe, &mb] { mb = probe(); }); // runs off the creating thread, as in the app
|
||||
t.join();
|
||||
EXPECT_TRUE(mb > 0.0); // the bash stand-in's process group
|
||||
d.stop(5000);
|
||||
std::error_code ec; std::filesystem::remove(script, ec);
|
||||
}
|
||||
|
||||
// S4: SIGTERM is graceful on POSIX (dragonxd treats it like RPC stop), and stop() reaps the clean
|
||||
// exit it leads to at once instead of waiting out the 10s SIGKILL timer (~11s before the fix).
|
||||
void testPosixStopSigtermIsGraceful()
|
||||
@@ -4522,6 +4547,63 @@ void testLowestBlockSearch()
|
||||
EXPECT_TRUE(p == BlockProbe::Unknown);
|
||||
}
|
||||
|
||||
// C103: the sampler (pure threading, no OS probes).
|
||||
void testBackgroundSampler()
|
||||
{
|
||||
using dragonx::util::BackgroundSampler;
|
||||
using Clock = BackgroundSampler::Clock;
|
||||
using std::chrono::milliseconds;
|
||||
auto waitFor = [](const std::function<bool()>& cond) {
|
||||
for (int i = 0; i < 400 && !cond(); ++i) std::this_thread::sleep_for(milliseconds(5));
|
||||
return cond();
|
||||
};
|
||||
{ BackgroundSampler idle; EXPECT_TRUE(idle.due(Clock::now(), milliseconds(1000))); } // no thread: no hang
|
||||
|
||||
BackgroundSampler s;
|
||||
const auto t0 = Clock::now();
|
||||
std::atomic<int> calls{0};
|
||||
EXPECT_TRUE(s.requestIfStale([&calls] { ++calls; return 42.0; }, t0, milliseconds(1000)));
|
||||
EXPECT_TRUE(waitFor([&s] { return s.hasSample(); }));
|
||||
EXPECT_NEAR(s.value(), 42.0, 1e-9);
|
||||
EXPECT_EQ(calls.load(), 1);
|
||||
EXPECT_FALSE(s.due(t0 + milliseconds(500), milliseconds(1000))); // throttled
|
||||
EXPECT_FALSE(s.requestIfStale([&calls] { ++calls; return 1.0; }, t0 + milliseconds(500), milliseconds(1000)));
|
||||
EXPECT_TRUE(s.requestIfStale([&calls] { ++calls; return 7.0; }, t0 + milliseconds(1000), milliseconds(1000)));
|
||||
EXPECT_TRUE(waitFor([&s] { return s.value() == 7.0; }));
|
||||
EXPECT_EQ(calls.load(), 2);
|
||||
|
||||
// Coalescing: nothing is accepted while a probe runs; reset() discards its result.
|
||||
std::atomic<bool> release{false}, running{false};
|
||||
EXPECT_TRUE(s.requestIfStale([&release, &running] {
|
||||
running = true;
|
||||
while (!release) std::this_thread::sleep_for(milliseconds(1));
|
||||
return 9.0;
|
||||
}, t0 + milliseconds(5000), milliseconds(1000)));
|
||||
EXPECT_TRUE(waitFor([&running] { return running.load(); }));
|
||||
EXPECT_FALSE(s.due(t0 + milliseconds(60000), milliseconds(1000)));
|
||||
EXPECT_FALSE(s.requestIfStale([&calls] { ++calls; return 1.0; }, t0 + milliseconds(60000), milliseconds(1000)));
|
||||
s.reset();
|
||||
release = true;
|
||||
EXPECT_TRUE(waitFor([&s, t0] { return s.due(t0 + milliseconds(5000), milliseconds(1000)); })); // after reset: at once
|
||||
EXPECT_FALSE(s.hasSample());
|
||||
EXPECT_NEAR(s.value(), 0.0, 1e-9);
|
||||
EXPECT_EQ(calls.load(), 2);
|
||||
|
||||
// A throwing probe leaves no sample and the thread keeps working.
|
||||
EXPECT_TRUE(s.requestIfStale([]() -> double { throw std::runtime_error("probe failed"); },
|
||||
t0 + milliseconds(5000), milliseconds(1000)));
|
||||
EXPECT_TRUE(waitFor([&s, t0] { return s.due(t0 + milliseconds(7000), milliseconds(1000)); }));
|
||||
EXPECT_FALSE(s.hasSample());
|
||||
EXPECT_TRUE(s.requestIfStale([] { return 5.0; }, t0 + milliseconds(7000), milliseconds(1000)));
|
||||
EXPECT_TRUE(waitFor([&s] { return s.hasSample(); }));
|
||||
EXPECT_NEAR(s.value(), 5.0, 1e-9);
|
||||
|
||||
s.stop();
|
||||
s.stop(); // idempotent
|
||||
EXPECT_FALSE(s.requestIfStale([] { return 1.0; }, t0 + milliseconds(60000), milliseconds(1000)));
|
||||
EXPECT_NEAR(s.value(), 5.0, 1e-9); // last value kept
|
||||
}
|
||||
|
||||
void testConnectRetryBackoff()
|
||||
{
|
||||
using dragonx::rpc::ConnectRetryBackoff;
|
||||
@@ -12885,6 +12967,7 @@ int main()
|
||||
testFetchBackoff();
|
||||
testHttpGetCancel();
|
||||
testLowestBlockSearch();
|
||||
testBackgroundSampler();
|
||||
testNetworkRefreshService();
|
||||
testNetworkRefreshSnapshotHelpers();
|
||||
testParseDaemonRescanOutput();
|
||||
@@ -12955,6 +13038,7 @@ int main()
|
||||
testDaemonCrashDetected();
|
||||
testPosixStopReturnsAfterCleanExit();
|
||||
testPosixRequestedStopIsNotACrash();
|
||||
testEmbeddedDaemonMemoryProbe();
|
||||
testPosixStopSigtermIsGraceful();
|
||||
testPosixDestructorLeavesProcessRunning();
|
||||
testPosixStopSessionDetach();
|
||||
|
||||
Reference in New Issue
Block a user