use super::aggregate::Aggregated;
use super::columns::Column;
use super::diff_types::{DerivedRow, DiffRow};
use super::scale::{
ScaleLadder, format_delta_cell, format_derived_delta_cell, format_derived_value_cell,
format_scaled_u64, format_value_cell,
};
use super::{CTPROF_METRICS, metric_display_name, metric_tags};
use crate::ctprof::{Psi, PsiHalf, PsiResource};
pub(super) fn format_arrow_cell(
baseline: &Aggregated,
candidate: &Aggregated,
delta: Option<f64>,
ladder: ScaleLadder,
) -> String {
let baseline_cell = format_value_cell(baseline, ladder);
let candidate_cell = format_value_cell(candidate, ladder);
let _ = delta;
format!("{baseline_cell} \u{2192} {candidate_cell}")
}
pub(super) fn format_arrow_cell_derived(row: &DerivedRow) -> String {
let baseline_cell = match row.baseline {
Some(v) => format_derived_value_cell(v, row.metric_ladder, row.is_ratio),
None => "-".to_string(),
};
let candidate_cell = match row.candidate {
Some(v) => format_derived_value_cell(v, row.metric_ladder, row.is_ratio),
None => "-".to_string(),
};
format!("{baseline_cell} \u{2192} {candidate_cell}")
}
pub(super) fn render_threads_cell(a: usize, b: usize) -> String {
if a == b {
a.to_string()
} else {
format!("{}\u{2192}{}", a, b)
}
}
pub(super) fn render_diff_row_cells(row: &DiffRow, columns: &[Column]) -> Vec<String> {
let metric_def = CTPROF_METRICS
.iter()
.find(|m| m.name == row.metric_name)
.expect("metric_name comes from CTPROF_METRICS via build_row");
let metric_cell = metric_display_name(metric_def).to_string();
let mut cells = Vec::with_capacity(columns.len());
for col in columns {
let cell = match col {
Column::Group => row.display_key.clone(),
Column::Threads => render_threads_cell(row.thread_count_a, row.thread_count_b),
Column::Metric => metric_cell.clone(),
Column::Baseline => format_value_cell(&row.baseline, row.metric_ladder),
Column::Candidate => format_value_cell(&row.candidate, row.metric_ladder),
Column::Delta => match row.delta {
Some(d) => format_delta_cell(d, row.metric_ladder),
None => match (&row.baseline, &row.candidate) {
(Aggregated::Mode { .. }, Aggregated::Mode { .. }) => {
if row.baseline.mode_value() == row.candidate.mode_value() {
"same".to_string()
} else {
"differs".to_string()
}
}
_ => "-".to_string(),
},
},
Column::Pct => match row.delta_pct {
Some(p) => format!("{:+.1}%", p * 100.0),
None => "-".to_string(),
},
Column::Arrow => {
format_arrow_cell(&row.baseline, &row.candidate, row.delta, row.metric_ladder)
}
Column::Value => "-".to_string(),
Column::Tags => metric_tags(metric_def),
Column::Uptime => match row.uptime_pct {
Some(pct) => format!("{pct:.0}%"),
None => "-".to_string(),
},
Column::SortBy => row.sort_by_cell.clone().unwrap_or_else(|| "-".to_string()),
};
cells.push(cell);
}
cells
}
pub fn color_diff_cell(
text: String,
col: Column,
delta: Option<f64>,
delta_pct: Option<f64>,
uptime_pct: Option<f64>,
sort_by_delta: Option<f64>,
) -> comfy_table::Cell {
use comfy_table::{Attribute, Color};
match col {
Column::Pct => {
let color = match delta {
Some(d) if d > 0.0 => Color::Yellow,
Some(d) if d < 0.0 => Color::Magenta,
_ => Color::White,
};
let mut cell = comfy_table::Cell::new(text).fg(color);
if matches!(delta_pct, Some(p) if p.abs() > 0.5) {
cell = cell.add_attribute(Attribute::Bold);
}
cell
}
Column::Delta => {
let color = match delta {
Some(d) if d > 0.0 => Color::Yellow,
Some(d) if d < 0.0 => Color::Magenta,
_ => Color::White,
};
comfy_table::Cell::new(text).fg(color)
}
Column::Uptime => {
let color = match uptime_pct {
Some(p) if p >= 75.0 => Color::Green,
Some(p) if p >= 50.0 => Color::Yellow,
Some(_) => Color::Red,
None => Color::White,
};
let mut cell = comfy_table::Cell::new(text).fg(color);
if matches!(uptime_pct, Some(p) if p < 50.0) {
cell = cell.add_attribute(Attribute::Bold);
}
cell
}
Column::SortBy => {
let color = match sort_by_delta {
Some(d) if d > 0.0 => Color::Yellow,
Some(d) if d < 0.0 => Color::Magenta,
_ => Color::Cyan,
};
comfy_table::Cell::new(text).fg(color)
}
_ => comfy_table::Cell::new(text),
}
}
pub(super) fn cgroup_parent_leaf(path: &str) -> (&str, &str) {
match path.rfind('/') {
Some(0) => ("/", &path[1..]),
Some(i) => (&path[..i], &path[i + 1..]),
None => ("", path),
}
}
pub fn colored_header(columns: &[Column], group_header: &'static str) -> Vec<comfy_table::Cell> {
colored_header_with_sort(columns, group_header, None)
}
pub fn colored_header_with_sort(
columns: &[Column],
group_header: &'static str,
sort_metric: Option<&str>,
) -> Vec<comfy_table::Cell> {
columns
.iter()
.map(|c| {
let label = if *c == Column::SortBy {
sort_metric.unwrap_or("sort-by")
} else {
c.header(group_header)
};
comfy_table::Cell::new(label).fg(comfy_table::Color::Cyan)
})
.collect()
}
pub fn color_derived_cells(cells: Vec<String>) -> Vec<comfy_table::Cell> {
cells
.into_iter()
.map(|c| comfy_table::Cell::new(c).fg(comfy_table::Color::Blue))
.collect()
}
pub(super) fn render_derived_row_cells(row: &DerivedRow, columns: &[Column]) -> Vec<String> {
let mut cells = Vec::with_capacity(columns.len());
for col in columns {
let cell = match col {
Column::Group => row.display_key.clone(),
Column::Threads => render_threads_cell(row.thread_count_a, row.thread_count_b),
Column::Metric => row.metric_name.to_string(),
Column::Baseline => match row.baseline {
Some(v) => format_derived_value_cell(v, row.metric_ladder, row.is_ratio),
None => "-".to_string(),
},
Column::Candidate => match row.candidate {
Some(v) => format_derived_value_cell(v, row.metric_ladder, row.is_ratio),
None => "-".to_string(),
},
Column::Delta => match row.delta {
Some(d) => format_derived_delta_cell(d, row.metric_ladder, row.is_ratio),
None => "-".to_string(),
},
Column::Pct => match row.delta_pct {
Some(p) => format!("{:+.1}%", p * 100.0),
None => "-".to_string(),
},
Column::Arrow => format_arrow_cell_derived(row),
Column::Value => "-".to_string(),
Column::Tags => String::new(),
Column::Uptime => "-".to_string(),
Column::SortBy => row.sort_by_cell.clone().unwrap_or_else(|| "-".to_string()),
};
cells.push(cell);
}
cells
}
pub fn cgroup_cell(baseline: Option<u64>, candidate: Option<u64>, ladder: ScaleLadder) -> String {
match (baseline, candidate) {
(Some(baseline), Some(candidate)) => {
let baseline_cell = format_scaled_u64(baseline, ladder);
let candidate_cell = format_scaled_u64(candidate, ladder);
let d = candidate as i128 - baseline as i128;
let delta_cell = format_delta_cell(d as f64, ladder);
format!("{baseline_cell} → {candidate_cell} ({delta_cell})")
}
(Some(baseline), None) => format!("{} → -", format_scaled_u64(baseline, ladder)),
(None, Some(candidate)) => format!("- → {}", format_scaled_u64(candidate, ladder)),
(None, None) => "-".to_string(),
}
}
pub fn format_psi_avg_cell(baseline: Option<u16>, candidate: Option<u16>) -> String {
match (baseline, candidate) {
(Some(b), Some(c)) => {
let baseline_cell = format_psi_avg_centi_percent(b);
let candidate_cell = format_psi_avg_centi_percent(c);
let d = c as i32 - b as i32;
let sign = if d >= 0 { "+" } else { "-" };
let abs = d.unsigned_abs();
let delta_int = abs / 100;
let delta_frac = abs % 100;
format!("{baseline_cell} → {candidate_cell} ({sign}{delta_int}.{delta_frac:02}%)")
}
(Some(b), None) => format!("{} → -", format_psi_avg_centi_percent(b)),
(None, Some(c)) => format!("- → {}", format_psi_avg_centi_percent(c)),
(None, None) => "-".to_string(),
}
}
pub fn format_psi_avg_centi_percent(v: u16) -> String {
let int = v / 100;
let frac = v % 100;
format!("{int}.{frac:02}%")
}
type PsiAccessor = (&'static str, fn(&Psi) -> PsiResource);
pub(super) fn psi_resource_accessors() -> [PsiAccessor; 4] {
[
("cpu", |p| p.cpu),
("memory", |p| p.memory),
("io", |p| p.io),
("irq", |p| p.irq),
]
}
pub(super) fn psi_pair_has_data(a: &Psi, b: &Psi) -> bool {
psi_has_data(a) || psi_has_data(b)
}
pub(super) fn psi_has_data(p: &Psi) -> bool {
[p.cpu, p.memory, p.io, p.irq]
.iter()
.any(psi_resource_has_data)
}
pub(super) fn psi_resource_has_data(r: &PsiResource) -> bool {
let h = |h: &PsiHalf| h.avg10 != 0 || h.avg60 != 0 || h.avg300 != 0 || h.total_usec != 0;
h(&r.some) || h(&r.full)
}