diff --git a/src/Managers/Process.vala b/src/Managers/Process.vala index db527f839..d5bfabb92 100644 --- a/src/Managers/Process.vala +++ b/src/Managers/Process.vala @@ -42,6 +42,9 @@ public class Monitor.Process : GLib.Object { // Contains info about io public ProcessIO io; + // Contains info about GPU usage + private ProcessDRM drm; + // Contains status info public ProcessStatus stat; @@ -61,26 +64,29 @@ public class Monitor.Process : GLib.Object { private uint64 cpu_last_used; // Memory usage of the process, measured in KiB. - public uint64 mem_usage { get; private set; } public double mem_percentage { get; private set; } - private uint64 last_total; + public double gpu_percentage { get; private set; } + + private uint64 last_total; // @TODO: Obsolete? const int HISTORY_BUFFER_SIZE = 30; - public Gee.ArrayList cpu_percentage_history = new Gee.ArrayList (); - public Gee.ArrayList mem_percentage_history = new Gee.ArrayList (); + public Gee.ArrayList cpu_percentage_history = new Gee.ArrayList (); + public Gee.ArrayList mem_percentage_history = new Gee.ArrayList (); // Construct a new process - public Process (int _pid) { + public Process (int _pid, int update_interval) { _icon = ProcessUtils.get_default_icon (); open_files_paths = new Gee.HashSet (); last_total = 0; + drm = new ProcessDRM (_pid, update_interval); + io = {}; stat = {}; stat.pid = _pid; @@ -101,8 +107,9 @@ public class Monitor.Process : GLib.Object { exists = parse_stat () && read_cmdline (); get_children_pids (); get_usage (0, 1); - } + gpu_percentage = 0; + } // Updates the process to get latest information // Returns if the update was successful @@ -110,6 +117,8 @@ public class Monitor.Process : GLib.Object { exists = parse_stat (); if (exists) { get_usage (cpu_total, cpu_last_total); + drm.update (); + gpu_percentage = drm.gpu_percentage; parse_io (); parse_statm (); get_open_files (); @@ -280,8 +289,8 @@ public class Monitor.Process : GLib.Object { } /** - * Reads the /proc/%pid%/cmdline file and updates from the information contained therein. - */ + * Reads the /proc/%pid%/cmdline file and updates from the information contained therein. + */ private bool read_cmdline () { string ? cmdline = ProcessUtils.read_file ("/proc/%d/cmdline".printf (stat.pid)); @@ -301,6 +310,7 @@ public class Monitor.Process : GLib.Object { return true; } + // @TODO: Divide into get_usage_cpu and get_usage_mem and write some tests private void get_usage (uint64 cpu_total, uint64 cpu_last_total) { // Get CPU usage by process GTop.ProcTime proc_time; diff --git a/src/Managers/ProcessDRM.vala b/src/Managers/ProcessDRM.vala new file mode 100644 index 000000000..b6d468e84 --- /dev/null +++ b/src/Managers/ProcessDRM.vala @@ -0,0 +1,118 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Monitor.ProcessDRM { + /** + * Time spent busy in nanoseconds by the render engine executing + * workloads from the last time it was read + */ + private uint64 last_engine_render; + private uint64 last_engine_gfx; + + + public double gpu_percentage { get; private set; } + + private int pid; + private int update_interval; + + public ProcessDRM (int pid, int update_interval) { + this.pid = pid; + this.update_interval = update_interval; + + last_engine_render = 0; + last_engine_gfx = 0; + } + + public void update () { + string path_fdinfo = "/proc/%d/fdinfo".printf (pid); + string path_fd = "/proc/%d/fd".printf (pid); + + + var drm_files = new Gee.ArrayList (); + + try { + Dir dir = Dir.open (path_fdinfo, 0); + string ? name = null; + + while ((name = dir.read_name ()) != null) { + + // skip standard fds + if (name == "0" || name == "1" || name == "2") { + continue; + } + string path = Path.build_filename (path_fdinfo, name); + + int fd_dir_fd = Posix.open (path_fd, Posix.O_RDONLY | Posix.O_DIRECTORY); + if (fd_dir_fd == -1) { + warning ("Cannot open file descriptor: %s", path_fd); + continue; + } + + bool is_drm = is_drm_fd (fd_dir_fd, name); + Posix.close (fd_dir_fd); + + if (is_drm) { + var drm_file = File.new_for_path (path); + drm_files.add (drm_file); + } + } + } catch (FileError err) { + // prevent flooding logs with permission errors + if (!(err is FileError.ACCES)) { + warning (err.message); + } + } + + foreach (var drm_file in drm_files) { + try { + var dis = new DataInputStream (drm_file.read ()); + string ? line; + + while ((line = dis.read_line ()) != null) { + var splitted_line = line.split (":"); + switch (splitted_line[0]) { + case "drm-engine-gfx": + update_engine (splitted_line[1], ref last_engine_gfx); + break; + // for i915 there is only drm-engine-render to check + case "drm-engine-render": + update_engine (splitted_line[1], ref last_engine_render); + break; + default: + // Ignore other entries + break; + } + } + } catch (Error err) { + if (!(err is FileError.ACCES)) { + warning ("Can't read fdinfo: '%s' %d", err.message, err.code); + } + } + break; + } + } + + private void update_engine (string line, ref uint64 last_engine) { + var engine = uint64.parse (line.strip ().split (" ")[0]); + if (last_engine != 0) { + gpu_percentage = calculate_percentage (engine, last_engine, update_interval); + } + last_engine = engine; + } + + private static double calculate_percentage (uint64 engine, uint64 last_engine, int interval) { + // Since values in the files are in nanoseconds, it is also needed to convert interval to nanoseconds (10^9) + return 100 * ((double) (engine - last_engine)) / (interval * 1e9); + } + + // Based on nvtop + // https://github.com/Syllo/nvtop/blob/4bf5db248d7aa7528f3a1ab7c94f504dff6834e4/src/extract_processinfo_fdinfo.c#L88 + private static bool is_drm_fd (int fd_dir_fd, string name) { + Posix.Stat stat; + int ret = Posix.fstatat (fd_dir_fd, name, out stat, 0); + return ret == 0 && (stat.st_mode & Posix.S_IFMT) == Posix.S_IFCHR && Posix.major (stat.st_rdev) == 226; + } + +} diff --git a/src/Managers/ProcessManager.vala b/src/Managers/ProcessManager.vala index 69033027c..fb33c30f6 100644 --- a/src/Managers/ProcessManager.vala +++ b/src/Managers/ProcessManager.vala @@ -259,7 +259,8 @@ namespace Monitor { */ private Process ? add_process (int pid, bool lazy_signal = false) { // create the process - var process = new Process (pid); + int update_interval = MonitorApp.settings.get_int ("update-time"); + var process = new Process (pid, update_interval); if (!process.exists) { return null; diff --git a/src/Models/ProcessRowData.vala b/src/Models/ProcessRowData.vala index 369c2973a..65e194382 100644 --- a/src/Models/ProcessRowData.vala +++ b/src/Models/ProcessRowData.vala @@ -3,12 +3,15 @@ * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) */ -/* This class holds data from Process class to use in the ColumnView */ +/** + * This class holds data from Process class to use in the ColumnView + */ public class Monitor.ProcessRowData : GLib.Object { public Icon icon { get; set; } public string name { get; set; } public int cpu { get; set; } public uint64 memory { get; set; } + public int gpu { get; set; } public int pid { get; set; } public string cmd { get; set; } public Gee.HashMap bindings = new Gee.HashMap (); diff --git a/src/Models/TreeViewModel.vala b/src/Models/TreeViewModel.vala index 15c566785..17be76c6a 100644 --- a/src/Models/TreeViewModel.vala +++ b/src/Models/TreeViewModel.vala @@ -83,6 +83,7 @@ public class Monitor.TreeViewModel : GLib.Object { name = process.application_name, cpu = (int) process.cpu_percentage, memory = process.mem_usage, + gpu = (int) process.gpu_percentage, pid = process.stat.pid, cmd = process.command }; @@ -112,6 +113,7 @@ public class Monitor.TreeViewModel : GLib.Object { var item = (ProcessRowData) store.get_item (pos); item.cpu = (int) process.cpu_percentage; item.memory = process.mem_usage; + item.gpu = (int) process.gpu_percentage; sorter.changed (DIFFERENT); } } diff --git a/src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala b/src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala index dc790d1d8..144e4da33 100644 --- a/src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala +++ b/src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala @@ -29,6 +29,11 @@ public class Monitor.ProcessTreeView : Granite.Bin { memory_item_factory.bind.connect (memory_item_factory_bind); memory_item_factory.unbind.connect (memory_item_factory_unbind); + var gpu_item_factory = new Gtk.SignalListItemFactory (); + gpu_item_factory.setup.connect (generic_item_factory_setup); + gpu_item_factory.bind.connect (gpu_item_factory_bind); + gpu_item_factory.unbind.connect (gpu_item_factory_unbind); + var pid_item_factory = new Gtk.SignalListItemFactory (); pid_item_factory.setup.connect (generic_item_factory_setup); pid_item_factory.bind.connect (pid_item_factory_bind); @@ -52,6 +57,12 @@ public class Monitor.ProcessTreeView : Granite.Bin { }; column_view.append_column (mem_column); + var gpu_column = new Gtk.ColumnViewColumn (_("GPU"), gpu_item_factory) { + sorter = model.num_sorter ("gpu"), + expand = false + }; + column_view.append_column (gpu_column); + var pid_column = new Gtk.ColumnViewColumn (_("PID"), pid_item_factory) { sorter = model.num_sorter ("pid"), expand = false @@ -144,6 +155,26 @@ public class Monitor.ProcessTreeView : Granite.Bin { item.bindings["memory"].unbind (); } + private void gpu_item_factory_bind (Object object) { + var cell = (Gtk.ColumnViewCell) object; + var label = (Gtk.Label) cell.child; + var item = (ProcessRowData) cell.item; + var binding_gpu = item.bind_property ("gpu", label, "label", SYNC_CREATE, (_, from_val, ref to_val) => { + int percentage = from_val.get_int (); + to_val.set_string ("%.0f%%".printf (percentage)); + return true; + }); + item.bindings.set ("gpu", binding_gpu); + } + + private void gpu_item_factory_unbind (Object object) { + var cell = (Gtk.ColumnViewCell) object; + var label = (Gtk.Label) cell.child; + var item = (ProcessRowData) cell.item; + label.label = null; + item.bindings["gpu"].unbind (); + } + private void pid_item_factory_bind (Object object) { var cell = (Gtk.ColumnViewCell) object; var label = (Gtk.Label) cell.child; diff --git a/src/meson.build b/src/meson.build index 036d04339..43377f327 100644 --- a/src/meson.build +++ b/src/meson.build @@ -44,6 +44,7 @@ source_app_files = [ 'Managers/Process.vala', 'Managers/ProcessStructs.vala', 'Managers/ProcessUtils.vala', + 'Managers/ProcessDRM.vala', # Services 'Services/DBusServer.vala',