feat: Added priority and nice as columns in process view (#1881)

* basic priority and nice functionality

* fix fmt, clippy and tests

* trying to fix other platform build errors

* fmt fix

* trying more fixes

* clean project

* refactored nice to be for all unix systems

* few more places where I had to change cfg for Nice

* Fix scheme names issues

* fix for schema

* fmt

* fixed clippy errors

* 'nice' fix

* fmt

* fixed cfg in tests also

* trying to fix macos test errors

* fix nice value extraction for mac

* modularised nice and priority for mac and freebsd

* fmt

* more multi target to unix

* removing unnecesary guards

* added safety comments

- also reverted string array reference changes

utils fix

* weird utils clippy error fix

* removed unneeded commit

* not needed after main merge(?)
This commit is contained in:
Adarsh Das
2025-12-22 01:01:43 +05:30
committed by GitHub
parent 0160251e29
commit 102ae6b247
13 changed files with 199 additions and 25 deletions
Generated
+12
View File
@@ -227,6 +227,7 @@ dependencies = [
"unicode-segmentation",
"unicode-width",
"windows 0.62.0",
"winprocinfo",
]
[[package]]
@@ -2840,6 +2841,17 @@ dependencies = [
"memchr",
]
[[package]]
name = "winprocinfo"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d010942d0ed74baf25362e267b54d4f57b859d0cb9b2dc7040e26333a87e1d8"
dependencies = [
"ntapi",
"regex",
"winapi",
]
[[package]]
name = "winreg"
version = "0.10.1"
+1
View File
@@ -131,6 +131,7 @@ windows = { version = "0.62.0", features = [
"Win32_System_ProcessStatus",
"Win32_System_Threading",
] }
winprocinfo = "0.1.2"
[target.'cfg(target_os = "freebsd")'.dependencies]
serde_json = { version = "1.0.145" }
+7
View File
@@ -147,6 +147,13 @@ pub struct ProcessHarvest {
/// The process entry "type".
#[cfg(target_os = "linux")]
pub process_type: ProcessType,
/// The nice value (user-settable scheduling hint).
#[cfg(unix)]
pub nice: i32,
/// The kernel scheduling priority.
pub priority: i32,
// TODO: Additional fields
// pub rss_kb: u64,
// pub virt_kb: u64,
+3
View File
@@ -285,6 +285,9 @@ fn read_proc(
#[cfg(feature = "gpu")]
gpu_util: 0,
process_type,
#[cfg(unix)]
nice: stat.nice,
priority: stat.priority,
},
new_process_times,
))
+22 -4
View File
@@ -62,6 +62,13 @@ pub(crate) struct Stat {
/// Kernel thread
pub is_kernel_thread: bool,
/// The kernel scheduling priority.
pub priority: i32,
/// The nice value (user-settable scheduling hint).
#[cfg(unix)]
pub nice: i32,
}
impl Stat {
@@ -107,11 +114,20 @@ impl Stat {
let utime: u64 = next_part(&mut rest)?.parse()?;
let stime: u64 = next_part(&mut rest)?.parse()?;
// Skip 6 fields until starttime (cutime, cstime, priority, nice, num_threads,
// itrealvalue).
let mut rest = rest.skip(6);
let start_time: u64 = next_part(&mut rest)?.parse()?;
// cutime
let _ = next_part(&mut rest)?;
// cstime
let _ = next_part(&mut rest)?;
// priority
let priority: i32 = next_part(&mut rest)?.parse()?;
// nice
let nice: i32 = next_part(&mut rest)?.parse()?;
// num_threads
let _ = next_part(&mut rest)?;
// itrealvalue
let _ = next_part(&mut rest)?;
let start_time: u64 = next_part(&mut rest)?.parse()?;
let vsize: u64 = next_part(&mut rest)?.parse()?;
let rss: u64 = next_part(&mut rest)?.parse()?;
@@ -125,6 +141,8 @@ impl Stat {
vsize,
start_time,
is_kernel_thread,
priority,
nice,
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
//! Process data collection for macOS. Uses sysinfo and custom bindings.
mod sysctl_bindings;
pub mod sysctl_bindings;
use std::{io, process::Command};
+62 -5
View File
@@ -1,5 +1,9 @@
//! Shared process data harvesting code from macOS and FreeBSD via sysinfo.
#[cfg(target_os = "macos")]
use crate::collection::processes::macos::sysctl_bindings;
use cfg_if::cfg_if;
use std::{io, time::Duration};
use itertools::Itertools;
@@ -9,6 +13,58 @@ use sysinfo::{ProcessStatus, System};
use super::{ProcessHarvest, process_status_str};
use crate::collection::{Pid, error::CollectionResult, processes::UserTable};
fn get_nice(pid: Pid) -> i32 {
// SAFETY: getpriority takes no user pointers; pid is passed as a value
// and errors are reported via the return value.
cfg_if! {
if #[cfg(target_os = "freebsd")] {
unsafe { libc::getpriority(libc::PRIO_PROCESS, pid) }
} else if #[cfg(target_os = "macos")] {
unsafe { libc::getpriority(libc::PRIO_PROCESS, pid as u32) }
} else {
0
}
}
}
fn get_priority(pid: Pid) -> i32 {
cfg_if! {
if #[cfg(target_os = "macos")] {
if let Ok(kinfo) = sysctl_bindings::kinfo_process(pid) {
kinfo.kp_proc.p_priority as i32
} else {
0
}
} else if #[cfg(target_os = "freebsd")] {
use libc::{c_int, c_void};
use std::{mem, ptr};
let mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid as c_int];
let mut kp: libc::kinfo_proc = unsafe { mem::zeroed() };
let mut size = mem::size_of::<libc::kinfo_proc>();
// SAFETY: sysctl takes the following pointer arguments
// - mib is valid for KERN_PROC_PID.
// - kp is a properly sized output buffer.
// - newp is null for a read-only sysctl.
let ret = unsafe {
libc::sysctl(
mib.as_ptr(),
mib.len() as u32,
&mut kp as *mut _ as *mut c_void,
&mut size,
ptr::null_mut(),
0,
)
};
if ret == 0 { kp.ki_pri.pri_level as i32 } else { 0 }
} else {
0
}
}
}
pub(crate) trait UnixProcessExt {
fn sysinfo_process_data(
sys: &System, use_current_cpu_total: bool, unnormalized_cpu: bool, total_memory: u64,
@@ -69,6 +125,9 @@ pub(crate) trait UnixProcessExt {
};
let uid = process_val.user_id().map(|u| **u);
let pid = process_val.pid().as_u32() as Pid;
let nice = get_nice(pid);
let priority = get_priority(pid);
process_vector.push(ProcessHarvest {
pid,
parent_pid: Self::parent_pid(process_val),
@@ -90,11 +149,6 @@ pub(crate) trait UnixProcessExt {
uid,
user: uid.and_then(|uid| user_table.uid_to_username(uid).ok()),
time: if process_val.start_time() == 0 {
// Workaround for sysinfo occasionally returning a start time equal to UNIX
// epoch, giving a run time in the range of 50+ years. We just
// return a time of zero in this case for simplicity.
//
// TODO: Maybe return an option instead?
Duration::ZERO
} else {
Duration::from_secs(process_val.run_time())
@@ -105,6 +159,9 @@ pub(crate) trait UnixProcessExt {
gpu_mem_percent: 0.0,
#[cfg(feature = "gpu")]
gpu_util: 0,
#[cfg(unix)]
nice,
priority,
});
}
+11 -3
View File
@@ -1,9 +1,9 @@
//! Process data collection for Windows. Uses sysinfo.
use std::time::Duration;
//! Process data collection for Windows. Uses sysinfo and winprocinfo.
use super::{ProcessHarvest, process_status_str};
use crate::collection::{DataCollector, error::CollectionResult};
use std::time::Duration;
use winprocinfo;
use itertools::Itertools;
@@ -90,6 +90,13 @@ pub fn sysinfo_process_data(
}
(gpu_mem, gpu_util, gpu_mem_percent)
};
let base_priority = winprocinfo::get_proc_info_by_pid(process_val.pid().as_u32())
.ok()
.flatten()
.map(|proc| proc.base_priority)
.unwrap_or(0);
process_vector.push(ProcessHarvest {
pid: process_val.pid().as_u32() as _,
parent_pid: process_val.parent().map(|p| p.as_u32() as _),
@@ -127,6 +134,7 @@ pub fn sysinfo_process_data(
gpu_util,
#[cfg(feature = "gpu")]
gpu_mem_percent,
priority: base_priority,
});
}
+6 -9
View File
@@ -146,17 +146,14 @@ mod test {
/// This doesn't do anything if you use something like nextest, which runs
/// a test-per-process, but that's fine.
fn init_test_logger() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Once;
static LOG_INIT: AtomicBool = AtomicBool::new(false);
static INIT: Once = Once::new();
if LOG_INIT.load(Ordering::SeqCst) {
return;
}
LOG_INIT.store(true, Ordering::SeqCst);
super::init_logger(log::LevelFilter::Trace, None)
.expect("initializing the logger should succeed");
INIT.call_once(|| {
super::init_logger(log::LevelFilter::Trace, None)
.expect("initializing the logger should succeed");
});
}
#[cfg(feature = "logging")]
+18
View File
@@ -165,6 +165,9 @@ fn make_column(column: ProcColumn) -> SortColumn<ProcColumn> {
User => SortColumn::soft(User, Some(0.05)),
State => SortColumn::hard(State, 9),
Time => SortColumn::new(Time),
Priority => SortColumn::new(Priority).default_descending(),
#[cfg(unix)]
Nice => SortColumn::new(Nice),
#[cfg(feature = "gpu")]
GpuMemValue => SortColumn::new(GpuMemValue).default_descending(),
#[cfg(feature = "gpu")]
@@ -198,6 +201,9 @@ pub enum ProcWidgetColumn {
User,
State,
Time,
Priority,
#[cfg(unix)]
Nice,
#[cfg(feature = "gpu")]
GpuMem,
#[cfg(feature = "gpu")]
@@ -340,6 +346,9 @@ impl ProcWidgetState {
ProcWidgetColumn::User => User,
ProcWidgetColumn::State => State,
ProcWidgetColumn::Time => Time,
ProcWidgetColumn::Priority => Priority,
#[cfg(unix)]
ProcWidgetColumn::Nice => Nice,
#[cfg(feature = "gpu")]
ProcWidgetColumn::GpuMem => {
if mem_as_values {
@@ -368,6 +377,9 @@ impl ProcWidgetState {
User,
State,
Time,
Priority,
#[cfg(unix)]
Nice,
];
default_columns.into_iter().map(make_column).collect()
@@ -393,6 +405,9 @@ impl ProcWidgetState {
State => ProcWidgetColumn::State,
User => ProcWidgetColumn::User,
Time => ProcWidgetColumn::Time,
Priority => ProcWidgetColumn::Priority,
#[cfg(unix)]
Nice => ProcWidgetColumn::Nice,
#[cfg(feature = "gpu")]
GpuMemValue | GpuMemPercent => ProcWidgetColumn::GpuMem,
#[cfg(feature = "gpu")]
@@ -1205,6 +1220,9 @@ mod test {
gpu_usage: 0,
#[cfg(target_os = "linux")]
process_type: crate::collection::processes::ProcessType::Regular,
#[cfg(unix)]
nice: 0,
priority: -20,
};
let b = ProcWidgetData {
@@ -30,6 +30,9 @@ pub enum ProcColumn {
State,
User,
Time,
#[cfg(unix)]
Nice,
Priority,
#[cfg(feature = "gpu")]
GpuMemValue,
#[cfg(feature = "gpu")]
@@ -63,6 +66,9 @@ impl ProcColumn {
ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => &["GMem", "GMem%"],
#[cfg(feature = "gpu")]
ProcColumn::GpuUtilPercent => &["GPU%"],
#[cfg(unix)]
ProcColumn::Nice => &["Nice"],
ProcColumn::Priority => &["Priority"],
}
}
}
@@ -85,6 +91,9 @@ impl ColumnHeader for ProcColumn {
ProcColumn::State => "State",
ProcColumn::User => "User",
ProcColumn::Time => "Time",
#[cfg(unix)]
ProcColumn::Nice => "Nice",
ProcColumn::Priority => "Priority",
#[cfg(feature = "gpu")]
ProcColumn::GpuMemValue => "GMem",
#[cfg(feature = "gpu")]
@@ -103,6 +112,9 @@ impl ColumnHeader for ProcColumn {
ProcColumn::Pid => "PID(p)".into(),
ProcColumn::Name => "Name(n)".into(),
ProcColumn::Command => "Command(n)".into(),
#[cfg(unix)]
ProcColumn::Nice => "Nice".into(),
ProcColumn::Priority => "Priority".into(),
_ => self.text(),
}
}
@@ -167,6 +179,13 @@ impl SortsRow for ProcColumn {
ProcColumn::Time => {
data.sort_by(|a, b| sort_partial_fn(descending)(a.time, b.time));
}
ProcColumn::Priority => {
data.sort_by(|a, b| sort_partial_fn(descending)(a.priority, b.priority));
}
#[cfg(unix)]
ProcColumn::Nice => {
data.sort_by(|a, b| sort_partial_fn(descending)(a.nice, b.nice));
}
#[cfg(feature = "gpu")]
ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => {
data.sort_by(|a, b| {
@@ -230,6 +249,9 @@ impl From<&ProcColumn> for ProcWidgetColumn {
ProcColumn::State => ProcWidgetColumn::State,
ProcColumn::User => ProcWidgetColumn::User,
ProcColumn::Time => ProcWidgetColumn::Time,
ProcColumn::Priority => ProcWidgetColumn::Priority,
#[cfg(unix)]
ProcColumn::Nice => ProcWidgetColumn::Nice,
#[cfg(feature = "gpu")]
ProcColumn::GpuMemPercent | ProcColumn::GpuMemValue => ProcWidgetColumn::GpuMem,
#[cfg(feature = "gpu")]
+13 -3
View File
@@ -218,6 +218,9 @@ pub struct ProcWidgetData {
/// The process "type". Used to color things.
#[cfg(target_os = "linux")]
pub process_type: crate::collection::processes::ProcessType,
#[cfg(unix)]
pub nice: i32,
pub priority: i32,
}
impl ProcWidgetData {
@@ -264,6 +267,9 @@ impl ProcWidgetData {
gpu_usage: process.gpu_util,
#[cfg(target_os = "linux")]
process_type: process.process_type,
#[cfg(unix)]
nice: process.nice,
priority: process.priority,
}
}
@@ -308,6 +314,9 @@ impl ProcWidgetData {
fn to_string(&self, column: &ProcColumn) -> String {
match column {
&ProcColumn::Priority => self.priority.to_string(),
#[cfg(unix)]
ProcColumn::Nice => self.nice.to_string(),
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent),
ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string(),
ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem),
@@ -337,12 +346,13 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
fn to_cell_text(
&self, column: &ProcColumn, calculated_width: NonZeroU16,
) -> Option<Cow<'static, str>> {
let calculated_width = calculated_width.get();
// TODO: Optimize the string allocations here...
// TODO: Also maybe just pull in the to_string call but add a variable for the
// differences.
Some(match column {
#[cfg(unix)]
ProcColumn::Nice => self.nice.to_string().into(),
&ProcColumn::Priority => self.priority.to_string().into(),
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(),
ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string().into(),
ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem).into(),
@@ -354,7 +364,7 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
ProcColumn::TotalRead => dec_bytes_string(self.total_read).into(),
ProcColumn::TotalWrite => dec_bytes_string(self.total_write).into(),
ProcColumn::State => {
if calculated_width < 8 {
if calculated_width.get() < 8 {
self.process_char.to_string().into()
} else {
self.process_state.into()
+21
View File
@@ -670,6 +670,9 @@ enum PrefixType {
State,
User,
Time,
#[cfg(unix)]
Nice,
Priority,
#[cfg(feature = "gpu")]
PGpu,
#[cfg(feature = "gpu")]
@@ -711,6 +714,13 @@ impl std::str::FromStr for PrefixType {
result = User;
} else if multi_eq_ignore_ascii_case!(s, "time") {
result = Time;
} else if multi_eq_ignore_ascii_case!(s, "nice") {
#[cfg(unix)]
{
result = Nice;
}
} else if multi_eq_ignore_ascii_case!(s, "priority") {
result = Priority;
}
#[cfg(feature = "gpu")]
{
@@ -880,6 +890,17 @@ impl Prefix {
process.gpu_mem_percent,
numerical_query.value,
),
#[cfg(unix)]
PrefixType::Nice => matches_condition(
&numerical_query.condition,
process.nice,
numerical_query.value,
),
PrefixType::Priority => matches_condition(
&numerical_query.condition,
process.priority,
numerical_query.value,
),
_ => true,
},
ComparableQuery::Time(time_query) => match prefix_type {