use std::collections::BTreeMap;
use std::fmt;
#[derive(Debug, Clone)]
pub enum Aggregated {
Sum(u64),
Max(u64),
OrdinalRange {
min: i64,
max: i64,
},
Mode {
tallies: BTreeMap<String, usize>,
total: usize,
},
Affinity(AffinitySummary),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AffinitySummary {
pub min_cpus: usize,
pub max_cpus: usize,
pub uniform: Option<Vec<u32>>,
}
impl Aggregated {
pub fn numeric(&self) -> Option<f64> {
match self {
Aggregated::Sum(v) => Some(*v as f64),
Aggregated::Max(v) => Some(*v as f64),
Aggregated::OrdinalRange { min, max } => {
Some((*min as f64 + *max as f64) / 2.0)
}
Aggregated::Mode { .. } => None,
Aggregated::Affinity(s) => {
Some((s.min_cpus as f64 + s.max_cpus as f64) / 2.0)
}
}
}
pub fn mode_single(value: String, count: usize, total: usize) -> Aggregated {
let mut tallies = BTreeMap::new();
if count > 0 {
tallies.insert(value, count);
}
Aggregated::Mode { tallies, total }
}
pub fn mode_value(&self) -> &str {
match self {
Aggregated::Mode { tallies, .. } => {
let mut best: Option<(&str, usize)> = None;
for (k, c) in tallies {
match best {
None => best = Some((k.as_str(), *c)),
Some((_, bc)) if *c > bc => best = Some((k.as_str(), *c)),
_ => {}
}
}
best.map(|(v, _)| v).unwrap_or("")
}
_ => "",
}
}
pub fn mode_count(&self) -> usize {
match self {
Aggregated::Mode { tallies, .. } => tallies.values().copied().max().unwrap_or(0),
_ => 0,
}
}
}
impl fmt::Display for Aggregated {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Aggregated::Sum(v) => write!(f, "{v}"),
Aggregated::Max(v) => write!(f, "{v}"),
Aggregated::OrdinalRange { min, max } => {
if min == max {
write!(f, "{min}")
} else {
write!(f, "{min}..{max}")
}
}
Aggregated::Mode { tallies, total } => {
let mut best: Option<(&str, usize)> = None;
for (k, c) in tallies {
match best {
None => best = Some((k.as_str(), *c)),
Some((_, bc)) if *c > bc => best = Some((k.as_str(), *c)),
_ => {}
}
}
let value = best.map(|(v, _)| v).unwrap_or("~");
let count = best.map(|(_, c)| c).unwrap_or(0);
if count == *total && count > 0 {
write!(f, "{value}")
} else {
write!(f, "{value} ({count}/{total})")
}
}
Aggregated::Affinity(s) => {
if let Some(cpus) = &s.uniform {
let n = cpus.len();
let range = format_cpu_range(cpus);
write!(f, "{n} cpus ({range})")
} else if s.min_cpus == s.max_cpus {
write!(f, "{} cpus (mixed)", s.min_cpus)
} else {
write!(f, "{}-{} cpus (mixed)", s.min_cpus, s.max_cpus)
}
}
}
}
}
pub(super) fn merge_aggregated_into(existing: &mut Aggregated, val: &Aggregated) {
match (existing, val) {
(Aggregated::Sum(s), Aggregated::Sum(v)) => {
*s += v;
}
(Aggregated::Max(m), Aggregated::Max(v)) => {
*m = (*m).max(*v);
}
(
Aggregated::OrdinalRange { min, max },
Aggregated::OrdinalRange {
min: vmin,
max: vmax,
},
) => {
*min = (*min).min(*vmin);
*max = (*max).max(*vmax);
}
(
Aggregated::Mode { tallies: et, total },
Aggregated::Mode {
tallies: vt,
total: vtot,
},
) => {
*total += vtot;
for (k, c) in vt {
*et.entry(k.clone()).or_insert(0) += c;
}
}
(Aggregated::Affinity(es), Aggregated::Affinity(vs)) => {
es.min_cpus = es.min_cpus.min(vs.min_cpus);
es.max_cpus = es.max_cpus.max(vs.max_cpus);
es.uniform = match (&es.uniform, &vs.uniform) {
(Some(a), Some(b)) if a == b => Some(a.clone()),
_ => None,
};
}
_ => {}
}
}
pub(super) fn format_cpu_range(cpus: &[u32]) -> String {
if cpus.is_empty() {
return String::new();
}
let mut out = String::new();
let mut start = cpus[0];
let mut prev = cpus[0];
for &c in &cpus[1..] {
if c == prev + 1 {
prev = c;
continue;
}
if !out.is_empty() {
out.push(',');
}
if start == prev {
out.push_str(&start.to_string());
} else {
out.push_str(&format!("{start}-{prev}"));
}
start = c;
prev = c;
}
if !out.is_empty() {
out.push(',');
}
if start == prev {
out.push_str(&start.to_string());
} else {
out.push_str(&format!("{start}-{prev}"));
}
out
}