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 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 17:42:30 -05:00
co-authored by Claude Opus 4.8
parent 2c9e99c022
commit f3f85d602d
5 changed files with 30 additions and 6 deletions
+2
View File
@@ -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;
+4 -2
View File
@@ -146,7 +146,8 @@ struct MiningInfo {
// History for chart
std::vector<double> hashrate_history; // Last N samples
static constexpr int MAX_HISTORY = 300; // 5 minutes at 1s intervals
std::vector<std::time_t> 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<std::string> log_lines;
@@ -263,7 +264,8 @@ struct PoolMiningState {
// Hashrate history for chart (mirrors MiningInfo::hashrate_history)
std::vector<double> hashrate_history;
static constexpr int MAX_HISTORY = 60; // 5 minutes at ~5s intervals
std::vector<std::time_t> 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<std::string> log_lines;
+2
View File
@@ -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());
}
}
+12 -4
View File
@@ -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<double>& 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<double>& chartHistory = s_pool_mode
? state.pool_mining.hashrate_history
: mining.hashrate_history;
const std::vector<std::time_t>& 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));
}
+10
View File
@@ -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<size_t>(1));
EXPECT_EQ(state.mining.hashrate_history_t.size(), static_cast<size_t>(1));
EXPECT_EQ(state.mining.hashrate_history_t[0], static_cast<std::time_t>(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<size_t>(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<int64_t>(2345));