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:
@@ -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