perf(mining): one save, at most one restart per thread-count gesture

Every thread-count change on the Mining tab saved settings.json and,
with a miner running, stopped and restarted it: xmrig re-inits its
RandomX dataset and reconnects to the pool, seconds of lost hashing
each time. A drag across the grid did this once per tile crossed (up to
~24), with a "Starting pool miner" toast each; every -/+ click or Enter
did it again.

The tab now hands each pick to the App, which puts the newest in
settings at once and applies it when the input settles: on drag
release, or 0.5 s after the last click, even if the Mining tab was left
in the meantime. The header, RAM estimate and tile fill still follow
the drag live.

- Pool: the restart is decided on the mining-control thread, after every
  earlier start or stop has run. It never starts a miner the user
  stopped, is a no-op when xmrig already runs at that count, and is no
  longer silently skipped when the pick lands in another restart's stop
  window (which left xmrig on the old count).
- Solo: one setgenerate per gesture. A pick made while one is in flight
  now waits for it instead of being dropped (another stale count).
- Clicking the tile that is already selected does nothing; it used to
  save settings and restart the miner.
- With idle thread scaling on, the grid no longer snaps back to the old
  count while a pick is on its way to the miner. A pool auto-balance
  restart keeps a count still queued.
- Idle thread scaling changes the pool count through the same retarget.
  Its correction after a start compares with the count last requested,
  not the xmrig API's lagging report, so it comes about 1 s in instead
  of after the first API report (3-5 s).
- A retarget whose pool config no longer validates shows the same error
  and stops the miner, as the old stop-then-start did.
- While a benchmark runs a pick is saved, but the miner change is
  dropped: the benchmark owns the count until it ends.

A crash within 0.5 s of a pick, or mid-drag, loses that pick; every
clean exit saves it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 21:09:48 -05:00
co-authored by Claude Opus 5.5
parent c9cb50f4dd
commit 6c02e67c6d
9 changed files with 296 additions and 36 deletions
+70 -7
View File
@@ -819,8 +819,7 @@ void App::checkIdleMining() {
if (!idle_scaled_to_idle_) {
idle_scaled_to_idle_ = true;
if (isPool) {
stopPoolMining();
startPoolMining(idleThreads);
retargetPoolMining(idleThreads);
} else {
startMining(idleThreads);
}
@@ -831,8 +830,7 @@ void App::checkIdleMining() {
if (idle_scaled_to_idle_) {
idle_scaled_to_idle_ = false;
if (isPool) {
stopPoolMining();
startPoolMining(activeThreads);
retargetPoolMining(activeThreads);
} else {
startMining(activeThreads);
}
@@ -840,13 +838,16 @@ void App::checkIdleMining() {
} else {
// Mining just started while user is active — ensure active
// thread count is applied (grid selection may differ).
// Pool: compare with the count last REQUESTED (a restart still queued on the control thread
// included), not the xmrig API's last report, which lags a restart by seconds.
int currentThreads = isPool
? xmrig_manager_->getStats().threads_active
? services::effectivePoolThreads(pool_threads_target_.load(std::memory_order_relaxed),
xmrig_manager_->getRequestedThreads(),
xmrig_manager_->getStats().threads_active)
: state_.mining.genproclimit;
if (currentThreads > 0 && currentThreads != activeThreads) {
if (isPool) {
stopPoolMining();
startPoolMining(activeThreads);
retargetPoolMining(activeThreads);
} else {
startMining(activeThreads);
}
@@ -889,6 +890,68 @@ void App::checkIdleMining() {
}
}
// ===========================================================================
// Mining thread-count picks — coalesced (one save + at most one miner change per gesture)
// ===========================================================================
namespace {
double miningSteadySeconds()
{
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
} // namespace
void App::selectMiningThreads(int threads)
{
if (!settings_) return;
threads = std::clamp(threads, 1, std::max(1, (int)std::thread::hardware_concurrency()));
if (settings_->getPoolThreads() != threads) { // in memory now; saved once when the pick settles
settings_->setPoolThreads(threads);
mining_threads_save_pending_ = true;
}
mining_thread_commit_.select(threads, miningSteadySeconds());
}
void App::tickMiningThreadCommit()
{
if (shutting_down_ || !settings_) return; // beginShutdown() saves settings itself
const double now = miningSteadySeconds();
// The grid holds the commit while the button is down. The release is noticed HERE, not by the tab, so
// a drag that ends while the Mining tab isn't drawn (page switch, focus loss) still commits.
bool held = false;
if (mining_thread_drag_) {
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) held = true;
else { mining_thread_drag_ = false; mining_thread_commit_.release(now); }
}
// The benchmark owns the miner's thread count while it runs (its cancel/finish restore it): keep the
// pick in settings, drop the miner change.
if (ui::IsMiningBenchmarkActive()) {
mining_thread_commit_.cancel();
mining_solo_threads_pending_ = 0;
if (mining_threads_save_pending_) { mining_threads_save_pending_ = false; settings_->save(); }
return;
}
int threads = 0;
if (mining_thread_commit_.take(now, held, threads)) {
if (mining_threads_save_pending_) { mining_threads_save_pending_ = false; settings_->save(); }
retargetPoolMining(threads); // restarts xmrig once, only if it is running
mining_solo_threads_pending_ = threads; // setgenerate, once no other toggle is in flight
} else if (mining_thread_commit_.pending() && !held) {
ui::material::RequestProgressFrame(); // keep frames coming until the debounce deadline
}
// Solo: startMining() drops a call while another setgenerate is in flight, so wait for it (its result
// wakes the loop). Applies only while solo mining is on, as before.
if (mining_solo_threads_pending_ > 0 && !mining_toggle_in_progress_.load(std::memory_order_relaxed)) {
const int t = mining_solo_threads_pending_;
mining_solo_threads_pending_ = 0;
if (state_.mining.generate) startMining(t);
}
}
// ===========================================================================
// Restart Setup Wizard (from Settings)
// ===========================================================================