// 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 #include #include #include #include #include #include #include namespace dragonx { namespace util { class BackgroundSampler { public: using Probe = std::function; 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 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 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 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 lk(mtx_); stopping_ = true; pending_ = nullptr; } cv_.notify_all(); if (thread_.joinable()) thread_.join(); } private: bool busyLocked() const { return running_ || static_cast(pending_); } void run() { for (;;) { Probe probe; std::uint64_t gen = 0; { std::unique_lock lk(mtx_); cv_.wait(lk, [this] { return stopping_ || static_cast(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 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 value_{0.0}; std::atomic have_{false}; }; } // namespace util } // namespace dragonx