use crate::Pid;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Field {
Pid,
Comm,
State,
Ppid,
Pgrp,
Session,
Tty,
Tpgid,
MinFlt,
MajFlt,
Priority,
Nice,
StartTime,
Processor,
VirtMem,
ResMem,
User,
PercentCpu,
PercentMem,
Time,
Nlwp,
Tgid,
PercentNormCpu,
Elapsed,
SchedPolicy,
ProcComm,
ProcExe,
Cwd,
#[cfg(target_os = "linux")]
Pss,
#[cfg(target_os = "linux")]
Swap,
#[cfg(target_os = "linux")]
Private,
#[cfg(target_os = "linux")]
Epss,
#[cfg(target_os = "linux")]
Cgroup,
#[cfg(target_os = "linux")]
Container,
#[cfg(target_os = "linux")]
GpuPercent,
#[cfg(target_os = "linux")]
GpuTime,
#[cfg(target_os = "linux")]
CpuDelayPercent,
#[cfg(target_os = "linux")]
IoDelayPercent,
#[cfg(target_os = "linux")]
SwapDelayPercent,
}
impl Field {
pub const fn needs_smaps_rollup(self) -> bool {
#[cfg(target_os = "linux")]
{
matches!(self, Field::Pss | Field::Epss | Field::Private | Field::Swap)
}
#[cfg(not(target_os = "linux"))]
{
let _ = self;
false
}
}
pub const fn needs_delayacct(self) -> bool {
#[cfg(target_os = "linux")]
{
matches!(
self,
Field::CpuDelayPercent | Field::IoDelayPercent | Field::SwapDelayPercent
)
}
#[cfg(not(target_os = "linux"))]
{
let _ = self;
false
}
}
pub const fn needs_gpu_fdinfo(self) -> bool {
#[cfg(target_os = "linux")]
{
matches!(self, Field::GpuPercent | Field::GpuTime)
}
#[cfg(not(target_os = "linux"))]
{
let _ = self;
false
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Align {
Left,
Right,
}
pub struct FieldMeta {
pub field: Field,
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
pub default_sort_desc: bool,
pub align: Align,
}
impl Field {
pub fn meta(self) -> Option<&'static FieldMeta> {
FIELD_META.iter().find(|m| m.field == self)
}
pub fn name(self) -> &'static str {
self.meta().map_or("UNKNOWN", |m| m.name)
}
pub fn title(self) -> &'static str {
self.meta().map_or(" ? ", |m| m.title)
}
pub fn default_sort_desc(self) -> bool {
self.meta().is_some_and(|m| m.default_sort_desc)
}
pub fn all() -> impl Iterator<Item = Field> {
FIELD_META.iter().map(|m| m.field)
}
pub fn from_name(name: &str) -> Option<Field> {
FIELD_META.iter().find(|m| m.name == name).map(|m| m.field)
}
}
const fn m(
field: Field,
name: &'static str,
title: &'static str,
description: &'static str,
default_sort_desc: bool,
align: Align,
) -> FieldMeta {
FieldMeta {
field,
name,
title,
description,
default_sort_desc,
align,
}
}
const FIELD_META: &[FieldMeta] = &[
m(
Field::Pid,
"PID",
" PID ",
"Process ID",
false,
Align::Right,
),
m(
Field::Comm,
"COMM",
" COMMAND ",
"Command name from /proc/<pid>/comm",
false,
Align::Left,
),
m(
Field::State,
"STATE",
" S ",
"Process state",
false,
Align::Left,
),
m(
Field::Ppid,
"PPID",
" PPID ",
"Parent process ID",
false,
Align::Right,
),
m(
Field::Pgrp,
"PGRP",
" PGRP ",
"Process group ID",
false,
Align::Right,
),
m(
Field::User,
"USER",
" USER ",
"Effective user",
false,
Align::Left,
),
m(
Field::Priority,
"PRIORITY",
" PRI ",
"Kernel scheduling priority",
false,
Align::Right,
),
m(
Field::Nice,
"NICE",
" NI ",
"Nice value",
false,
Align::Right,
),
m(
Field::PercentCpu,
"PERCENT_CPU",
" CPU% ",
"CPU usage in percent",
true,
Align::Right,
),
m(
Field::PercentMem,
"PERCENT_MEM",
" MEM% ",
"Memory usage in percent",
true,
Align::Right,
),
m(
Field::PercentNormCpu,
"PERCENT_NORM_CPU",
" NCPU% ",
"CPU usage normalized by core count",
true,
Align::Right,
),
m(
Field::Time,
"TIME",
" TIME+ ",
"Cumulative CPU time",
true,
Align::Right,
),
m(
Field::Elapsed,
"ELAPSED",
" ELAPSED ",
"Time since the process started",
false,
Align::Right,
),
m(
Field::ResMem,
"M_RESIDENT",
" RES ",
"Resident set size",
true,
Align::Right,
),
m(
Field::VirtMem,
"M_VIRT",
" VIRT ",
"Virtual memory size",
true,
Align::Right,
),
m(
Field::Nlwp,
"NLWP",
" NLWP ",
"Number of threads",
false,
Align::Right,
),
m(
Field::Processor,
"PROCESSOR",
" CPU ",
"Last used CPU",
false,
Align::Right,
),
m(
Field::MinFlt,
"MINFLT",
" MINFLT ",
"Minor page faults",
false,
Align::Right,
),
m(
Field::MajFlt,
"MAJFLT",
" MAJFLT ",
"Major page faults",
false,
Align::Right,
),
m(
Field::Session,
"SESSION",
" SID ",
"Session ID: the login session this process belongs to",
false,
Align::Right,
),
m(
Field::Tty,
"TTY",
" TTY ",
"Controlling terminal, or `?` when there is none",
false,
Align::Left,
),
m(
Field::Tpgid,
"TPGID",
" TPGID ",
"Process group of the foreground job on this terminal",
false,
Align::Right,
),
m(
Field::StartTime,
"STARTTIME",
" START ",
"Wall-clock time this process started",
false,
Align::Left,
),
m(
Field::Tgid,
"TGID",
" TGID ",
"Thread group ID: the pid of the process a thread row belongs to",
false,
Align::Right,
),
m(
Field::SchedPolicy,
"SCHED_POLICY",
" POL ",
"Scheduling policy: other, fifo, rr, batch, idle, deadline",
false,
Align::Left,
),
m(
Field::ProcComm,
"PROC_COMM",
" COMMAND ",
"Kernel's short command name, always the executable's base name",
false,
Align::Left,
),
m(
Field::ProcExe,
"PROC_EXE",
" EXE ",
"Path to the executable, as the kernel resolved it",
false,
Align::Left,
),
m(
Field::Cwd,
"CWD",
" CWD ",
"Working directory of the process",
false,
Align::Left,
),
#[cfg(target_os = "linux")]
m(
Field::Cgroup,
"CGROUP",
" CGROUP ",
"Full cgroup path",
false,
Align::Left,
),
#[cfg(target_os = "linux")]
m(
Field::Container,
"CONTAINER",
" CONTAINER ",
"Container id, prefixed with its runtime",
false,
Align::Left,
),
#[cfg(target_os = "linux")]
m(
Field::Pss,
"M_PSS",
" PSS ",
"Proportional set size: shared pages divided between sharers",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::Epss,
"M_EPSS",
" EPSS ",
"Effective PSS: PSS plus the swapped-out equivalent",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::Private,
"M_PRIVATE",
" PRIV ",
"Private memory: what would be freed if this process exited",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::Swap,
"M_SWAP",
" SWP ",
"Memory this process has pushed to swap",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::GpuPercent,
"GPU%",
" GPU% ",
"Share of GPU time this process used, from drm-usage-stats",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::GpuTime,
"GPU_TIME",
" GPUT ",
"Cumulative GPU time, across every engine class",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::CpuDelayPercent,
"CPUD%",
"CPUD% ",
"Share of this task's life spent runnable but waiting for a CPU",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::IoDelayPercent,
"IOD%",
" IOD% ",
"Share of this task's life spent waiting for synchronous block I/O",
true,
Align::Right,
),
#[cfg(target_os = "linux")]
m(
Field::SwapDelayPercent,
"SWPD%",
"SWPD% ",
"Share of this task's life spent waiting for a page to be swapped in",
true,
Align::Right,
),
];
pub const DEFAULT_COLUMNS: &[Field] = &[
Field::Pid,
Field::User,
Field::Priority,
Field::Nice,
Field::PercentCpu,
Field::PercentMem,
Field::Time,
Field::Comm,
];
pub type CommBuf = arrayvec::ArrayString<64>;
#[derive(Copy, Clone, Default, Debug)]
pub struct CpuTicks {
pub proc: u64,
pub total: u64,
}
bitflags_like::flags! {
pub struct RowFlags: u16 {
const KERNEL_THREAD = 1 << 0;
const USERLAND_THREAD = 1 << 1;
const STALE_EXE = 1 << 2;
const STALE_LIB = 1 << 3;
const NEW = 1 << 4;
}
}
mod bitflags_like {
macro_rules! flags {
(
$(#[$meta:meta])*
pub struct $name:ident: $ty:ty {
$(
const $flag:ident = $value:expr;
)*
}
) => {
$(#[$meta])*
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct $name(pub $ty);
impl $name {
$(pub const $flag: $name = $name($value);)*
pub const fn empty() -> Self { $name(0) }
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub const fn intersects(self, other: Self) -> bool {
(self.0 & other.0) != 0
}
pub const fn insert(self, other: Self) -> Self {
$name(self.0 | other.0)
}
pub const fn remove(self, other: Self) -> Self {
$name(self.0 & !other.0)
}
pub const fn set(self, other: Self, on: bool) -> Self {
if on { self.insert(other) } else { self.remove(other) }
}
}
};
}
pub(crate) use flags;
}
#[derive(Clone, Debug)]
pub struct Row {
pub pid: Pid,
pub ppid: Pid,
pub pgrp: Pid,
pub session: Pid,
pub tty_nr: i64,
pub tpgid: i64,
pub starttime: u64,
pub tgid: Pid,
pub is_thread: bool,
pub thread_id: Option<u64>,
pub comm: CommBuf,
pub state: u8,
pub uid: u32,
pub user: Option<u32>,
pub priority: i8,
pub nice: i8,
pub processor: i32,
pub nlwp: i32,
pub sched_policy: u8,
pub ticks: CpuTicks,
pub prev_ticks: CpuTicks,
pub min_flt: u64,
pub maj_flt: u64,
pub virt: u64,
pub res: u64,
pub percent_cpu: f32,
pub percent_mem: f32,
pub time_ticks: u64,
pub elapsed: u64,
pub cmdline: Option<Box<str>>,
pub exe: Option<Box<str>>,
pub cwd: Option<Box<str>>,
#[cfg(target_os = "linux")]
pub pss: Option<u64>,
#[cfg(target_os = "linux")]
pub epss: Option<u64>,
#[cfg(target_os = "linux")]
pub private: Option<u64>,
#[cfg(target_os = "linux")]
pub swap_bytes: Option<u64>,
#[cfg(target_os = "linux")]
pub gpu_percent: Option<f32>,
#[cfg(target_os = "linux")]
pub gpu_ns: Option<u64>,
pub cpu_delay_percent: Option<f32>,
pub io_delay_percent: Option<f32>,
pub swap_delay_percent: Option<f32>,
pub cgroup: Option<Box<str>>,
#[cfg(target_os = "linux")]
pub container: Option<Box<str>>,
#[cfg(target_os = "linux")]
pub container_runtime: Option<Box<str>>,
pub depth: u16,
pub has_children: bool,
pub is_last_child: bool,
pub flags: RowFlags,
}
impl Row {
pub fn default_for_test(pid: Pid) -> Self {
Row {
pid,
ppid: 0,
pgrp: 0,
session: 0,
tty_nr: 0,
tpgid: -1,
starttime: 0,
tgid: pid,
is_thread: false,
thread_id: None,
comm: CommBuf::new(),
state: b'R',
uid: 0,
user: None,
priority: 20,
nice: 0,
processor: 0,
nlwp: 1,
sched_policy: 0,
ticks: CpuTicks::default(),
prev_ticks: CpuTicks::default(),
min_flt: 0,
maj_flt: 0,
virt: 0,
res: 0,
percent_cpu: 0.0,
percent_mem: 0.0,
time_ticks: 0,
elapsed: 0,
cmdline: None,
exe: None,
cwd: None,
#[cfg(target_os = "linux")]
pss: None,
#[cfg(target_os = "linux")]
epss: None,
#[cfg(target_os = "linux")]
private: None,
#[cfg(target_os = "linux")]
swap_bytes: None,
#[cfg(target_os = "linux")]
gpu_percent: None,
#[cfg(target_os = "linux")]
gpu_ns: None,
cpu_delay_percent: None,
io_delay_percent: None,
swap_delay_percent: None,
cgroup: None,
#[cfg(target_os = "linux")]
container: None,
#[cfg(target_os = "linux")]
container_runtime: None,
depth: 0,
has_children: false,
is_last_child: false,
flags: RowFlags::empty(),
}
}
pub const fn maj_flt_count(&self) -> u64 {
self.maj_flt
}
pub const fn time_ticks_total(&self) -> u64 {
self.time_ticks
}
pub fn is_kernel_thread(&self) -> bool {
self.flags.contains(RowFlags::KERNEL_THREAD)
}
pub fn identity(&self) -> u64 {
match self.thread_id {
Some(id) => id,
None => self.pid as u64,
}
}
pub fn display_command(&self) -> &str {
match &self.cmdline {
Some(c) if !c.is_empty() => c,
_ => self.comm.as_str(),
}
}
pub fn base_name_span(&self) -> Option<(usize, usize)> {
self.base_name_span_in(self.display_command())
}
pub fn base_name_span_in(&self, text: &str) -> Option<(usize, usize)> {
if text.is_empty() {
return None;
}
if let Some(exe) = self.exe.as_deref() {
if !exe.is_empty() {
if let Some(pos) = text.find(exe) {
let start = pos + exe.rfind('/').map_or(0, |i| i + 1);
let end = pos + exe.len();
if end > start {
return Some((start, end));
}
}
}
}
let end = text.find(' ').unwrap_or(text.len());
let word = &text[..end];
if word.starts_with('[') {
return None;
}
let start = word.rfind('/').map_or(0, |i| i + 1);
if start == 0 && end == text.len() {
return None;
}
if end > start {
Some((start, end))
} else {
None
}
}
}
#[cfg(test)]
mod base_name_tests {
use super::*;
fn row_with(cmdline: &str, exe: Option<&str>) -> Row {
let mut r = Row::default_for_test(1);
r.cmdline = Some(cmdline.into());
r.exe = exe.map(Into::into);
r
}
#[test]
fn finds_the_basename_inside_a_full_path() {
let r = row_with(
"/Applications/iTerm.app/Contents/MacOS/iTerm2 --server",
Some("/Applications/iTerm.app/Contents/MacOS/iTerm2"),
);
let text = r.display_command();
let (s, e) = r.base_name_span().expect("span");
assert_eq!(&text[s..e], "iTerm2");
}
#[test]
fn an_argument_path_does_not_win_over_the_exe() {
let r = row_with("grep /usr/lib/foo", Some("/usr/bin/grep"));
let text = r.display_command();
let (s, e) = r.base_name_span().expect("span");
assert_eq!(&text[s..e], "grep");
}
#[test]
fn falls_back_to_argv0_without_an_exe() {
let r = row_with("/usr/bin/python3 script.py", None);
let text = r.display_command();
let (s, e) = r.base_name_span().expect("span");
assert_eq!(&text[s..e], "python3");
}
#[test]
fn a_bare_command_name_gets_no_span() {
let r = row_with("launchd", None);
assert_eq!(r.base_name_span(), None);
}
#[test]
fn a_kernel_thread_gets_no_span() {
let r = row_with("[kworker/0:1]", None);
assert_eq!(r.base_name_span(), None);
}
#[test]
fn an_empty_command_gets_no_span() {
let mut r = Row::default_for_test(1);
r.cmdline = None;
r.comm.try_push_str("").unwrap();
assert_eq!(r.base_name_span(), None);
}
#[test]
fn the_span_tracks_a_prepended_tree_prefix() {
let r = row_with("/usr/bin/nginx -g daemon", Some("/usr/bin/nginx"));
let plain = r.display_command();
let (s, e) = r.base_name_span().expect("span");
assert_eq!(&plain[s..e], "nginx");
let prefixed = format!("├─ {plain}");
let (ps, pe) = r.base_name_span_in(&prefixed).expect("span");
assert_eq!(&prefixed[ps..pe], "nginx");
assert_eq!(ps, s + "├─ ".len(), "offset by the prefix");
}
#[test]
fn an_exe_with_spaces_is_located_by_path_not_by_word() {
let r = row_with(
"/Users/x/My App.app/Contents/MacOS/My App --flag",
Some("/Users/x/My App.app/Contents/MacOS/My App"),
);
let text = r.display_command();
let (s, e) = r.base_name_span().expect("span");
assert_eq!(&text[s..e], "My App");
}
}
#[cfg(test)]
mod field_meta_tests {
use super::*;
#[test]
fn every_variant_has_metadata() {
let all = [
Field::Pid,
Field::Comm,
Field::State,
Field::Ppid,
Field::Pgrp,
Field::Session,
Field::Tty,
Field::Tpgid,
Field::MinFlt,
Field::MajFlt,
Field::Priority,
Field::Nice,
Field::StartTime,
Field::Processor,
Field::VirtMem,
Field::ResMem,
Field::User,
Field::PercentCpu,
Field::PercentMem,
Field::Time,
Field::Nlwp,
Field::Tgid,
Field::PercentNormCpu,
Field::Elapsed,
Field::SchedPolicy,
Field::ProcComm,
Field::ProcExe,
Field::Cwd,
];
#[allow(unused_mut)]
let mut all = all.to_vec();
#[cfg(target_os = "linux")]
all.extend([
Field::Pss,
Field::Swap,
Field::Private,
Field::Epss,
Field::Cgroup,
Field::Container,
Field::GpuPercent,
Field::GpuTime,
Field::CpuDelayPercent,
Field::IoDelayPercent,
Field::SwapDelayPercent,
]);
for field in all {
let meta = field
.meta()
.unwrap_or_else(|| panic!("{field:?} has no FieldMeta"));
assert_ne!(meta.name, "UNKNOWN", "{field:?} has no name");
assert_ne!(meta.title.trim(), "?", "{field:?} has no header title");
assert!(
!meta.description.is_empty(),
"{field:?} has no description for the setup screen"
);
assert_eq!(
Field::from_name(meta.name),
Some(field),
"{field:?} does not round-trip through {}",
meta.name
);
}
}
#[test]
fn every_metadata_entry_is_offered_by_all() {
let offered: Vec<Field> = Field::all().collect();
for field in &offered {
assert!(field.meta().is_some());
}
for wanted in [Field::Session, Field::Tty, Field::Cwd, Field::ProcComm] {
assert!(
offered.contains(&wanted),
"{wanted:?} must be selectable in the setup screen"
);
}
}
#[test]
fn names_are_unique() {
let mut seen = std::collections::HashSet::new();
for field in Field::all() {
assert!(seen.insert(field.name()), "duplicate name {}", field.name());
}
}
}