From 55bcd7c48181c8c18a9210828b8723ac3fe1d4c7 Mon Sep 17 00:00:00 2001 From: Ibrahim Ansari <6884828+retrixe@users.noreply.github.com> Date: Wed, 17 May 2023 21:31:17 +0530 Subject: [PATCH] Avoid blocking NVIDIA GPUs from sleeping (#259) * Only poll NVIDIA GPUs which are awake. * Slow down polling if an NVIDIA GPU can sleep. If an NVIDIA GPU is eligible to sleep, i.e. runtime D3 sleep is enabled and no processes except GNOME or X.org are running, then don't poll the GPU for at least another 30 seconds. * NVIDIA: Check for X.org correctly on most distros. If a distro is naming X.org differently than these common and sensible names, then we can't detect or account for that in any way unfortunately * Show N/A instead when NVIDIA GPU is sleeping. --- .../nvidiaUtil.js | 169 +++++++++++++++++- 1 file changed, 163 insertions(+), 6 deletions(-) diff --git a/freon@UshakovVasilii_Github.yahoo.com/nvidiaUtil.js b/freon@UshakovVasilii_Github.yahoo.com/nvidiaUtil.js index c5a2243..3bfac8d 100644 --- a/freon@UshakovVasilii_Github.yahoo.com/nvidiaUtil.js +++ b/freon@UshakovVasilii_Github.yahoo.com/nvidiaUtil.js @@ -5,12 +5,153 @@ const Gio = imports.gi.Gio; const Me = imports.misc.extensionUtils.getCurrentExtension(); const CommandLineUtil = Me.imports.commandLineUtil; -var NvidiaUtil = class extends CommandLineUtil.CommandLineUtil { +var NvidiaUtil = class { constructor() { - super(); - let path = GLib.find_program_in_path('nvidia-smi'); - this._argv = path ? [path, '--query-gpu=name,temperature.gpu', '--format=csv,noheader'] : null; + this._nvidiaSmiPath = GLib.find_program_in_path('nvidia-smi'); + this._updated = false; + this._gpuInfo = {}; + this._output = []; + } + + async execute(callback) { + try { + // Read all GPUs from /proc/driver/nvidia/gpus. + const directory = Gio.File.new_for_path('/proc/driver/nvidia/gpus'); + const iter = await new Promise((resolve, reject) => { + directory.enumerate_children_async( + 'standard::*', + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + GLib.PRIORITY_DEFAULT, + null, + (file_, result) => { + try { + resolve(directory.enumerate_children_finish(result)); + } catch (e) { + reject(e); + } + } + ); + }); + const gpus = [] + + while (true) { + const infos = await new Promise((resolve, reject) => { + iter.next_files_async(10, GLib.PRIORITY_DEFAULT, null, (iter_, res) => { + try { + resolve(iter.next_files_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + if (infos.length === 0) + break; + + for (const info of infos) + gpus.push(info.get_name()); + } + + // For each GPU... + let gpuInfo = {}; + for (const gpu of gpus) { + // ...read /proc/driver/nvidia/gpus//power and check if it supports sleep. + const file = Gio.File.new_for_path(`/proc/driver/nvidia/gpus/${gpu}/power`); + const [, contents, etag] = await new Promise((resolve, reject) => { + file.load_contents_async(null, (file_, result) => { + try { + resolve(file.load_contents_finish(result)); + } catch (e) { + reject(e); + } + }); + }); + + // If the GPU is sleeping, don't poll it. + const decoder = new TextDecoder('utf-8'); + const contentsString = decoder.decode(contents); + const prevGpuInfo = this._gpuInfo[gpu] || {}; + if (contentsString.split('\n')[1].endsWith('Off') && prevGpuInfo.output) { + gpuInfo[gpu] = { output: prevGpuInfo.output.split(',')[0] + ',N/A' }; + continue; + } + + // If the GPU needs time to sleep, then keep showing the old temperature. + // Since even process monitoring prevents sleep, we don't check && sleepEligible :/ + if ((prevGpuInfo.skipUntil || 0) > Date.now()) { + gpuInfo[gpu] = prevGpuInfo; + continue; + } + + // Poll the GPU. + gpuInfo[gpu] = { output: await this.getGpuInfo(gpu) }; + + // If runtime D3 is enabled and the GPU is eligible to sleep... + try { + const sleepEligible = await this.isGpuEligibleToSleep(gpu); + if (contentsString.split('\n')[0].includes('Enabled') && sleepEligible) { + // ...skip polling it for 30 seconds. + gpuInfo[gpu].skipUntil = Date.now() + 30000; + } + } catch (e) { + console.error(e); + } + } + this._gpuInfo = gpuInfo; + this._output = Object.keys(this._gpuInfo) + .sort() + .map(gpu => gpuInfo[gpu].output) + .filter(output => !!output); + } catch (e) { + console.error(e); + } finally { + callback(); + this._updated = true; + } + } + + isGpuEligibleToSleep(id) { + return new Promise((resolve, reject) => { + let proc = Gio.Subprocess.new( + [this._nvidiaSmiPath, 'pmon', '--count=1', `--id=${id}`], + Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_PIPE); + + proc.communicate_utf8_async(null, null, (proc, result) => { + try { + let [, stdout, stderr] = proc.communicate_utf8_finish(result); + const processes = stdout ? stdout.trim().split('\n').slice(2) : []; + if (processes.length === 1) { + const process = processes[0].toLowerCase().split(' ').pop().replace(/-?server/, ''); + resolve(process === 'xorg' || process === 'x' || process === 'x11' || // X11 + process === 'gnome-shell'); // Wayland + } else { + resolve(processes.length === 0); + } + } catch (e) { + reject(e); + } + }); + }); + } + + getGpuInfo(id) { + return new Promise((resolve, reject) => { + let proc = Gio.Subprocess.new( + [this._nvidiaSmiPath, '--query-gpu=name,temperature.gpu', '--format=csv,noheader', `--id=${id}`], + Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_PIPE); + + proc.communicate_utf8_async(null, null, (proc, result) => { + try { + let [, stdout, stderr] = proc.communicate_utf8_finish(result); + resolve(stdout ? stdout.trim() : ''); + } catch (e) { + reject(e); + } + }); + }); } get temp() { @@ -23,9 +164,9 @@ var NvidiaUtil = class extends CommandLineUtil.CommandLineUtil { continue; let label = values[0].trim(); - let temp = parseFloat(values[1]); + let temp = values[1] === 'N/A' ? null : parseFloat(values[1]); - if(!label || !temp) + if(!label || isNaN(temp)) continue; gpus.push({ label: label, temp: temp }); @@ -35,4 +176,20 @@ var NvidiaUtil = class extends CommandLineUtil.CommandLineUtil { return gpus; } + get available() { + return !!this._nvidiaSmiPath; + } + + get updated() { + return this._updated; + } + + set updated(updated) { + this._updated = updated; + } + + destroy(callback) { + this._gpuInfo = {}; + this._output = []; + } };