perf(mining): run the mine-when-idle check at 1 Hz, probe the GPU sparingly

checkIdleMining ran every frame: an X11 idle query and, with GPU-aware
idle detection, an nvidia-smi launch on the UI thread per frame.

- The check runs once a second, since its decisions work on a seconds
  scale (the idle delay is tens of seconds or more). A miner it started
  or scaled up still yields on the first frame that sees the user back:
  that path skips the gate, because frames can be seconds apart while
  the app idles.
- The GPU is probed only when input is idle and the result can change
  a decision, at most every 5 s, with hysteresis (busy at 10%+, clear
  below 5%) so a GPU hovering near 10% can't keep restarting the miner.
- Thread scaling returns early when no miner is running or a toggle is
  in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:52:10 -05:00
co-authored by Claude Opus 5.5
parent 7ceff1eaaa
commit 2cf183ddd9
3 changed files with 42 additions and 14 deletions
+38 -13
View File
@@ -752,6 +752,17 @@ void App::checkIdleMining() {
// Skip idle mining adjustments while thread benchmark is running
if (ui::IsMiningBenchmarkActive()) return;
// The decisions below work on a seconds scale (the idle delay is tens of seconds or more), so check
// at 1 Hz. This used to run every frame: an X11 idle query and, with GPU-aware idle, an nvidia-smi
// launch on the UI thread per frame. Steady clock rather than ImGui::GetTime(), whose clock stops
// while minimized (update() still runs then).
const auto now = std::chrono::steady_clock::now();
// ...except while a miner this feature started (or scaled up) is running: it must yield on the first
// frame that sees the user back, and frames can be seconds apart when the app idles, so a gated frame
// may be the only one carrying that input. The idle query is cheap; the GPU probe has its own cache.
if (!(idle_mining_active_ || idle_scaled_to_idle_) && now < idle_mining_next_check_) return;
idle_mining_next_check_ = now + std::chrono::seconds(1);
int idleSec = util::Platform::getSystemIdleSeconds();
int delay = settings_->getMineIdleDelay();
bool isPool = settings_->getPoolMode() || !supportsSoloMining();
@@ -759,26 +770,40 @@ void App::checkIdleMining() {
bool threadScaling = settings_->getIdleThreadScaling();
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
// GPU-aware idle detection: if enabled, treat GPU utilization >= 10%
// as "user active" (e.g. watching a video). Disabled = unrestricted
// mode that only looks at keyboard/mouse input.
bool gpuBusy = false;
if (settings_->getIdleGpuAware()) {
int gpuUtil = util::Platform::getGpuUtilization();
gpuBusy = (gpuUtil >= 10);
}
bool systemIdle = (idleSec >= delay) && !gpuBusy;
// Check if mining is already running (manually started by user)
bool miningActive = isPool
? (xmrig_manager_ && xmrig_manager_->isRunning())
: state_.mining.generate;
// Thread scaling only adjusts a miner the user started: with none running there is nothing to
// decide, or to probe for.
if (threadScaling && (!miningActive || mining_toggle_in_progress_.load())) return;
// GPU-aware idle detection: if enabled, treat GPU utilization >= 10% as "user active" (e.g. watching
// a video). Disabled = unrestricted mode that only looks at keyboard/mouse input. The probe can only
// matter once input is idle and a decision can change (start/stop mode leaves a miner the user
// started alone), so it runs only then, at most every 5 s, with hysteresis (busy at >= 10%, clear
// below 5%) so a GPU hovering near 10% can't keep restarting the miner. Unknown (-1) reads as not busy.
const bool inputIdle = idleSec >= delay;
const bool decisionPossible = threadScaling || !miningActive || idle_mining_active_;
bool gpuBusy = false;
if (inputIdle && decisionPossible && settings_->getIdleGpuAware()) {
if (now >= idle_gpu_next_sample_) {
const int gpuUtil = util::Platform::getGpuUtilization();
if (gpuUtil >= 10) idle_gpu_busy_ = true;
else if (gpuUtil < 5) idle_gpu_busy_ = false;
idle_gpu_next_sample_ = now + std::chrono::seconds(5);
}
gpuBusy = idle_gpu_busy_;
} else if (!inputIdle) {
idle_gpu_busy_ = false; // input activity decides now; sample afresh once idle again
idle_gpu_next_sample_ = {};
}
bool systemIdle = inputIdle && !gpuBusy;
if (threadScaling) {
// --- Thread scaling mode ---
// Mining must already be running (started by user). We just adjust threads.
if (!miningActive || mining_toggle_in_progress_.load()) return;
// Mining is already running (started by user; checked above). We just adjust threads.
int activeThreads = settings_->getIdleThreadsActive();
int idleThreads = settings_->getIdleThreadsIdle();
// Resolve auto values: active defaults to half, idle defaults to all