use super::{CTPROF_DERIVED_METRICS, CTPROF_METRICS, GroupBy};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
#[non_exhaustive]
pub enum DisplayFormat {
#[default]
Full,
DeltaOnly,
NoPct,
Arrow,
PctOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Column {
Group,
Threads,
Metric,
Baseline,
Candidate,
Delta,
Pct,
Arrow,
Value,
Tags,
Uptime,
SortBy,
}
impl Column {
pub fn cli_name(self) -> &'static str {
match self {
Column::Group => "group",
Column::Threads => "threads",
Column::Metric => "metric",
Column::Baseline => "baseline",
Column::Candidate => "candidate",
Column::Delta => "delta",
Column::Pct => "%",
Column::Arrow => "arrow",
Column::Value => "value",
Column::Tags => "tags",
Column::Uptime => "uptime",
Column::SortBy => "sort-by", }
}
pub fn header(self, group_header: &'static str) -> &'static str {
match self {
Column::Group => group_header,
Column::Threads => "threads",
Column::Metric => "metric",
Column::Baseline => "baseline",
Column::Candidate => "candidate",
Column::Delta => "delta",
Column::Pct => "%",
Column::Arrow => "value",
Column::Value => "value",
Column::Tags => "tags",
Column::Uptime => "%uptime",
Column::SortBy => "sort-by", }
}
}
pub(super) fn compare_columns_for(format: DisplayFormat) -> Vec<Column> {
let mut cols = vec![Column::Group, Column::Threads, Column::Metric];
let trailing: &[Column] = match format {
DisplayFormat::Full => &[
Column::Baseline,
Column::Candidate,
Column::Delta,
Column::Pct,
],
DisplayFormat::DeltaOnly => &[Column::Delta, Column::Pct],
DisplayFormat::NoPct => &[Column::Baseline, Column::Candidate, Column::Delta],
DisplayFormat::Arrow => &[Column::Arrow, Column::Delta, Column::Pct, Column::Uptime],
DisplayFormat::PctOnly => &[Column::Pct],
};
cols.extend_from_slice(trailing);
cols
}
pub(super) fn show_columns_default() -> Vec<Column> {
vec![
Column::Group,
Column::Threads,
Column::Metric,
Column::Value,
]
}
pub fn parse_columns(spec: &str, compare_side: bool) -> anyhow::Result<Vec<Column>> {
if spec.trim().is_empty() {
return Ok(Vec::new());
}
let allowed: &[Column] = if compare_side {
&[
Column::Group,
Column::Threads,
Column::Metric,
Column::Baseline,
Column::Candidate,
Column::Delta,
Column::Pct,
Column::Arrow,
Column::Tags,
Column::Uptime,
]
} else {
&[
Column::Group,
Column::Threads,
Column::Metric,
Column::Value,
Column::Tags,
Column::Uptime,
]
};
let valid_names = allowed
.iter()
.map(|c| c.cli_name())
.collect::<Vec<_>>()
.join(", ");
let mut out: Vec<Column> = Vec::new();
let mut seen: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
for entry in spec.split(',') {
let entry = entry.trim();
if entry.is_empty() {
anyhow::bail!(
"empty entry in --columns spec {spec:?}; \
entries are comma-separated and must be non-empty"
);
}
let normalized = entry.to_ascii_lowercase();
let Some(col) = allowed.iter().copied().find(|c| c.cli_name() == normalized) else {
anyhow::bail!(
"unknown column {entry:?} in --columns spec {spec:?}; \
must be one of: {valid_names}",
);
};
if !seen.insert(col.cli_name()) {
anyhow::bail!(
"duplicate column {entry:?} in --columns spec {spec:?}; \
each column may appear at most once"
);
}
out.push(col);
}
let has_arrow = out.iter().any(|c| matches!(c, Column::Arrow));
let has_redundant_with_arrow = out
.iter()
.any(|c| matches!(c, Column::Baseline | Column::Candidate));
if has_arrow && has_redundant_with_arrow {
anyhow::bail!(
"column 'arrow' is mutually exclusive with baseline/candidate \
— the arrow cell already shows baseline -> candidate. \
Pair arrow with delta/% (or use it alone) instead."
);
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Section {
Primary,
Derived,
CgroupStats,
Limits,
MemoryStat,
MemoryEvents,
Pressure,
HostPressure,
Smaps,
SchedExt,
TaskstatsDelay,
}
impl Section {
pub const ALL: &'static [Section] = &[
Section::Primary,
Section::TaskstatsDelay,
Section::Derived,
Section::CgroupStats,
Section::Limits,
Section::MemoryStat,
Section::MemoryEvents,
Section::Pressure,
Section::HostPressure,
Section::Smaps,
Section::SchedExt,
];
pub fn cli_name(self) -> &'static str {
match self {
Section::Primary => "primary",
Section::TaskstatsDelay => "taskstats-delay",
Section::Derived => "derived",
Section::CgroupStats => "cgroup-stats",
Section::Limits => "cgroup-limits",
Section::MemoryStat => "memory-stat",
Section::MemoryEvents => "memory-events",
Section::Pressure => "pressure",
Section::HostPressure => "host-pressure",
Section::Smaps => "smaps-rollup",
Section::SchedExt => "sched-ext",
}
}
pub fn requires_cgroup_grouping(self) -> bool {
matches!(
self,
Section::CgroupStats
| Section::Limits
| Section::MemoryStat
| Section::MemoryEvents
| Section::Pressure
)
}
}
pub fn warn_cgroup_only_sections_under_non_cgroup(sections: &[Section], group_by: GroupBy) {
if sections.is_empty() || group_by == GroupBy::Cgroup {
return;
}
for section in sections {
if section.requires_cgroup_grouping() {
eprintln!("{}", format_cgroup_only_section_warning(*section, group_by));
}
}
}
pub(crate) fn format_cgroup_only_section_warning(section: Section, group_by: GroupBy) -> String {
format!(
"section '{}' requires --group-by cgroup; omitted under --group-by {}",
section.cli_name(),
group_by_cli_name(group_by),
)
}
fn group_by_cli_name(group_by: GroupBy) -> &'static str {
match group_by {
GroupBy::Pcomm => "pcomm",
GroupBy::Cgroup => "cgroup",
GroupBy::Comm => "comm",
GroupBy::CommExact => "comm-exact",
GroupBy::All => "all",
}
}
pub fn parse_sections(spec: &str) -> anyhow::Result<Vec<Section>> {
if spec.trim().is_empty() {
return Ok(Vec::new());
}
let valid_names = Section::ALL
.iter()
.map(|s| s.cli_name())
.collect::<Vec<_>>()
.join(", ");
let mut out: Vec<Section> = Vec::new();
let mut seen: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
for entry in spec.split(',') {
let entry = entry.trim();
if entry.is_empty() {
anyhow::bail!(
"empty entry in --sections spec {spec:?}; \
entries are comma-separated and must be non-empty"
);
}
let normalized = entry.to_ascii_lowercase();
let Some(section) = Section::ALL
.iter()
.copied()
.find(|s| s.cli_name() == normalized)
else {
anyhow::bail!(
"unknown section {entry:?} in --sections spec {spec:?}; \
must be one of: {valid_names}",
);
};
if !seen.insert(section.cli_name()) {
anyhow::bail!(
"duplicate section {entry:?} in --sections spec {spec:?}; \
each section may appear at most once"
);
}
out.push(section);
}
Ok(out)
}
pub fn parse_metrics(spec: &str) -> anyhow::Result<Vec<&'static str>> {
if spec.trim().is_empty() {
return Ok(Vec::new());
}
let mut out: Vec<&'static str> = Vec::new();
let mut seen: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
for entry in spec.split(',') {
let entry = entry.trim();
if entry.is_empty() {
anyhow::bail!(
"empty entry in --metrics spec {spec:?}; \
entries are comma-separated and must be non-empty"
);
}
let primary = CTPROF_METRICS
.iter()
.find(|m| m.name == entry)
.map(|m| m.name);
let derived = CTPROF_DERIVED_METRICS
.iter()
.find(|d| d.name == entry)
.map(|d| d.name);
let Some(name) = primary.or(derived) else {
anyhow::bail!(
"unknown metric {entry:?} in --metrics spec {spec:?}; \
must be one of the names from `ctprof metric-list` \
(CTPROF_METRICS or CTPROF_DERIVED_METRICS)",
);
};
if !seen.insert(name) {
anyhow::bail!(
"duplicate metric {entry:?} in --metrics spec {spec:?}; \
each metric may appear at most once"
);
}
out.push(name);
}
Ok(out)
}