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:
@@ -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
|
||||
Reference in New Issue
Block a user