Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions src/Managers/Process.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<double ? > cpu_percentage_history = new Gee.ArrayList<double ? > ();
public Gee.ArrayList<double ? > mem_percentage_history = new Gee.ArrayList<double ? > ();
public Gee.ArrayList<double ?> cpu_percentage_history = new Gee.ArrayList<double?> ();
public Gee.ArrayList<double ?> mem_percentage_history = new Gee.ArrayList<double?> ();



// 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<string> ();

last_total = 0;

drm = new ProcessDRM (_pid, update_interval);

io = {};
stat = {};
stat.pid = _pid;
Expand All @@ -101,15 +107,18 @@ 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
public bool update (uint64 cpu_total, uint64 cpu_last_total) {
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 ();
Expand Down Expand Up @@ -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));

Expand All @@ -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;
Expand Down
188 changes: 188 additions & 0 deletions src/Managers/ProcessDRM.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
*/

public class Monitor.ProcessDRM : GLib.Object {

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_ccs = 0;
private uint64 cycles_ccs_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<GLib.File> drm_files;

public ProcessDRM (int pid, int update_interval) {
this.pid = pid;
this.update_interval = update_interval;

last_engine_render = 0;
last_engine_gfx = 0;

get_drm_files ();
}

private void get_drm_files () {
string path_fdinfo = "/proc/%d/fdinfo".printf (pid);
string path_fd = "/proc/%d/fd".printf (pid);

drm_files = new Gee.ArrayList<GLib.File ?> ();

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);
debug ("Found DRM file: %s", path);
}
}
} catch (FileError err) {
// prevent flooding logs with permission errors
if (!(err is FileError.ACCES)) {
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 {
var dis = new DataInputStream (drm_file.read ());
string ? line;
while ((line = dis.read_line ()) != null) {
parse_drm_line (line);
}
} catch (Error err) {
if (!(err is FileError.ACCES)) {
warning ("Can't read fdinfo: '%s' %d", err.message, err.code);
}
}
break;
}

switch (driver) {
case "i915":
calculate_percentage_ns (ref engine_render, ref last_engine_render);
break;
case "xe":
calculate_percentage_cycles (ref delta_rcs, ref delta_total_rcs);
break;
case "amdgpu":
calculate_percentage_ns (ref engine_gfx, ref last_engine_gfx);
break;
default:
// Handle default case
break;
}
}

private void calculate_percentage_ns (ref uint64 engine, ref uint64 last_engine) {
if (last_engine != 0) {
// 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;
last_cycles = cycles;
}

// 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;
}

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":
update_cycles (splitted_line[1], ref cycles_rcs, ref delta_rcs);
break;
case "drm-total-cycles-rcs":
update_cycles (splitted_line[1], ref cycles_rcs_total, ref delta_total_rcs);
break;
default:
// Ignore other entries
break;
}
}

}
3 changes: 2 additions & 1 deletion src/Managers/ProcessManager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/Models/ProcessRowData.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Binding> bindings = new Gee.HashMap<string, Binding> ();
Expand Down
2 changes: 2 additions & 0 deletions src/Models/TreeViewModel.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down Expand Up @@ -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);
}
}
Expand Down
31 changes: 31 additions & 0 deletions src/Views/ProcessView/ProcessTreeView/ProcessTreeView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ source_app_files = [
'Managers/Process.vala',
'Managers/ProcessStructs.vala',
'Managers/ProcessUtils.vala',
'Managers/ProcessDRM.vala',

# Services
'Services/DBusServer.vala',
Expand Down
Loading