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:
dan_s
2026-04-29 12:47:57 -05:00
parent 9e1b1397ad
commit d684db446e
95 changed files with 8776 additions and 37563 deletions

View File

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