Skip to content

Commit cad6161

Browse files
committed
Introduce per process GPU usage metrics
1 parent 16ec6cd commit cad6161

7 files changed

Lines changed: 176 additions & 10 deletions

File tree

src/Managers/Process.vala

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public class Monitor.Process : GLib.Object {
4242
// Contains info about io
4343
public ProcessIO io;
4444

45+
// Contains info about GPU usage
46+
private ProcessDRM drm;
47+
4548
// Contains status info
4649
public ProcessStatus stat;
4750

@@ -61,26 +64,29 @@ public class Monitor.Process : GLib.Object {
6164
private uint64 cpu_last_used;
6265

6366
// Memory usage of the process, measured in KiB.
64-
6567
public uint64 mem_usage { get; private set; }
6668
public double mem_percentage { get; private set; }
6769

68-
private uint64 last_total;
70+
public double gpu_percentage { get; private set; }
71+
72+
private uint64 last_total; // @TODO: Obsolete?
6973

7074
const int HISTORY_BUFFER_SIZE = 30;
71-
public Gee.ArrayList<double ? > cpu_percentage_history = new Gee.ArrayList<double ? > ();
72-
public Gee.ArrayList<double ? > mem_percentage_history = new Gee.ArrayList<double ? > ();
75+
public Gee.ArrayList<double ?> cpu_percentage_history = new Gee.ArrayList<double?> ();
76+
public Gee.ArrayList<double ?> mem_percentage_history = new Gee.ArrayList<double?> ();
7377

7478

7579

7680
// Construct a new process
77-
public Process (int _pid) {
81+
public Process (int _pid, int update_interval) {
7882
_icon = ProcessUtils.get_default_icon ();
7983

8084
open_files_paths = new Gee.HashSet<string> ();
8185

8286
last_total = 0;
8387

88+
drm = new ProcessDRM (_pid, update_interval);
89+
8490
io = {};
8591
stat = {};
8692
stat.pid = _pid;
@@ -101,15 +107,18 @@ public class Monitor.Process : GLib.Object {
101107
exists = parse_stat () && read_cmdline ();
102108
get_children_pids ();
103109
get_usage (0, 1);
104-
}
105110

111+
gpu_percentage = 0;
112+
}
106113

107114
// Updates the process to get latest information
108115
// Returns if the update was successful
109116
public bool update (uint64 cpu_total, uint64 cpu_last_total) {
110117
exists = parse_stat ();
111118
if (exists) {
112119
get_usage (cpu_total, cpu_last_total);
120+
drm.update ();
121+
gpu_percentage = drm.gpu_percentage;
113122
parse_io ();
114123
parse_statm ();
115124
get_open_files ();
@@ -280,8 +289,8 @@ public class Monitor.Process : GLib.Object {
280289
}
281290

282291
/**
283-
* Reads the /proc/%pid%/cmdline file and updates from the information contained therein.
284-
*/
292+
* Reads the /proc/%pid%/cmdline file and updates from the information contained therein.
293+
*/
285294
private bool read_cmdline () {
286295
string ? cmdline = ProcessUtils.read_file ("/proc/%d/cmdline".printf (stat.pid));
287296

@@ -301,6 +310,7 @@ public class Monitor.Process : GLib.Object {
301310
return true;
302311
}
303312

313+
// @TODO: Divide into get_usage_cpu and get_usage_mem and write some tests
304314
private void get_usage (uint64 cpu_total, uint64 cpu_last_total) {
305315
// Get CPU usage by process
306316
GTop.ProcTime proc_time;

src/Managers/ProcessDRM.vala

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-or-later
3+
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
4+
*/
5+
6+
public class Monitor.ProcessDRM {
7+
/**
8+
* Time spent busy in nanoseconds by the render engine executing
9+
* workloads from the last time it was read
10+
*/
11+
private uint64 last_engine_render;
12+
private uint64 last_engine_gfx;
13+
14+
15+
public double gpu_percentage { get; private set; }
16+
17+
private int pid;
18+
private int update_interval;
19+
20+
public ProcessDRM (int pid, int update_interval) {
21+
this.pid = pid;
22+
this.update_interval = update_interval;
23+
24+
last_engine_render = 0;
25+
last_engine_gfx = 0;
26+
}
27+
28+
public void update () {
29+
string path_fdinfo = "/proc/%d/fdinfo".printf (pid);
30+
string path_fd = "/proc/%d/fd".printf (pid);
31+
32+
33+
var drm_files = new Gee.ArrayList<GLib.File ?> ();
34+
35+
try {
36+
Dir dir = Dir.open (path_fdinfo, 0);
37+
string ? name = null;
38+
39+
while ((name = dir.read_name ()) != null) {
40+
41+
// skip standard fds
42+
if (name == "0" || name == "1" || name == "2") {
43+
continue;
44+
}
45+
string path = Path.build_filename (path_fdinfo, name);
46+
47+
int fd_dir_fd = Posix.open (path_fd, Posix.O_RDONLY | Posix.O_DIRECTORY);
48+
if (fd_dir_fd == -1) {
49+
warning ("Cannot open file descriptor: %s", path_fd);
50+
continue;
51+
}
52+
53+
bool is_drm = is_drm_fd (fd_dir_fd, name);
54+
Posix.close (fd_dir_fd);
55+
56+
if (is_drm) {
57+
var drm_file = File.new_for_path (path);
58+
drm_files.add (drm_file);
59+
}
60+
}
61+
} catch (FileError err) {
62+
// prevent flooding logs with permission errors
63+
if (!(err is FileError.ACCES)) {
64+
warning (err.message);
65+
}
66+
}
67+
68+
foreach (var drm_file in drm_files) {
69+
try {
70+
var dis = new DataInputStream (drm_file.read ());
71+
string ? line;
72+
73+
while ((line = dis.read_line ()) != null) {
74+
var splitted_line = line.split (":");
75+
switch (splitted_line[0]) {
76+
case "drm-engine-gfx":
77+
update_engine (splitted_line[1], ref last_engine_gfx);
78+
break;
79+
// for i915 there is only drm-engine-render to check
80+
case "drm-engine-render":
81+
update_engine (splitted_line[1], ref last_engine_render);
82+
break;
83+
default:
84+
// Ignore other entries
85+
break;
86+
}
87+
}
88+
} catch (Error err) {
89+
if (!(err is FileError.ACCES)) {
90+
warning ("Can't read fdinfo: '%s' %d", err.message, err.code);
91+
}
92+
}
93+
break;
94+
}
95+
}
96+
97+
private void update_engine (string line, ref uint64 last_engine) {
98+
var engine = uint64.parse (line.strip ().split (" ")[0]);
99+
if (last_engine != 0) {
100+
gpu_percentage = calculate_percentage (engine, last_engine, update_interval);
101+
}
102+
last_engine = engine;
103+
}
104+
105+
private static double calculate_percentage (uint64 engine, uint64 last_engine, int interval) {
106+
// Since values in the files are in nanoseconds, it is also needed to convert interval to nanoseconds (10^9)
107+
return 100 * ((double) (engine - last_engine)) / (interval * 1e9);
108+
}
109+
110+
// Based on nvtop
111+
// https://github.com/Syllo/nvtop/blob/4bf5db248d7aa7528f3a1ab7c94f504dff6834e4/src/extract_processinfo_fdinfo.c#L88
112+
private static bool is_drm_fd (int fd_dir_fd, string name) {
113+
Posix.Stat stat;
114+
int ret = Posix.fstatat (fd_dir_fd, name, out stat, 0);
115+
return ret == 0 && (stat.st_mode & Posix.S_IFMT) == Posix.S_IFCHR && Posix.major (stat.st_rdev) == 226;
116+
}
117+
118+
}

src/Managers/ProcessManager.vala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ namespace Monitor {
259259
*/
260260
private Process ? add_process (int pid, bool lazy_signal = false) {
261261
// create the process
262-
var process = new Process (pid);
262+
int update_interval = MonitorApp.settings.get_int ("update-time");
263+
var process = new Process (pid, update_interval);
263264

264265
if (!process.exists) {
265266
return null;

src/Models/ProcessRowData.vala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
* SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io)
44
*/
55

6-
/* This class holds data from Process class to use in the ColumnView */
6+
/**
7+
* This class holds data from Process class to use in the ColumnView
8+
*/
79
public class Monitor.ProcessRowData : GLib.Object {
810
public Icon icon { get; set; }
911
public string name { get; set; }
1012
public int cpu { get; set; }
1113
public uint64 memory { get; set; }
14+
public int gpu { get; set; }
1215
public int pid { get; set; }
1316
public string cmd { get; set; }
1417
public Gee.HashMap<string, Binding> bindings = new Gee.HashMap<string, Binding> ();

src/Models/TreeViewModel.vala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public class Monitor.TreeViewModel : GLib.Object {
8383
name = process.application_name,
8484
cpu = (int) process.cpu_percentage,
8585
memory = process.mem_usage,
86+
gpu = (int) process.gpu_percentage,
8687
pid = process.stat.pid,
8788
cmd = process.command
8889
};
@@ -112,6 +113,7 @@ public class Monitor.TreeViewModel : GLib.Object {
112113
var item = (ProcessRowData) store.get_item (pos);
113114
item.cpu = (int) process.cpu_percentage;
114115
item.memory = process.mem_usage;
116+
item.gpu = (int) process.gpu_percentage;
115117
sorter.changed (DIFFERENT);
116118
}
117119
}

src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public class Monitor.ProcessTreeView : Granite.Bin {
2929
memory_item_factory.bind.connect (memory_item_factory_bind);
3030
memory_item_factory.unbind.connect (memory_item_factory_unbind);
3131

32+
var gpu_item_factory = new Gtk.SignalListItemFactory ();
33+
gpu_item_factory.setup.connect (generic_item_factory_setup);
34+
gpu_item_factory.bind.connect (gpu_item_factory_bind);
35+
gpu_item_factory.unbind.connect (gpu_item_factory_unbind);
36+
3237
var pid_item_factory = new Gtk.SignalListItemFactory ();
3338
pid_item_factory.setup.connect (generic_item_factory_setup);
3439
pid_item_factory.bind.connect (pid_item_factory_bind);
@@ -52,6 +57,12 @@ public class Monitor.ProcessTreeView : Granite.Bin {
5257
};
5358
column_view.append_column (mem_column);
5459

60+
var gpu_column = new Gtk.ColumnViewColumn (_("GPU"), gpu_item_factory) {
61+
sorter = model.num_sorter ("gpu"),
62+
expand = false
63+
};
64+
column_view.append_column (gpu_column);
65+
5566
var pid_column = new Gtk.ColumnViewColumn (_("PID"), pid_item_factory) {
5667
sorter = model.num_sorter ("pid"),
5768
expand = false
@@ -144,6 +155,26 @@ public class Monitor.ProcessTreeView : Granite.Bin {
144155
item.bindings["memory"].unbind ();
145156
}
146157

158+
private void gpu_item_factory_bind (Object object) {
159+
var cell = (Gtk.ColumnViewCell) object;
160+
var label = (Gtk.Label) cell.child;
161+
var item = (ProcessRowData) cell.item;
162+
var binding_gpu = item.bind_property ("gpu", label, "label", SYNC_CREATE, (_, from_val, ref to_val) => {
163+
int percentage = from_val.get_int ();
164+
to_val.set_string ("%.0f%%".printf (percentage));
165+
return true;
166+
});
167+
item.bindings.set ("gpu", binding_gpu);
168+
}
169+
170+
private void gpu_item_factory_unbind (Object object) {
171+
var cell = (Gtk.ColumnViewCell) object;
172+
var label = (Gtk.Label) cell.child;
173+
var item = (ProcessRowData) cell.item;
174+
label.label = null;
175+
item.bindings["gpu"].unbind ();
176+
}
177+
147178
private void pid_item_factory_bind (Object object) {
148179
var cell = (Gtk.ColumnViewCell) object;
149180
var label = (Gtk.Label) cell.child;

src/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ source_app_files = [
4444
'Managers/Process.vala',
4545
'Managers/ProcessStructs.vala',
4646
'Managers/ProcessUtils.vala',
47+
'Managers/ProcessDRM.vala',
4748

4849
# Services
4950
'Services/DBusServer.vala',

0 commit comments

Comments
 (0)