Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ jobs:
export XDG_RUNTIME_DIR="$(mktemp --tmpdir -d xdg-runtime-XXXXXX)"
weston --backend=headless --socket=wayland-1 --idle-time=0 &
dbus-run-session -- env WAYLAND_DISPLAY=wayland-1 meson test -C build --suite monitor-gui-headless "*:" --print-errorlogs
# non-headless gui example: meson test -C build --suite monitor "*:" --print-errorlogs
# This test has been disabled, because it wasn't running correctly on CI
# dbus-run-session -- env WAYLAND_DISPLAY=wayland-1 meson test -C build --suite monitor-backend "*:" --print-errorlogs
3 changes: 2 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ app_dependencies = [

config_data = configuration_data()
config_data.set('GETTEXT_PACKAGE', meson.project_name())

subdir('data')

config_data.set('TESTASSETSDIR', meson.project_build_root () / 'tests/assets/')

config_data.set('VCS_TAG', '@VCS_TAG@')
project_config = vcs_tag(
input: configure_file(
Expand Down
1 change: 1 addition & 0 deletions src/Conf.vala.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Monitor {
public const string DBDIR = "@DBDIR@";
public const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@";
public const string TESTASSETSDIR = "@TESTASSETSDIR@";
public const string VCS_TAG = "@VCS_TAG@";
}
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
192 changes: 192 additions & 0 deletions src/Managers/ProcessDRM.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2026 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 = 0;
private uint64 last_engine_gfx = 0;

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;

internal string path_fdinfo;
internal string path_fd;

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

path_fdinfo = "/proc/%d/fdinfo".printf (pid);
path_fd = "/proc/%d/fd".printf (pid);

get_drm_files ();
}

public ProcessDRM.with_paths (int pid, int update_interval, string path_fdinfo, string path_fd) {
this.pid = pid;
this.update_interval = update_interval;
this.path_fdinfo = path_fdinfo;
this.path_fd = path_fd;

get_drm_files ();
}

private void get_drm_files () {
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 = ProcessUtils.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;
}

// Every GPU driver creates a bit different DRM file content,
// so we need to use a specific gpu percentage calculation functions
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
gpu_percentage = -1;
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
// the 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 fraction = (float) delta / (float) delta_total;
gpu_percentage = delta_total > 0 ? 100 * (fraction.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;
}

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
8 changes: 8 additions & 0 deletions src/Managers/ProcessUtils.vala
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,12 @@ public class Monitor.ProcessUtils {
return null;
}
}

// Based on nvtop
// https://github.com/Syllo/nvtop/blob/4bf5db248d7aa7528f3a1ab7c94f504dff6834e4/src/extract_processinfo_fdinfo.c#L88
public 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;
}
}
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
6 changes: 6 additions & 0 deletions src/Resources/Resources.vala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
*/

public class Monitor.Resources : Object {

private static GLib.Once<Resources> instance;
public static unowned Resources get_default () {
return instance.once (() => { return new Resources (); });
}

public CPU cpu;
public Memory memory;
public Swap swap;
Expand Down
Loading
Loading