From f3f85d602de517c2f517d53ab4baf359271524ae Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 13 Oct 2026 17:42:30 -0500 Subject: [PATCH] fix(mining): stamp hashrate history and label the chart by real time The hashrate sparkline guessed the age of its oldest sample from the sample count, labelling the left edge "5m ago", "1m ago" or "start" regardless of how fast samples actually arrived. Solo and pool samples come in at different intervals, and a slow or stalled poll made those labels wrong. - Each hashrate sample now carries a wall-clock timestamp, kept in lockstep with the value as the history fills and rolls over, for both the solo and pool histories. - The chart's left-edge label is derived from the oldest sample's actual timestamp, falling back to "start" only when no stamp is available. Tests: a mining refresh records a timestamp per sample, and the timestamp history stays the same length as the value history once the history is capped. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 2 ++ src/data/wallet_state.h | 6 ++++-- src/services/network_refresh_service.cpp | 2 ++ src/ui/windows/mining_stats.cpp | 16 ++++++++++++---- tests/test_phase4.cpp | 10 ++++++++++ 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 0143d71..45a8658 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1271,8 +1271,10 @@ void App::update() // Record hashrate sample for the chart ps.hashrate_history.push_back(ps.hashrate_10s); + ps.hashrate_history_t.push_back(std::time(nullptr)); if (ps.hashrate_history.size() > PoolMiningState::MAX_HISTORY) { ps.hashrate_history.erase(ps.hashrate_history.begin()); + ps.hashrate_history_t.erase(ps.hashrate_history_t.begin()); } } else if (xmrig_manager_ && !xmrig_manager_->isRunning()) { state_.pool_mining.xmrig_running = false; diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index 79f20d9..4f87016 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -146,7 +146,8 @@ struct MiningInfo { // History for chart std::vector hashrate_history; // Last N samples - static constexpr int MAX_HISTORY = 300; // 5 minutes at 1s intervals + std::vector hashrate_history_t; // wall-clock stamp per sample (lockstep with hashrate_history) + static constexpr int MAX_HISTORY = 300; // 5 minutes at ~1s intervals // Recent daemon log lines for the mining log panel std::vector log_lines; @@ -263,7 +264,8 @@ struct PoolMiningState { // Hashrate history for chart (mirrors MiningInfo::hashrate_history) std::vector hashrate_history; - static constexpr int MAX_HISTORY = 60; // 5 minutes at ~5s intervals + std::vector hashrate_history_t; // wall-clock stamp per sample (lockstep with hashrate_history) + static constexpr int MAX_HISTORY = 60; // ~2 min at the ~2s pool poll // Recent log lines for the log panel std::vector log_lines; diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index c5a4068..8b235a9 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -1844,8 +1844,10 @@ void NetworkRefreshService::applyMiningRefreshResult(WalletState& state, if (result.localHashrate) { state.mining.localHashrate = *result.localHashrate; state.mining.hashrate_history.push_back(state.mining.localHashrate); + state.mining.hashrate_history_t.push_back(updatedAt); if (state.mining.hashrate_history.size() > MiningInfo::MAX_HISTORY) { state.mining.hashrate_history.erase(state.mining.hashrate_history.begin()); + state.mining.hashrate_history_t.erase(state.mining.hashrate_history_t.begin()); } } diff --git a/src/ui/windows/mining_stats.cpp b/src/ui/windows/mining_stats.cpp index 38f75a7..1987a55 100644 --- a/src/ui/windows/mining_stats.cpp +++ b/src/ui/windows/mining_stats.cpp @@ -9,6 +9,7 @@ #include "../../config/settings.h" #include "../../util/i18n.h" #include "../../util/pool_registry.h" +#include "../../util/text_format.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" @@ -40,6 +41,7 @@ static bool s_show_solo_log = false; // --------------------------------------------------------------------------- static void DrawHashrateSparkline(ImDrawList* dl, ImVec2 regionMin, ImVec2 regionMax, const std::vector& chartHistory, + std::time_t oldestSampleT, // 0 when unknown ImFont* capFont, float dp) { if (chartHistory.size() < 2) return; @@ -116,10 +118,11 @@ static void DrawHashrateSparkline(ImDrawList* dl, ImVec2 regionMin, ImVec2 regio dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotBottom + 4 * dp), OnSurfaceDisabled(), yMinStr.c_str()); // X-axis labels + std::string agoLbl = (oldestSampleT > 0) ? dragonx::util::formatTimeAgoShort(oldestSampleT) + : std::string(TR("mining_chart_start")); dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft, chartBot - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), - chartHistory.size() >= 300 ? TR("mining_chart_5m_ago") : - chartHistory.size() >= 60 ? TR("mining_chart_1m_ago") : TR("mining_chart_start")); + agoLbl.c_str()); std::string nowLbl = TR("mining_chart_now"); ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str()); dl->AddText(capFont, capFont->LegacySize, @@ -374,6 +377,11 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining, const std::vector& chartHistory = s_pool_mode ? state.pool_mining.hashrate_history : mining.hashrate_history; + const std::vector& chartHistoryT = s_pool_mode + ? state.pool_mining.hashrate_history_t + : mining.hashrate_history_t; + const std::time_t oldestT = (chartHistoryT.size() == chartHistory.size() && !chartHistoryT.empty()) + ? chartHistoryT.front() : 0; bool hasChartContent = chartHistory.size() >= 2; float statRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm(); @@ -432,7 +440,7 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining, DrawHashrateSparkline(dl, ImVec2(chartBandX, rightMin.y + statRowH * 0.5f), ImVec2(chartBandX + chartBandW, rightMax.y), - chartHistory, capFont, dp); + chartHistory, oldestT, capFont, dp); } else { const char* hint = TR("mining_chart_start"); ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint); @@ -542,7 +550,7 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining, DrawHashrateSparkline(dl, ImVec2(cardMin.x + pad, cardMin.y + statRowH), ImVec2(cardMax.x - pad, cardMax.y), - chartHistory, capFont, dp); + chartHistory, oldestT, capFont, dp); } ImGui::Dummy(ImVec2(availWidth, totalCardH)); } diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index e093b35..e49e5c5 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -2612,6 +2612,16 @@ void testNetworkRefreshResultModels() EXPECT_EQ(state.mining.genproclimit, 4); EXPECT_NEAR(state.mining.localHashrate, 45.0, 0.0001); EXPECT_EQ(state.mining.hashrate_history.size(), static_cast(1)); + EXPECT_EQ(state.mining.hashrate_history_t.size(), static_cast(1)); + EXPECT_EQ(state.mining.hashrate_history_t[0], static_cast(2345)); + { + auto capState = state; + for (int i = 0; i < dragonx::MiningInfo::MAX_HISTORY + 20; ++i) { + Refresh::applyMiningRefreshResult(capState, mining, 3000 + i); + } + EXPECT_EQ(capState.mining.hashrate_history.size(), static_cast(dragonx::MiningInfo::MAX_HISTORY)); + EXPECT_EQ(capState.mining.hashrate_history_t.size(), capState.mining.hashrate_history.size()); + } EXPECT_NEAR(state.mining.networkHashrate, 900.0, 0.0001); EXPECT_NEAR(state.mining.daemon_memory_mb, 64.0, 0.0001); EXPECT_EQ(state.last_mining_update, static_cast(2345));