Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "mining_tab.h"
|
||||
#include "mining_benchmark.h"
|
||||
#include "mining_tab_helpers.h"
|
||||
#include "mining_pool_panel.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../config/version.h"
|
||||
@@ -43,130 +46,10 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
|
||||
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
|
||||
static int s_earnings_filter = 0;
|
||||
|
||||
// Thread benchmark state
|
||||
struct ThreadBenchmark {
|
||||
enum class Phase { Idle, Starting, WarmingUp, Measuring, Advancing, CoolingDown, Done };
|
||||
Phase phase = Phase::Idle;
|
||||
|
||||
std::vector<int> candidates;
|
||||
int current_index = 0;
|
||||
|
||||
struct Result {
|
||||
int threads;
|
||||
double hashrate;
|
||||
};
|
||||
std::vector<Result> results;
|
||||
|
||||
float phase_timer = 0.0f;
|
||||
|
||||
// Warmup: wait at least MIN then check for hashrate stability; cap at MAX.
|
||||
// Laptops need 90s+ for thermal throttling to fully manifest.
|
||||
static constexpr float MIN_WARMUP_SECS = 90.0f;
|
||||
static constexpr float MAX_WARMUP_SECS = 300.0f;
|
||||
static constexpr float MEASURE_SECS = 30.0f;
|
||||
static constexpr float COOLDOWN_SECS = 5.0f;
|
||||
|
||||
// Stability detection — compare rolling 10s hashrate windows.
|
||||
// Require STABLE_WINDOWS_NEEDED consecutive stable readings.
|
||||
static constexpr float STABILITY_WINDOW_SECS = 10.0f;
|
||||
static constexpr float STABILITY_THRESHOLD = 0.05f; // 5% change → stable
|
||||
static constexpr int STABLE_WINDOWS_NEEDED = 3;
|
||||
double prev_window_avg = 0.0;
|
||||
double window_sum = 0.0;
|
||||
int window_samples = 0;
|
||||
float window_timer = 0.0f;
|
||||
int consecutive_stable = 0; // count of consecutive stable windows
|
||||
|
||||
// Measurement: average-based (sustained performance, not peak burst)
|
||||
double measure_sum = 0.0;
|
||||
int measure_samples = 0;
|
||||
|
||||
int optimal_threads = 0;
|
||||
double optimal_hashrate = 0.0;
|
||||
bool was_pool_running = false;
|
||||
int prev_threads = 0;
|
||||
|
||||
// Track actual warmup durations for better time estimates
|
||||
float total_warmup_secs = 0.0f;
|
||||
|
||||
void 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 buildCandidates(int max_threads) {
|
||||
candidates.clear();
|
||||
// Start at half the cores — lower counts are rarely optimal and
|
||||
// testing them first would waste time warming up the CPU before
|
||||
// reaching the thread counts that actually matter.
|
||||
int start = std::max(1, max_threads / 2);
|
||||
for (int t = start; t <= max_threads; t++)
|
||||
candidates.push_back(t);
|
||||
}
|
||||
|
||||
/// Average warmup duration based on tests completed so far
|
||||
float avgWarmupSecs() const {
|
||||
if (current_index > 0)
|
||||
return total_warmup_secs / (float)current_index;
|
||||
return (MIN_WARMUP_SECS + MAX_WARMUP_SECS) * 0.5f; // initial estimate
|
||||
}
|
||||
|
||||
/// Estimated seconds per test (uses observed warmup average)
|
||||
float perTestSecs() const {
|
||||
return avgWarmupSecs() + MEASURE_SECS;
|
||||
}
|
||||
|
||||
float totalEstimatedSecs() const {
|
||||
int n = (int)candidates.size();
|
||||
if (n <= 0) return 0.0f;
|
||||
// Completed tests use actual time; remaining use estimate
|
||||
float completed_time = total_warmup_secs
|
||||
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
int remaining = n - current_index;
|
||||
float remaining_time = (float)remaining * (avgWarmupSecs() + MEASURE_SECS)
|
||||
+ (float)std::max(0, remaining - 1) * COOLDOWN_SECS;
|
||||
return completed_time + remaining_time;
|
||||
}
|
||||
|
||||
float elapsedSecs() const {
|
||||
float completed = total_warmup_secs
|
||||
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
|
||||
return completed + phase_timer;
|
||||
}
|
||||
|
||||
float progress() const {
|
||||
float total = totalEstimatedSecs();
|
||||
return (total > 0.0f) ? std::min(1.0f, elapsedSecs() / total) : 0.0f;
|
||||
}
|
||||
|
||||
void resetStabilityTracking() {
|
||||
prev_window_avg = 0.0;
|
||||
window_sum = 0.0;
|
||||
window_samples = 0;
|
||||
window_timer = 0.0f;
|
||||
consecutive_stable = 0;
|
||||
}
|
||||
};
|
||||
static ThreadBenchmark s_benchmark;
|
||||
|
||||
bool IsMiningBenchmarkActive() {
|
||||
return s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
||||
return s_benchmark.active();
|
||||
}
|
||||
|
||||
// Pool mode state
|
||||
@@ -178,59 +61,6 @@ static bool s_pool_state_loaded = false;
|
||||
static bool s_show_pool_log = false; // Toggle: false=chart, true=log
|
||||
static bool s_show_solo_log = false; // Toggle: false=chart, true=log (solo mode)
|
||||
|
||||
// Get max threads based on hardware
|
||||
static int GetMaxMiningThreads()
|
||||
{
|
||||
int hw_threads = std::thread::hardware_concurrency();
|
||||
return std::max(1, hw_threads);
|
||||
}
|
||||
|
||||
// Format hashrate with appropriate units
|
||||
static std::string FormatHashrate(double hashrate)
|
||||
{
|
||||
char buf[64];
|
||||
if (hashrate >= 1e12) {
|
||||
snprintf(buf, sizeof(buf), "%.2f TH/s", hashrate / 1e12);
|
||||
} else if (hashrate >= 1e9) {
|
||||
snprintf(buf, sizeof(buf), "%.2f GH/s", hashrate / 1e9);
|
||||
} else if (hashrate >= 1e6) {
|
||||
snprintf(buf, sizeof(buf), "%.2f MH/s", hashrate / 1e6);
|
||||
} else if (hashrate >= 1e3) {
|
||||
snprintf(buf, sizeof(buf), "%.2f KH/s", hashrate / 1e3);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%.2f H/s", hashrate);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Calculate estimated hours to find a block
|
||||
static double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty)
|
||||
{
|
||||
if (localHashrate <= 0 || networkHashrate <= 0) return 0;
|
||||
double blocksPerHour = 3600.0 / 75.0;
|
||||
double yourShare = localHashrate / networkHashrate;
|
||||
if (yourShare <= 0) return 0;
|
||||
return 1.0 / (blocksPerHour * yourShare);
|
||||
}
|
||||
|
||||
// Format estimated time
|
||||
static std::string FormatEstTime(double est_hours)
|
||||
{
|
||||
char buf[64];
|
||||
if (est_hours <= 0) {
|
||||
return "N/A";
|
||||
} else if (est_hours < 1.0) {
|
||||
snprintf(buf, sizeof(buf), "~%.0f min", est_hours * 60.0);
|
||||
} else if (est_hours < 24.0) {
|
||||
snprintf(buf, sizeof(buf), "~%.1f hrs", est_hours);
|
||||
} else if (est_hours < 168.0) {
|
||||
snprintf(buf, sizeof(buf), "~%.1f days", est_hours / 24.0);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "~%.1f weeks", est_hours / 168.0);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
static void RenderMiningTabContent(App* app);
|
||||
|
||||
void RenderMiningTab(App* app)
|
||||
@@ -279,9 +109,9 @@ static void RenderMiningTabContent(App* app)
|
||||
if (!s_threads_initialized) {
|
||||
int saved = app->settings()->getPoolThreads();
|
||||
if (mining.generate)
|
||||
s_selected_threads = std::max(1, mining.genproclimit);
|
||||
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
|
||||
else if (saved > 0)
|
||||
s_selected_threads = std::min(saved, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(saved, max_threads);
|
||||
else
|
||||
s_selected_threads = 1;
|
||||
s_threads_initialized = true;
|
||||
@@ -295,11 +125,11 @@ static void RenderMiningTabContent(App* app)
|
||||
// than threads_active from the xmrig API which lags during restarts.
|
||||
int reqThreads = app->getXmrigRequestedThreads();
|
||||
if (reqThreads > 0)
|
||||
s_selected_threads = std::min(reqThreads, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(reqThreads, max_threads);
|
||||
else if (state.pool_mining.threads_active > 0)
|
||||
s_selected_threads = std::min(state.pool_mining.threads_active, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(state.pool_mining.threads_active, max_threads);
|
||||
} else if (mining.generate && mining.genproclimit > 0) {
|
||||
s_selected_threads = std::min(mining.genproclimit, max_threads);
|
||||
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,15 +158,8 @@ static void RenderMiningTabContent(App* app)
|
||||
{
|
||||
static bool s_pool_worker_defaulted = false;
|
||||
std::string workerStr(s_pool_worker);
|
||||
if (!s_pool_worker_defaulted && !state.addresses.empty() &&
|
||||
(workerStr.empty() || workerStr == "x")) {
|
||||
std::string defaultAddr;
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "shielded" && !addr.address.empty()) {
|
||||
defaultAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDefaultPoolWorker(workerStr, s_pool_worker_defaulted) && !state.addresses.empty()) {
|
||||
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
|
||||
if (!defaultAddr.empty()) {
|
||||
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
@@ -368,136 +191,27 @@ static void RenderMiningTabContent(App* app)
|
||||
// Determine active mining state for UI
|
||||
// Include pool mining running state even when user just switched to solo,
|
||||
// so the button shows STOP/STOPPING while xmrig shuts down.
|
||||
bool isMiningActive = s_pool_mode
|
||||
? state.pool_mining.xmrig_running
|
||||
: (mining.generate || state.pool_mining.xmrig_running);
|
||||
bool isMiningActive = IsPoolMiningActive(s_pool_mode,
|
||||
state.pool_mining.xmrig_running,
|
||||
mining.generate);
|
||||
|
||||
// ================================================================
|
||||
// Thread Benchmark state machine — runs pool mining at each candidate
|
||||
// thread count to find the optimal setting for this CPU.
|
||||
// ================================================================
|
||||
if (s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||
s_benchmark.phase != ThreadBenchmark::Phase::Done) {
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
s_benchmark.phase_timer += dt;
|
||||
|
||||
switch (s_benchmark.phase) {
|
||||
case ThreadBenchmark::Phase::Starting:
|
||||
// Start pool mining at current candidate
|
||||
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
|
||||
int t = s_benchmark.candidates[s_benchmark.current_index];
|
||||
app->stopPoolMining();
|
||||
app->startPoolMining(t);
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::WarmingUp;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
s_benchmark.resetStabilityTracking();
|
||||
s_benchmark.measure_sum = 0.0;
|
||||
s_benchmark.measure_samples = 0;
|
||||
} else {
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::WarmingUp: {
|
||||
// Adaptive warmup: wait for hashrate to stabilize (thermal steady state).
|
||||
// After MIN_WARMUP (90s), compare rolling 10s hashrate windows.
|
||||
// Require 3 consecutive windows within 5% to confirm equilibrium.
|
||||
// Laptops can take 2-3+ minutes for thermal throttling to fully
|
||||
// manifest, so a single stable window isn't sufficient.
|
||||
bool past_min = s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS;
|
||||
bool past_max = s_benchmark.phase_timer >= ThreadBenchmark::MAX_WARMUP_SECS;
|
||||
|
||||
// Accumulate samples into current window
|
||||
if (state.pool_mining.hashrate_10s > 0.0) {
|
||||
s_benchmark.window_sum += state.pool_mining.hashrate_10s;
|
||||
s_benchmark.window_samples++;
|
||||
}
|
||||
s_benchmark.window_timer += dt;
|
||||
|
||||
bool stable = false;
|
||||
if (past_min && s_benchmark.window_timer >= ThreadBenchmark::STABILITY_WINDOW_SECS
|
||||
&& s_benchmark.window_samples > 0) {
|
||||
double current_avg = s_benchmark.window_sum / s_benchmark.window_samples;
|
||||
if (s_benchmark.prev_window_avg > 0.0) {
|
||||
double change = std::abs(current_avg - s_benchmark.prev_window_avg)
|
||||
/ s_benchmark.prev_window_avg;
|
||||
if (change < ThreadBenchmark::STABILITY_THRESHOLD)
|
||||
s_benchmark.consecutive_stable++;
|
||||
else
|
||||
s_benchmark.consecutive_stable = 0; // reset on instability
|
||||
if (s_benchmark.consecutive_stable >= ThreadBenchmark::STABLE_WINDOWS_NEEDED)
|
||||
stable = true;
|
||||
}
|
||||
// Shift window
|
||||
s_benchmark.prev_window_avg = current_avg;
|
||||
s_benchmark.window_sum = 0.0;
|
||||
s_benchmark.window_samples = 0;
|
||||
s_benchmark.window_timer = 0.0f;
|
||||
}
|
||||
|
||||
if (stable || past_max) {
|
||||
s_benchmark.total_warmup_secs += s_benchmark.phase_timer;
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Measuring;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
s_benchmark.measure_sum = 0.0;
|
||||
s_benchmark.measure_samples = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ThreadBenchmark::Phase::Measuring:
|
||||
// Sample average hashrate — reflects sustained (thermally throttled) performance
|
||||
if (state.pool_mining.hashrate_10s > 0.0) {
|
||||
s_benchmark.measure_sum += state.pool_mining.hashrate_10s;
|
||||
s_benchmark.measure_samples++;
|
||||
}
|
||||
if (s_benchmark.phase_timer >= ThreadBenchmark::MEASURE_SECS) {
|
||||
int t = s_benchmark.candidates[s_benchmark.current_index];
|
||||
double avg = (s_benchmark.measure_samples > 0)
|
||||
? s_benchmark.measure_sum / s_benchmark.measure_samples
|
||||
: 0.0;
|
||||
s_benchmark.results.push_back({t, avg});
|
||||
if (avg > s_benchmark.optimal_hashrate) {
|
||||
s_benchmark.optimal_hashrate = avg;
|
||||
s_benchmark.optimal_threads = t;
|
||||
}
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Advancing;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::Advancing:
|
||||
if (s_benchmark.active()) {
|
||||
auto benchmarkUpdate = AdvanceThreadBenchmark(
|
||||
s_benchmark, ImGui::GetIO().DeltaTime, state.pool_mining.hashrate_10s);
|
||||
if (benchmarkUpdate.stopPoolMining) {
|
||||
app->stopPoolMining();
|
||||
s_benchmark.current_index++;
|
||||
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
|
||||
// Cool down before next test to reduce thermal throttling bias
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::CoolingDown;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
} else {
|
||||
// Done — apply optimal thread count
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Done;
|
||||
if (s_benchmark.optimal_threads > 0) {
|
||||
s_selected_threads = s_benchmark.optimal_threads;
|
||||
app->settings()->setPoolThreads(s_selected_threads);
|
||||
app->settings()->save();
|
||||
}
|
||||
// Restart mining if it was running before, using optimal count
|
||||
if (s_benchmark.was_pool_running && s_benchmark.optimal_threads > 0) {
|
||||
app->startPoolMining(s_benchmark.optimal_threads);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ThreadBenchmark::Phase::CoolingDown:
|
||||
// Idle pause — let CPU temps drop before starting next test
|
||||
if (s_benchmark.phase_timer >= ThreadBenchmark::COOLDOWN_SECS) {
|
||||
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||
s_benchmark.phase_timer = 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (benchmarkUpdate.saveOptimalThreads) {
|
||||
s_selected_threads = benchmarkUpdate.optimalThreads;
|
||||
app->settings()->setPoolThreads(s_selected_threads);
|
||||
app->settings()->save();
|
||||
}
|
||||
if (benchmarkUpdate.startPoolMining) {
|
||||
app->startPoolMining(benchmarkUpdate.startThreads);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,10 +404,7 @@ static void RenderMiningTabContent(App* app)
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
std::string currentUrl(s_pool_url);
|
||||
bool alreadySaved = false;
|
||||
for (const auto& u : app->settings()->getSavedPoolUrls()) {
|
||||
if (u == currentUrl) { alreadySaved = true; break; }
|
||||
}
|
||||
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
@@ -875,10 +586,7 @@ static void RenderMiningTabContent(App* app)
|
||||
ImDrawList* dl2 = ImGui::GetWindowDrawList();
|
||||
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
|
||||
std::string currentWorker(s_pool_worker);
|
||||
bool alreadySaved = false;
|
||||
for (const auto& w : app->settings()->getSavedPoolWorkers()) {
|
||||
if (w == currentWorker) { alreadySaved = true; break; }
|
||||
}
|
||||
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
|
||||
if (btnHov) {
|
||||
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
|
||||
StateHover(), 4.0f * dp);
|
||||
@@ -1036,16 +744,10 @@ static void RenderMiningTabContent(App* app)
|
||||
OnSurfaceMedium(), resetIcon);
|
||||
|
||||
if (btnClk) {
|
||||
strncpy(s_pool_url, "pool.dragonx.is:3433", sizeof(s_pool_url) - 1);
|
||||
strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1);
|
||||
// Default to user's first shielded (z) address for pool payouts.
|
||||
// Leave blank if no z-address exists yet.
|
||||
std::string defaultAddr;
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "shielded" && !addr.address.empty()) {
|
||||
defaultAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
|
||||
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
|
||||
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
|
||||
s_pool_settings_dirty = true;
|
||||
|
||||
Reference in New Issue
Block a user