From cad61614e223f7ec98a4e7d2ea1e9f690e1a28ad Mon Sep 17 00:00:00 2001 From: stsdc <6031763+stsdc@users.noreply.github.com> Date: Sat, 30 May 2026 15:55:20 +0200 Subject: [PATCH 1/4] Introduce per process GPU usage metrics --- src/Managers/Process.vala | 26 ++-- src/Managers/ProcessDRM.vala | 118 ++++++++++++++++++ src/Managers/ProcessManager.vala | 3 +- src/Models/ProcessRowData.vala | 5 +- src/Models/TreeViewModel.vala | 2 + .../ProcessTreeView/ProcessTreeView.vala | 31 +++++ src/meson.build | 1 + 7 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/Managers/ProcessDRM.vala 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', From bc65e109d4989f1f6a7e4ff1fbabff0e38cef035 Mon Sep 17 00:00:00 2001 From: stsdc <6031763+stsdc@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:58:29 +0200 Subject: [PATCH 2/4] Introduce per process GPU measure for Xe driver --- src/Managers/ProcessDRM.vala | 118 +++++++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 19 deletions(-) diff --git a/src/Managers/ProcessDRM.vala b/src/Managers/ProcessDRM.vala index b6d468e84..f3b9f7818 100644 --- a/src/Managers/ProcessDRM.vala +++ b/src/Managers/ProcessDRM.vala @@ -4,18 +4,42 @@ */ public class Monitor.ProcessDRM { - /** + + private string driver; + + /** * 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; + private uint64 engine_gfx; + private uint64 engine_render; + + // Xe driver related fields + private uint64 cycles_rcs = 0; + private uint64 cycles_rcs_total = 0; + private uint64 cycles_bcs = 0; + private uint64 cycles_bcs_total = 0; + private uint64 cycles_vcs = 0; + private uint64 cycles_vcs_total = 0; + private uint64 cycles_ccs = 0; + private uint64 cycles_ccs_total = 0; + private uint64 cycles_vecs = 0; + private uint64 cycles_vecs_total = 0; + + private uint64 delta_rcs = 0; + private uint64 delta_total_rcs = 0; + + private uint64 delta_ccs = 0; + private uint64 delta_total_ccs = 0; public double gpu_percentage { get; private set; } private int pid; private int update_interval; + private Gee.ArrayList drm_files; public ProcessDRM (int pid, int update_interval) { this.pid = pid; @@ -23,14 +47,15 @@ public class Monitor.ProcessDRM { last_engine_render = 0; last_engine_gfx = 0; + + get_drm_files (); } - public void update () { + private void get_drm_files () { string path_fdinfo = "/proc/%d/fdinfo".printf (pid); string path_fd = "/proc/%d/fd".printf (pid); - - var drm_files = new Gee.ArrayList (); + drm_files = new Gee.ArrayList (); try { Dir dir = Dir.open (path_fdinfo, 0); @@ -56,6 +81,7 @@ public class Monitor.ProcessDRM { if (is_drm) { var drm_file = File.new_for_path (path); drm_files.add (drm_file); + debug ("Found DRM file: %s", path); } } } catch (FileError err) { @@ -64,26 +90,23 @@ public class Monitor.ProcessDRM { warning (err.message); } } + // debug ("Found %d drm fdinfo files for pid %d", drm_files.size, pid); + } + + public void update () { + if (drm_files.size == 0) { + gpu_percentage = 0; + return; + } foreach (var drm_file in drm_files) { try { + debug ("Reading fdinfo from: %s", drm_file.get_path ()); 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; - } + parse_drm_line (line); } } catch (Error err) { if (!(err is FileError.ACCES)) { @@ -92,16 +115,39 @@ public class Monitor.ProcessDRM { } break; } + + switch (driver) { + case "i915": + update_engine (ref engine_render, ref last_engine_render); + break; + case "xe": + var pre = (float) delta_rcs / (float) delta_total_rcs; + gpu_percentage = delta_total_rcs > 0 ? 100 * (pre.clamp (0.0f, 1.0f)) : 0; + break; + case "amdgpu": + update_engine (ref engine_gfx, ref last_engine_gfx); + break; + default: + // Handle default case + break; + } + } - private void update_engine (string line, ref uint64 last_engine) { - var engine = uint64.parse (line.strip ().split (" ")[0]); + private void update_engine (ref uint64 engine, ref uint64 last_engine) { if (last_engine != 0) { gpu_percentage = calculate_percentage (engine, last_engine, update_interval); } last_engine = engine; } + private void update_cycles (string line, ref uint64 last_cycles, ref uint64 delta) { + var cycles = uint64.parse (line.strip ().split (" ")[0]); + delta = cycles > last_cycles ? cycles - last_cycles : 0; + // debug ("pid %d Cycles: %llu, Last Cycles: %llu, Delta: %llu", pid, cycles, last_cycles, delta); + last_cycles = cycles; + } + 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); @@ -115,4 +161,38 @@ public class Monitor.ProcessDRM { return ret == 0 && (stat.st_mode & Posix.S_IFMT) == Posix.S_IFCHR && Posix.major (stat.st_rdev) == 226; } + private void parse_drm_line (string line) { + var splitted_line = line.split (":"); + switch (splitted_line[0]) { + case "drm-driver": + driver = splitted_line[1].strip (); + break; + case "drm-engine-gfx": + engine_gfx = uint64.parse (splitted_line[1].strip ().split (" ")[0]); + break; + // for i915 there is only drm-engine-render to check + case "drm-engine-render": + engine_render = uint64.parse (splitted_line[1].strip ().split (" ")[0]); + break; + // Xe driver specific entries + case "drm-cycles-ccs": + update_cycles (splitted_line[1], ref cycles_ccs, ref delta_ccs); + break; + case "drm-total-cycles-ccs": + update_cycles (splitted_line[1], ref cycles_ccs_total, ref delta_total_ccs); + break; + case "drm-cycles-rcs": + // debug ("path: %s, line: %s", drm_file.get_path (), line); + update_cycles (splitted_line[1], ref cycles_rcs, ref delta_rcs); + break; + case "drm-total-cycles-rcs": + // debug ("path: %s, line: %s", drm_file.get_path (), line); + update_cycles (splitted_line[1], ref cycles_rcs_total, ref delta_total_rcs); + break; + default: + // Ignore other entries + break; + } + } + } From af1e3709302039370e09daec6b4c9b581649746d Mon Sep 17 00:00:00 2001 From: stsdc <6031763+stsdc@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:35:27 +0200 Subject: [PATCH 3/4] cleanup --- src/Managers/ProcessDRM.vala | 41 +++++++++++++----------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/Managers/ProcessDRM.vala b/src/Managers/ProcessDRM.vala index f3b9f7818..c2a877553 100644 --- a/src/Managers/ProcessDRM.vala +++ b/src/Managers/ProcessDRM.vala @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ -public class Monitor.ProcessDRM { +public class Monitor.ProcessDRM : GLib.Object { private string driver; @@ -20,14 +20,9 @@ public class Monitor.ProcessDRM { // Xe driver related fields private uint64 cycles_rcs = 0; private uint64 cycles_rcs_total = 0; - private uint64 cycles_bcs = 0; - private uint64 cycles_bcs_total = 0; - private uint64 cycles_vcs = 0; - private uint64 cycles_vcs_total = 0; + private uint64 cycles_ccs = 0; private uint64 cycles_ccs_total = 0; - private uint64 cycles_vecs = 0; - private uint64 cycles_vecs_total = 0; private uint64 delta_rcs = 0; private uint64 delta_total_rcs = 0; @@ -42,8 +37,7 @@ public class Monitor.ProcessDRM { private Gee.ArrayList drm_files; public ProcessDRM (int pid, int update_interval) { - this.pid = pid; - this.update_interval = update_interval; + Object (pid: pid, update_interval: update_interval); last_engine_render = 0; last_engine_gfx = 0; @@ -101,10 +95,8 @@ public class Monitor.ProcessDRM { foreach (var drm_file in drm_files) { try { - debug ("Reading fdinfo from: %s", drm_file.get_path ()); var dis = new DataInputStream (drm_file.read ()); string ? line; - while ((line = dis.read_line ()) != null) { parse_drm_line (line); } @@ -118,41 +110,40 @@ public class Monitor.ProcessDRM { switch (driver) { case "i915": - update_engine (ref engine_render, ref last_engine_render); + calculate_percentage_ns (ref engine_render, ref last_engine_render); break; case "xe": - var pre = (float) delta_rcs / (float) delta_total_rcs; - gpu_percentage = delta_total_rcs > 0 ? 100 * (pre.clamp (0.0f, 1.0f)) : 0; + calculate_percentage_cycles (ref delta_rcs, ref delta_total_rcs); break; case "amdgpu": - update_engine (ref engine_gfx, ref last_engine_gfx); + calculate_percentage_ns (ref engine_gfx, ref last_engine_gfx); break; default: // Handle default case break; } - } - private void update_engine (ref uint64 engine, ref uint64 last_engine) { + private void calculate_percentage_ns (ref uint64 engine, ref uint64 last_engine) { if (last_engine != 0) { - gpu_percentage = calculate_percentage (engine, last_engine, update_interval); + // Since values in the files are in nanoseconds, it is also needed to convert + // interval to nanoseconds (10^9) + gpu_percentage = 100 * ((double) (engine - last_engine)) / (update_interval * 1e9); } last_engine = engine; } + private void calculate_percentage_cycles (ref uint64 delta, ref uint64 delta_total) { + var pre = (float) delta / (float) delta_total; + gpu_percentage = delta_total > 0 ? 100 * (pre.clamp (0.0f, 1.0f)) : 0; + } + private void update_cycles (string line, ref uint64 last_cycles, ref uint64 delta) { var cycles = uint64.parse (line.strip ().split (" ")[0]); delta = cycles > last_cycles ? cycles - last_cycles : 0; - // debug ("pid %d Cycles: %llu, Last Cycles: %llu, Delta: %llu", pid, cycles, last_cycles, delta); last_cycles = cycles; } - 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) { @@ -182,11 +173,9 @@ public class Monitor.ProcessDRM { update_cycles (splitted_line[1], ref cycles_ccs_total, ref delta_total_ccs); break; case "drm-cycles-rcs": - // debug ("path: %s, line: %s", drm_file.get_path (), line); update_cycles (splitted_line[1], ref cycles_rcs, ref delta_rcs); break; case "drm-total-cycles-rcs": - // debug ("path: %s, line: %s", drm_file.get_path (), line); update_cycles (splitted_line[1], ref cycles_rcs_total, ref delta_total_rcs); break; default: From 15e67b41191d3baefc57ac84be87792d9865e0e9 Mon Sep 17 00:00:00 2001 From: stsdc <6031763+stsdc@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:59:52 +0200 Subject: [PATCH 4/4] Fix constructor initialization in ProcessDRM class --- src/Managers/ProcessDRM.vala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Managers/ProcessDRM.vala b/src/Managers/ProcessDRM.vala index c2a877553..950422bad 100644 --- a/src/Managers/ProcessDRM.vala +++ b/src/Managers/ProcessDRM.vala @@ -37,7 +37,8 @@ public class Monitor.ProcessDRM : GLib.Object { private Gee.ArrayList drm_files; public ProcessDRM (int pid, int update_interval) { - Object (pid: pid, update_interval: update_interval); + this.pid = pid; + this.update_interval = update_interval; last_engine_render = 0; last_engine_gfx = 0;