perf(app): run miner shutdown off the UI thread

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 06:26:13 -05:00
co-authored by Claude Opus 4.8
parent 30f5243dd1
commit 8df0fe1734
2 changed files with 35 additions and 10 deletions
+32 -10
View File
@@ -175,16 +175,29 @@ void App::postMiningControl(std::function<void()> 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<std::mutex> 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();
+3
View File
@@ -1196,7 +1196,9 @@ private:
std::deque<std::function<void()>> mining_ctl_queue_;
bool mining_ctl_stop_ = false;
void postMiningControl(std::function<void()> 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