From 8df0fe1734c2d67fe3352280264d421b3dd34f99 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 14 Oct 2026 06:26:13 -0500 Subject: [PATCH] perf(app): run miner shutdown off the UI thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At quit the UI thread drained and joined the mining-control thread and then called the miner's blocking stop with a 3 s budget. Both ran on the UI thread, so the window could stall for up to that long before the shutdown screen rendered. - Quit now only signals the mining-control thread (no join) on the UI thread. The blocking teardown — join the control thread, then stop the miner — moves to a background thread, so xmrig still has a single driver but the UI thread never blocks on it. - On the daemon-stop path that teardown runs first on the shutdown thread, so the miner is fully stopped before the daemon stop begins. On the disconnect-only quit paths it runs on its own thread, which the final shutdown joins before tearing xmrig down, so there is never a second concurrent join of the control thread. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 42 ++++++++++++++++++++++++++++++++---------- src/app.h | 3 +++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index b7a607f..32ecbec 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -175,16 +175,29 @@ void App::postMiningControl(std::function job) // Signal the mining-control thread to stop and join it. Called at shutdown BEFORE xmrig_manager_ is stopped // or destroyed, so no control job runs concurrently with teardown. Idempotent. -void App::stopMiningControlThread() +void App::signalMiningControlStop() { { std::lock_guard lk(mining_ctl_mutex_); mining_ctl_stop_ = true; } mining_ctl_cv_.notify_all(); +} + +void App::stopMiningControlThread() +{ + signalMiningControlStop(); if (mining_ctl_thread_.joinable()) mining_ctl_thread_.join(); } +// Blocking miner teardown, run OFF the UI thread at quit. Joins the control thread first (waits out any in-flight +// start/stop job) so this is the sole driver of xmrig_manager_, then stops the miner. +void App::finishMinerShutdown() +{ + if (mining_ctl_thread_.joinable()) mining_ctl_thread_.join(); + if (xmrig_manager_ && xmrig_manager_->isRunning()) xmrig_manager_->stop(3000); +} + namespace { // How often auto-balance re-evaluates the pool while active. Long, because switching // restarts the miner (drops in-flight shares + reconnect); the incumbent stickiness @@ -6591,14 +6604,12 @@ void App::beginShutdown() http_worker_->requestStop(); } - // Drain + join the mining-control thread FIRST so no async start/stop job runs while we tear the miner - // down here (avoids two threads driving xmrig_manager_ during shutdown). (M-03 cluster) - stopMiningControlThread(); - - // Stop xmrig pool miner before stopping the daemon + // Signal the mining-control thread now (no join: the UI thread must not block). The blocking + // join-control-thread -> xmrig stop(3000) teardown runs on a background thread (finishMinerShutdown), so + // xmrig_manager_ still has a single driver. (M-03 cluster) + signalMiningControlStop(); if (xmrig_manager_ && xmrig_manager_->isRunning()) { shutdown_status_ = TR("appx_stopping_pool_miner"); - xmrig_manager_->stop(3000); } DEBUG_LOGF("beginShutdown: starting (daemon_controller_=%s)\n", @@ -6614,7 +6625,10 @@ void App::beginShutdown() if (settings_) { settings_->save(); } - shutdown_complete_ = true; + miner_stop_thread_ = std::thread([this]() { + finishMinerShutdown(); + shutdown_complete_ = true; + }); return; } @@ -6628,7 +6642,10 @@ void App::beginShutdown() if (shutdownDecision.action == daemon::DaemonController::ShutdownAction::DisconnectOnly) { DEBUG_LOGF("beginShutdown: %s, skipping daemon stop\n", shutdownDecision.logReason); shutdown_status_ = shutdownDecision.status; - shutdown_complete_ = true; + miner_stop_thread_ = std::thread([this]() { + finishMinerShutdown(); + shutdown_complete_ = true; + }); return; } shutdown_status_ = shutdownDecision.status; @@ -6641,6 +6658,7 @@ void App::beginShutdown() // keeps rendering the shutdown screen (like SilentDragonX's // modal "Please wait" dialog). shutdown_thread_ = std::thread([this]() { + finishMinerShutdown(); // miner fully stopped before the daemon stop begins DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n"); shutdown_status_ = TR("appx_sending_stop_command_to_daemon"); // Deliver stop and wait for the node to exit: no time limit, never killed unasked. The shutdown @@ -7735,7 +7753,11 @@ void App::shutdown() // Ensure the mining-control thread is stopped + joined (idempotent; beginShutdown already did it on the // normal quit path, but shutdown() can also run without it). Must precede xmrig_manager_ teardown. - stopMiningControlThread(); + // The background miner teardown (disconnect-only quit paths) is joined first: it is the one driver. + if (miner_stop_thread_.joinable()) miner_stop_thread_.join(); + // After beginShutdown() the control thread is joined by finishMinerShutdown() (on shutdown_thread_ or + // miner_stop_thread_); joining it here too would be two concurrent joins. + if (!shutting_down_) stopMiningControlThread(); abortRpcBusyCanary(); if (canary_thread_.joinable()) canary_thread_.join(); diff --git a/src/app.h b/src/app.h index cd36d57..eecd018 100644 --- a/src/app.h +++ b/src/app.h @@ -1196,7 +1196,9 @@ private: std::deque> mining_ctl_queue_; bool mining_ctl_stop_ = false; void postMiningControl(std::function job); // enqueue a blocking xmrig op onto the FIFO thread + void signalMiningControlStop(); // signal only (no join): safe on the UI thread void stopMiningControlThread(); // signal + join the control thread (shutdown) + void finishMinerShutdown(); // join control thread -> xmrig stop(3000); background only // 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); @@ -1262,6 +1264,7 @@ private: bool address_list_dirty_ = false; // P8: dedup rebuildAddressList GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05) std::thread shutdown_thread_; + std::thread miner_stop_thread_; // disconnect-only quit paths: miner teardown off the UI thread // Confirm-before-stopping-daemon-mid-witness-rebuild guard (see beginShutdown / renderDaemonStopConfirm) bool pending_shutdown_confirm_ = false; // a quit is deferred, waiting to open the confirm modal bool daemon_stop_confirm_open_ = false; // the confirm modal is currently showing