use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::extensions::model::Proc;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub enum GroupBy {
#[default]
User,
Command,
Parent,
}
impl GroupBy {
pub fn label(self) -> &'static str {
match self {
GroupBy::User => "user",
GroupBy::Command => "command",
GroupBy::Parent => "parent",
}
}
pub fn next(self) -> GroupBy {
match self {
GroupBy::User => GroupBy::Command,
GroupBy::Command => GroupBy::Parent,
GroupBy::Parent => GroupBy::User,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Group {
pub key: String,
pub count: usize,
pub cpu: f32,
pub mem_kb: u64,
}
pub fn aggregate(table: &[Proc], by: GroupBy) -> Vec<Group> {
let comm_by_pid: HashMap<u32, &str> = if by == GroupBy::Parent {
table.iter().map(|p| (p.pid, p.comm.as_str())).collect()
} else {
HashMap::new()
};
let mut groups: HashMap<String, Group> = HashMap::new();
for p in table {
let key = match by {
GroupBy::User => p.user.clone(),
GroupBy::Command => p.comm.clone(),
GroupBy::Parent => match comm_by_pid.get(&p.ppid) {
Some(comm) => format!("{} {}", p.ppid, comm),
None => p.ppid.to_string(),
},
};
let g = groups.entry(key.clone()).or_insert(Group {
key,
count: 0,
cpu: 0.0,
mem_kb: 0,
});
g.count += 1;
g.cpu += p.cpu;
g.mem_kb += p.mem_kb;
}
let mut out: Vec<Group> = groups.into_values().collect();
out.sort_by(|a, b| {
b.cpu
.total_cmp(&a.cpu)
.then(b.mem_kb.cmp(&a.mem_kb))
.then(a.key.cmp(&b.key))
});
out
}
#[cfg(test)]
mod tests {
use super::*;
fn proc(pid: u32, ppid: u32, user: &str, comm: &str, cpu: f32, mem_kb: u64) -> Proc {
Proc {
pid,
ppid,
user: user.into(),
comm: comm.into(),
cmdline: comm.into(),
state: 'R',
cpu,
mem_kb,
}
}
fn table() -> Vec<Proc> {
vec![
proc(1, 0, "root", "init", 0.0, 100),
proc(2, 1, "root", "bash", 5.0, 200),
proc(3, 1, "jacob", "chrome", 30.0, 4000),
proc(4, 1, "jacob", "chrome", 20.0, 3000),
proc(5, 2, "jacob", "vim", 1.0, 500),
]
}
#[test]
fn groups_by_user_sum_and_sort() {
let g = aggregate(&table(), GroupBy::User);
assert_eq!(g[0].key, "jacob");
assert_eq!(g[0].count, 3);
assert_eq!(g[0].cpu, 51.0);
assert_eq!(g[0].mem_kb, 7500);
assert_eq!(g[1].key, "root");
assert_eq!(g[1].cpu, 5.0);
}
#[test]
fn groups_by_command_merges_same_comm() {
let g = aggregate(&table(), GroupBy::Command);
assert_eq!(g[0].key, "chrome");
assert_eq!(g[0].count, 2);
assert_eq!(g[0].cpu, 50.0);
}
#[test]
fn groups_by_parent_labels_known_parent() {
let g = aggregate(&table(), GroupBy::Parent);
assert_eq!(g[0].key, "1 init");
assert_eq!(g[0].count, 3);
assert_eq!(g[0].cpu, 55.0);
assert!(g.iter().any(|x| x.key == "0"));
}
#[test]
fn empty_table_is_empty() {
assert!(aggregate(&[], GroupBy::User).is_empty());
}
}