From 6c02e67c6d69021ca0926ca97a9c774b02b94127 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 9 Oct 2026 21:09:48 -0500 Subject: [PATCH] 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 --- CMakeLists.txt | 1 + src/app.cpp | 8 ++- src/app.h | 34 +++++++++++++ src/app_network.cpp | 37 +++++++++++++- src/app_security.cpp | 77 ++++++++++++++++++++++++++--- src/services/mining_thread_commit.h | 63 +++++++++++++++++++++++ src/ui/windows/mining_controls.cpp | 35 +++++-------- src/ui/windows/mining_tab.cpp | 13 +++-- tests/test_phase4.cpp | 64 ++++++++++++++++++++++++ 9 files changed, 296 insertions(+), 36 deletions(-) create mode 100644 src/services/mining_thread_commit.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 02b8eea..13f0e8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -623,6 +623,7 @@ set(APP_HEADERS src/services/connect_scan_gate.h src/services/scan_policy.h src/services/chat_scan_policy.h + src/services/mining_thread_commit.h src/services/wallet_security_controller.h src/services/wallet_security_workflow.h src/services/wallet_security_workflow_executor.h diff --git a/src/app.cpp b/src/app.cpp index 6fea693..51bf293 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -264,7 +264,9 @@ void App::applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap) // Restart so the new pool (and its per-pool algo) takes effect immediately. if (xmrig_manager_ && xmrig_manager_->isRunning()) { - const int threads = xmrig_manager_->getRequestedThreads(); + // A thread-count retarget may still be queued on the control thread: keep its count, not the old one. + const int queued = pool_threads_target_.load(std::memory_order_relaxed); + const int threads = queued > 0 ? queued : xmrig_manager_->getRequestedThreads(); stopPoolMining(); startPoolMining(threads); } @@ -994,6 +996,9 @@ void App::update() checkAutoLock(); } + // Apply a settled mining thread-count pick: one settings save + at most one miner change per gesture. + tickMiningThreadCommit(); + // Mine-when-idle check (cheap on most frames: it gates itself to 1 Hz) checkIdleMining(); @@ -1131,6 +1136,7 @@ void App::update() state_.pool_mining.hashrate_60s = 0.0; state_.pool_mining.hashrate_15m = 0.0; pool_starting_.store(false, std::memory_order_relaxed); + pool_threads_target_.store(0, std::memory_order_relaxed); // that miner is gone const std::string err = xmrig_manager_->getLastError(); ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly") : (std::string(TR("appx_miner_stopped_prefix")) + err)); diff --git a/src/app.h b/src/app.h index 87919a5..cda73cb 100644 --- a/src/app.h +++ b/src/app.h @@ -32,6 +32,7 @@ #include "services/wallet_scan_governor.h" #include "services/chat_scan_policy.h" #include "services/note_consolidation.h" +#include "services/mining_thread_commit.h" #include "services/network_refresh_service.h" #include "services/wallet_security_controller.h" #include "services/wallet_security_workflow.h" @@ -347,9 +348,23 @@ public: // Pool mining (xmrig) void startPoolMining(int threads); void stopPoolMining(); + // Change the thread count of the pool miner that is running (or queued to start). Never starts one. + void retargetPoolMining(int threads); int getXmrigRequestedThreads() const { return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0; } + // Thread count of the newest pool start/retarget posted and not stopped since (0 = none). Leads + // getXmrigRequestedThreads(), which only changes once the mining-control thread runs that job. + int poolThreadsTarget() const { return pool_threads_target_.load(std::memory_order_relaxed); } + // A thread-count pick from the Mining tab (grid tile, -/+, Enter). The settings value changes at once; + // the save and the miner change happen ONCE, when the input settles (drag released, or ~0.5 s after + // the last click). Latest wins. + void selectMiningThreads(int threads); + void setMiningThreadDragActive(bool active) { mining_thread_drag_ = active; } + // A pick has not reached the miner yet (the grid must not snap back to the old count meanwhile). + bool miningThreadCommitPending() const { + return mining_thread_commit_.pending() || mining_solo_threads_pending_ > 0; + } // True while the pool miner process is live — used to refuse replacing the binary under it. bool isPoolMinerRunning() const { return xmrig_manager_ && xmrig_manager_->isRunning(); @@ -1107,6 +1122,15 @@ private: bool mining_ctl_stop_ = false; void postMiningControl(std::function job); // enqueue a blocking xmrig op onto the FIFO thread void stopMiningControlThread(); // signal + join the control thread (shutdown) + // Builds the pool config on the main thread and posts stop(if running)+start to the control thread. + // retargetOnly: a thread-count change, decided on the control thread (never starts a miner). + void postPoolMinerJob(int threads, bool retargetOnly); + // Coalesced thread-count picks (main thread only; see services/mining_thread_commit.h). + services::MiningThreadCommit mining_thread_commit_; + bool mining_thread_drag_ = false; // the thread grid is mid-drag (set by the Mining tab) + bool mining_threads_save_pending_ = false; // settings hold a picked count that is not on disk yet + int mining_solo_threads_pending_ = 0; // settled pick waiting for an in-flight setgenerate + void tickMiningThreadCommit(); // per frame, from update() // Auto-balance runtime state (pool mining, full-node only). The service fetches // pool hashrates off-thread; the RNG drives the weighted-random pick. util::PoolStatsService pool_stats_service_; @@ -1589,6 +1613,16 @@ private: // poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay). std::atomic pool_starting_{false}; + // Thread count of the newest pool start/retarget job posted (0 = none / stopped). A hint for the main + // thread: set when a job is posted, cleared by stopPoolMining(), by a start that fails and by a retarget + // that finds no miner. The control thread re-checks the real state, so a stale value can only skip a + // job the newest posted one already covers, or post a no-op one. + std::atomic pool_threads_target_{0}; + void clearPoolThreadsTarget(int mine) { // only if it still holds this job's value + int expected = mine; + pool_threads_target_.compare_exchange_strong(expected, 0, std::memory_order_relaxed); + } + // Auto-shield guard (prevents concurrent auto-shield operations) std::atomic auto_shield_pending_{false}; // v1.3.0+ daemons auto-shield coinbase themselves; probe z_autoshieldstatus once per connection and diff --git a/src/app_network.cpp b/src/app_network.cpp index c8eb6d1..e45dd51 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -3369,6 +3369,28 @@ void App::startPoolMining(int threads) if (state_.mining.generate) stopMining(); // (the "stop the already-running miner first" step is done inside the control job below, in FIFO order) + postPoolMinerJob(threads, /*retargetOnly=*/false); +} + +// Change the thread count of the pool miner that is running, or queued to start (Mining tab picks, idle +// thread scaling). Never starts a miner: the job decides on the mining-control thread (postPoolMinerJob). +// Nothing is posted when there is no miner, or when the newest start/retarget already asks for `threads`. +void App::retargetPoolMining(int threads) +{ + if (!supportsPoolMining() || !xmrig_manager_) return; + threads = std::clamp(threads, 1, std::max(1, (int)std::thread::hardware_concurrency())); + const int target = pool_threads_target_.load(std::memory_order_relaxed); + if (target <= 0 && !xmrig_manager_->isRunning()) return; // no miner, none queued + if (target == threads) return; // the newest queued job already does this + postPoolMinerJob(threads, /*retargetOnly=*/true); +} + +// Builds the xmrig config from settings + wallet state on this (main) thread and posts the blocking +// stop(if running)+start to the mining-control thread. retargetOnly (a thread-count change): the job does +// nothing unless a miner is running when it runs, at another count; a config that no longer validates +// stops the miner, as the old stop-then-start did. +void App::postPoolMinerJob(int threads, bool retargetOnly) +{ daemon::XmrigManager::Config cfg; cfg.pool_url = settings_->getPoolUrl(); cfg.worker_name = settings_->getPoolWorker(); @@ -3381,6 +3403,7 @@ void App::startPoolMining(int threads) ui::Notifications::instance().error(util::isTransparentAddress(cfg.worker_name) ? TR("mining_payout_needs_shielded") : "Pool payout address is not a valid DragonX address — mining not started."); + if (retargetOnly) stopPoolMining(); // never keep mining on a config that no longer validates return; } // The algo follows the pool: official pools use their own algo (pool.dragonx.cc @@ -3411,6 +3434,7 @@ void App::startPoolMining(int threads) if (cfg.wallet_address.empty()) { DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n"); ui::Notifications::instance().error("No wallet address available — generate a Z address in the Receive tab"); + if (retargetOnly) stopPoolMining(); return; } @@ -3418,10 +3442,20 @@ void App::startPoolMining(int threads) // never blocks on stop()'s SIGTERM->SIGKILL->join; marshal the spawn result back to the UI. (M-03/L-06/ // L-08/L-09/L-13). cfg was fully built above on this (main) thread. daemon::XmrigManager::Config cfgCopy = cfg; - postMiningControl([this, cfgCopy]() { + const int target = cfg.threads; + pool_threads_target_.store(target, std::memory_order_relaxed); + postMiningControl([this, cfgCopy, target, retargetOnly]() { + // A retarget is decided HERE, after every earlier start/stop ran: nothing happens unless a miner is + // running now and at another count. So it never starts a miner the user stopped, and a restart's + // stop window (isRunning() false on the UI thread) can't hide the running one. + if (retargetOnly) { + if (!xmrig_manager_->isRunning()) { clearPoolThreadsTarget(target); return; } + if (xmrig_manager_->getRequestedThreads() == target) return; + } if (xmrig_manager_->isRunning()) xmrig_manager_->stop(3000); const bool ok = xmrig_manager_->start(cfgCopy); const std::string err = ok ? std::string() : xmrig_manager_->getLastError(); + if (!ok) clearPoolThreadsTarget(target); if (!worker_) return; worker_->post([this, ok, err]() -> rpc::RPCWorker::MainCb { return [this, ok, err]() { @@ -3450,6 +3484,7 @@ void App::startPoolMining(int threads) void App::stopPoolMining() { if (!xmrig_manager_) return; + pool_threads_target_.store(0, std::memory_order_relaxed); // no start/retarget is current any more // Off the render thread — stop()'s SIGTERM->SIGKILL->join can block up to ~3s. (M-03/L-06/L-08/L-09) postMiningControl([this]() { if (xmrig_manager_->isRunning()) xmrig_manager_->stop(3000); diff --git a/src/app_security.cpp b/src/app_security.cpp index b8e0358..c59fbae 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -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(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) // =========================================================================== diff --git a/src/services/mining_thread_commit.h b/src/services/mining_thread_commit.h new file mode 100644 index 0000000..c406652 --- /dev/null +++ b/src/services/mining_thread_commit.h @@ -0,0 +1,63 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#pragma once + +// Coalesces mining thread-count picks. The Mining tab reports every intermediate value of a gesture (a tile +// drag crosses up to ~24 tiles; the -/+ stepper is clicked repeatedly); only the LAST one should reach the +// settings file and the miner, once, when the input settles: on drag release, or kDebounceSec after the +// last click. Pure: time is passed in; nothing here touches settings, the miner or ImGui. + +namespace dragonx { +namespace services { + +class MiningThreadCommit { +public: + static constexpr double kDebounceSec = 0.5; + + // The user picked `threads` at `now` (seconds, any monotonic clock). Latest wins. + void select(int threads, double now) + { + target_ = threads; + pending_ = true; + due_ = now + kDebounceSec; + } + + // The drag that produced the pending pick ended: it is due now, not after the debounce. + void release(double now) + { + if (pending_ && due_ > now) due_ = now; + } + + void cancel() { pending_ = false; } + bool pending() const { return pending_; } + + // Once per frame. `held`: a drag is still in progress (nothing commits until it ends). Returns true + // exactly once per settled pick and sets `out` to it. + bool take(double now, bool held, int& out) + { + if (!pending_ || held || now < due_) return false; + pending_ = false; + out = target_; + return true; + } + +private: + int target_ = 0; + bool pending_ = false; + double due_ = 0.0; +}; + +// The pool thread count a wanted count is compared with: the newest start/retarget posted (it includes a +// restart still queued on the mining-control thread), else the count the running xmrig was started with, +// else what its HTTP API last reported (that lags a restart by seconds, so it is only a last resort). +inline int effectivePoolThreads(int queuedTarget, int requested, int apiActive) +{ + if (queuedTarget > 0) return queuedTarget; + if (requested > 0) return requested; + return apiActive; +} + +} // namespace services +} // namespace dragonx diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index 044960e..6b266c3 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -143,14 +143,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& auto applyThreads = [&](int tc) { tc = std::clamp(tc, 1, max_threads); if (tc == s_selected_threads) return; - s_selected_threads = tc; - app->settings()->setPoolThreads(tc); - app->settings()->save(); - if (mining.generate) app->startMining(tc); - if (s_pool_mode && state.pool_mining.xmrig_running) { - app->stopPoolMining(); - app->startPoolMining(tc); - } + s_selected_threads = tc; // shown at once; the App applies it ~0.5 s after the last change + app->selectMiningThreads(tc); }; ImFont* stepFont = Type().iconSmall(); // Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight == @@ -707,11 +701,14 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& // Drag-to-select logic (disabled during benchmark; ignored while a dropdown popup is open) if (!benchActive && !anyPopupOpen && ImGui::IsMouseClicked(0) && hovered_thread > 0) { - // Begin drag + // Begin drag. A click on the tile that is already selected changes nothing (it used to save + // settings and restart the miner). s_drag_active = true; s_drag_anchor_thread = hovered_thread; - s_selected_threads = hovered_thread; - threads_changed = true; + if (hovered_thread != s_selected_threads) { + s_selected_threads = hovered_thread; + threads_changed = true; + } } if (s_drag_active) { if (ImGui::IsMouseDown(0)) { @@ -783,17 +780,11 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf); } - if (threads_changed) { - app->settings()->setPoolThreads(s_selected_threads); - app->settings()->save(); - if (mining.generate) { - app->startMining(s_selected_threads); - } - if (s_pool_mode && state.pool_mining.xmrig_running) { - app->stopPoolMining(); - app->startPoolMining(s_selected_threads); - } - } + // Report every change (the header, RAM estimate and tile fill follow s_selected_threads live), but + // let the App apply it: it holds the commit while the mouse is down, so a drag across 16 tiles is + // ONE settings save and at most ONE miner change, on release. + if (threads_changed) app->selectMiningThreads(s_selected_threads); + app->setMiningThreadDragActive(s_drag_active); curY += gridH + secGap; } diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index 4b3f80b..c28ed63 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -140,16 +140,19 @@ static void RenderMiningTabContent(App* app) // Sync thread grid with actual count when idle thread scaling adjusts threads // Skip during benchmark — the benchmark controls thread counts directly - if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && !IsMiningBenchmarkActive()) { + // ...nor while a thread-count pick is still on its way to the miner (the grid would snap back to the old count). + if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && + !app->miningThreadCommitPending() && !IsMiningBenchmarkActive()) { if (s_pool_mode && state.pool_mining.xmrig_running) { - // Use the requested thread count (available immediately) rather - // than threads_active from the xmrig API which lags during restarts. - int reqThreads = app->getXmrigRequestedThreads(); + // Use the requested thread count (available immediately) rather than threads_active from the + // xmrig API, which lags during restarts; the App's target also covers a restart still queued. + const int queued = app->poolThreadsTarget(); + int reqThreads = queued > 0 ? queued : app->getXmrigRequestedThreads(); if (reqThreads > 0) s_selected_threads = ClampMiningThreads(reqThreads, max_threads); else if (state.pool_mining.threads_active > 0) s_selected_threads = ClampMiningThreads(state.pool_mining.threads_active, max_threads); - } else if (mining.generate && mining.genproclimit > 0) { + } else if (mining.generate && mining.genproclimit > 0 && !app->isMiningToggleInProgress()) { s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads); } } diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 4a05b58..7f6f467 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -33,6 +33,7 @@ #include "services/scan_policy.h" #include "services/connect_scan_gate.h" #include "services/chat_scan_policy.h" +#include "services/mining_thread_commit.h" #include "services/network_refresh_service.h" #include "services/refresh_scheduler.h" #include "services/note_consolidation.h" @@ -5686,6 +5687,68 @@ void testConsoleInputModel() EXPECT_EQ(errorLines[0].text, std::string("Error: bad rpc")); } +void testMiningThreadCommit() +{ + using dragonx::services::MiningThreadCommit; + using dragonx::services::effectivePoolThreads; + int out = 0; + + // Stepper: three clicks inside the debounce window -> one commit, with the last value. + { + MiningThreadCommit c; + c.select(5, 10.0); c.select(6, 10.1); c.select(7, 10.2); + EXPECT_TRUE(c.pending()); + EXPECT_FALSE(c.take(10.5, false, out)); // 0.3 s after the last click: not yet + EXPECT_TRUE(c.take(10.75, false, out)); + EXPECT_EQ(out, 7); + EXPECT_FALSE(c.pending()); + EXPECT_FALSE(c.take(11.0, false, out)); // consumed exactly once + } + // Drag: nothing commits while held, however long; the release commits the last tile at once. + { + MiningThreadCommit c; + for (int i = 1; i <= 16; ++i) { + c.select(i, 20.0 + i * 0.1); + EXPECT_FALSE(c.take(20.0 + i * 0.1 + 0.9, true, out)); + } + EXPECT_FALSE(c.take(40.0, true, out)); // still held 20 s later + c.release(40.1); + EXPECT_TRUE(c.take(40.1, false, out)); + EXPECT_EQ(out, 16); + } + // Plain click in real frame order: the tab's render selects (button down); on the next frame the App's + // tick sees the button up, calls release() and then take(): it commits on that frame, not 0.5 s later. + { + MiningThreadCommit c; + c.select(4, 30.0); + c.release(30.016); + EXPECT_TRUE(c.take(30.016, false, out)); + EXPECT_EQ(out, 4); + } + // release() with nothing pending changes nothing. + { + MiningThreadCommit c; + c.release(1.0); + EXPECT_FALSE(c.pending()); + EXPECT_FALSE(c.take(2.0, false, out)); + } + // A new pick during the wait pushes the deadline out; cancel() drops it. + { + MiningThreadCommit c; + c.select(2, 50.0); + c.select(9, 50.4); + EXPECT_FALSE(c.take(50.6, false, out)); + c.cancel(); + EXPECT_FALSE(c.pending()); + EXPECT_FALSE(c.take(60.0, false, out)); + } + // Which pool count idle scaling compares with. + EXPECT_EQ(effectivePoolThreads(8, 4, 2), 8); // a queued restart's target beats the stale values + EXPECT_EQ(effectivePoolThreads(0, 4, 2), 4); + EXPECT_EQ(effectivePoolThreads(0, 0, 2), 2); + EXPECT_EQ(effectivePoolThreads(0, 0, 0), 0); +} + void testMiningBenchmarkModel() { dragonx::ui::ThreadBenchmark benchmark; @@ -12660,6 +12723,7 @@ int main() testRendererHelpers(); testConsoleInputModel(); testMiningBenchmarkModel(); + testMiningThreadCommit(); testBalanceAddressListModel(); testExplorerBlockCache(); testTransactionHistoryCache();