The thread benchmark state machine was advanced from the Mining tab's render path, so it only progressed while that tab was visible and the window was on screen. Hiding the tab or minimizing the window froze the benchmark mid-sweep. - The benchmark now advances from the main update loop, ungated by connectivity, so it keeps running when the tab is hidden or the window is minimized. - Its clock is driven from a steady wall-clock reading rather than the render delta, and each step is clamped like the other logic timers so a suspend/resume cannot leap a benchmark phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
233 lines
8.1 KiB
C++
233 lines
8.1 KiB
C++
#include "mining_benchmark.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
float AdvanceBenchmarkClock(double& lastSec, double nowSec, float maxStep)
|
|
{
|
|
float dt = 0.0f;
|
|
if (lastSec >= 0.0) dt = static_cast<float>(std::min<double>(std::max(nowSec - lastSec, 0.0), maxStep));
|
|
lastSec = nowSec;
|
|
return dt;
|
|
}
|
|
|
|
void ThreadBenchmark::reset()
|
|
{
|
|
phase = Phase::Idle;
|
|
candidates.clear();
|
|
current_index = 0;
|
|
results.clear();
|
|
phase_timer = 0.0f;
|
|
prev_window_avg = 0.0;
|
|
window_sum = 0.0;
|
|
window_samples = 0;
|
|
window_timer = 0.0f;
|
|
consecutive_stable = 0;
|
|
measure_sum = 0.0;
|
|
measure_samples = 0;
|
|
optimal_threads = 0;
|
|
optimal_hashrate = 0.0;
|
|
was_pool_running = false;
|
|
prev_threads = 0;
|
|
total_warmup_secs = 0.0f;
|
|
}
|
|
|
|
void ThreadBenchmark::buildCandidates(int maxThreads)
|
|
{
|
|
candidates.clear();
|
|
int start = std::max(1, maxThreads / 2);
|
|
for (int threads = start; threads <= maxThreads; ++threads) {
|
|
candidates.push_back(threads);
|
|
}
|
|
}
|
|
|
|
float ThreadBenchmark::avgWarmupSecs() const
|
|
{
|
|
if (current_index > 0) {
|
|
return total_warmup_secs / static_cast<float>(current_index);
|
|
}
|
|
return (MIN_WARMUP_SECS + MAX_WARMUP_SECS) * 0.5f;
|
|
}
|
|
|
|
float ThreadBenchmark::perTestSecs() const
|
|
{
|
|
return avgWarmupSecs() + MEASURE_SECS;
|
|
}
|
|
|
|
float ThreadBenchmark::totalEstimatedSecs() const
|
|
{
|
|
int count = static_cast<int>(candidates.size());
|
|
if (count <= 0) return 0.0f;
|
|
float completedTime = total_warmup_secs
|
|
+ static_cast<float>(current_index) * (MEASURE_SECS + COOLDOWN_SECS);
|
|
int remaining = count - current_index;
|
|
float remainingTime = static_cast<float>(remaining) * (avgWarmupSecs() + MEASURE_SECS)
|
|
+ static_cast<float>(std::max(0, remaining - 1)) * COOLDOWN_SECS;
|
|
return completedTime + remainingTime;
|
|
}
|
|
|
|
float ThreadBenchmark::elapsedSecs() const
|
|
{
|
|
float completed = total_warmup_secs
|
|
+ static_cast<float>(current_index) * (MEASURE_SECS + COOLDOWN_SECS);
|
|
return completed + phase_timer;
|
|
}
|
|
|
|
float ThreadBenchmark::progress() const
|
|
{
|
|
float total = totalEstimatedSecs();
|
|
return (total > 0.0f) ? std::min(1.0f, elapsedSecs() / total) : 0.0f;
|
|
}
|
|
|
|
void ThreadBenchmark::resetStabilityTracking()
|
|
{
|
|
prev_window_avg = 0.0;
|
|
window_sum = 0.0;
|
|
window_samples = 0;
|
|
window_timer = 0.0f;
|
|
consecutive_stable = 0;
|
|
}
|
|
|
|
bool ThreadBenchmark::active() const
|
|
{
|
|
return phase != Phase::Idle && phase != Phase::Done;
|
|
}
|
|
|
|
ThreadBenchmarkCancel PlanThreadBenchmarkCancel(const ThreadBenchmark& benchmark)
|
|
{
|
|
ThreadBenchmarkCancel plan;
|
|
plan.restartThreads = benchmark.prev_threads;
|
|
plan.restartMiner = benchmark.was_pool_running && benchmark.prev_threads > 0;
|
|
return plan;
|
|
}
|
|
|
|
ThreadBenchmarkUpdate AdvanceThreadBenchmark(ThreadBenchmark& benchmark,
|
|
float deltaSeconds,
|
|
double poolHashrate10s)
|
|
{
|
|
ThreadBenchmarkUpdate update;
|
|
if (!benchmark.active()) return update;
|
|
|
|
benchmark.phase_timer += deltaSeconds;
|
|
|
|
switch (benchmark.phase) {
|
|
case ThreadBenchmark::Phase::Starting:
|
|
if (benchmark.current_index < static_cast<int>(benchmark.candidates.size())) {
|
|
int threads = benchmark.candidates[benchmark.current_index];
|
|
update.stopPoolMining = true;
|
|
update.startPoolMining = true;
|
|
update.startThreads = threads;
|
|
benchmark.phase = ThreadBenchmark::Phase::WarmingUp;
|
|
benchmark.phase_timer = 0.0f;
|
|
benchmark.resetStabilityTracking();
|
|
benchmark.measure_sum = 0.0;
|
|
benchmark.measure_samples = 0;
|
|
} else {
|
|
benchmark.phase = ThreadBenchmark::Phase::Done;
|
|
}
|
|
break;
|
|
|
|
case ThreadBenchmark::Phase::WarmingUp: {
|
|
bool pastMin = benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS;
|
|
bool pastMax = benchmark.phase_timer >= ThreadBenchmark::MAX_WARMUP_SECS;
|
|
|
|
if (poolHashrate10s > 0.0) {
|
|
benchmark.window_sum += poolHashrate10s;
|
|
benchmark.window_samples++;
|
|
}
|
|
benchmark.window_timer += deltaSeconds;
|
|
|
|
bool stable = false;
|
|
if (pastMin && benchmark.window_timer >= ThreadBenchmark::STABILITY_WINDOW_SECS &&
|
|
benchmark.window_samples > 0) {
|
|
double currentAverage = benchmark.window_sum / benchmark.window_samples;
|
|
if (benchmark.prev_window_avg > 0.0) {
|
|
double change = std::abs(currentAverage - benchmark.prev_window_avg)
|
|
/ benchmark.prev_window_avg;
|
|
if (change < ThreadBenchmark::STABILITY_THRESHOLD)
|
|
benchmark.consecutive_stable++;
|
|
else
|
|
benchmark.consecutive_stable = 0;
|
|
if (benchmark.consecutive_stable >= ThreadBenchmark::STABLE_WINDOWS_NEEDED)
|
|
stable = true;
|
|
}
|
|
benchmark.prev_window_avg = currentAverage;
|
|
benchmark.window_sum = 0.0;
|
|
benchmark.window_samples = 0;
|
|
benchmark.window_timer = 0.0f;
|
|
}
|
|
|
|
if (stable || pastMax) {
|
|
benchmark.total_warmup_secs += benchmark.phase_timer;
|
|
benchmark.phase = ThreadBenchmark::Phase::Measuring;
|
|
benchmark.phase_timer = 0.0f;
|
|
benchmark.measure_sum = 0.0;
|
|
benchmark.measure_samples = 0;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ThreadBenchmark::Phase::Measuring:
|
|
if (poolHashrate10s > 0.0) {
|
|
benchmark.measure_sum += poolHashrate10s;
|
|
benchmark.measure_samples++;
|
|
}
|
|
if (benchmark.phase_timer >= ThreadBenchmark::MEASURE_SECS) {
|
|
int threads = benchmark.candidates[benchmark.current_index];
|
|
double average = (benchmark.measure_samples > 0)
|
|
? benchmark.measure_sum / benchmark.measure_samples
|
|
: 0.0;
|
|
benchmark.results.push_back({threads, average});
|
|
if (average > benchmark.optimal_hashrate) {
|
|
benchmark.optimal_hashrate = average;
|
|
benchmark.optimal_threads = threads;
|
|
}
|
|
benchmark.phase = ThreadBenchmark::Phase::Advancing;
|
|
benchmark.phase_timer = 0.0f;
|
|
}
|
|
break;
|
|
|
|
case ThreadBenchmark::Phase::Advancing:
|
|
update.stopPoolMining = true;
|
|
benchmark.current_index++;
|
|
if (benchmark.current_index < static_cast<int>(benchmark.candidates.size())) {
|
|
benchmark.phase = ThreadBenchmark::Phase::CoolingDown;
|
|
benchmark.phase_timer = 0.0f;
|
|
} else {
|
|
benchmark.phase = ThreadBenchmark::Phase::Done;
|
|
if (benchmark.optimal_threads > 0) {
|
|
update.saveOptimalThreads = true;
|
|
update.optimalThreads = benchmark.optimal_threads;
|
|
if (benchmark.was_pool_running) {
|
|
update.startPoolMining = true;
|
|
update.startThreads = benchmark.optimal_threads;
|
|
}
|
|
} else {
|
|
// No candidate produced a nonzero hashrate (e.g. a pool that never
|
|
// reported a rate). Don't finish silently — flag the run as
|
|
// inconclusive so the caller can surface it (keeps this core pure).
|
|
update.inconclusive = true;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case ThreadBenchmark::Phase::CoolingDown:
|
|
if (benchmark.phase_timer >= ThreadBenchmark::COOLDOWN_SECS) {
|
|
benchmark.phase = ThreadBenchmark::Phase::Starting;
|
|
benchmark.phase_timer = 0.0f;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return update;
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|