use std::collections::BTreeMap;
use std::fmt::Write as _;
use super::meta::Unit;
use super::{Meta, Stats};
pub fn format(stats: &Stats, cycles: u64, instructions_retired: u64) -> String {
format_sections(stats, cycles, instructions_retired, None)
}
pub fn format_sections(
stats: &Stats,
cycles: u64,
instructions_retired: u64,
sections: Option<&[&str]>,
) -> String {
let mut out = String::new();
let _ = writeln!(out, "cycles {cycles}");
let _ = writeln!(out, "instructions_retired {instructions_retired}");
let include = |subject: &str| sections.is_none_or(|list| list.contains(&subject));
let entries: Vec<Entry<'_>> =
collect_entries(stats).into_iter().filter(|e| include(e.subject)).collect();
if entries.is_empty() {
return out;
}
let mut by_subject: BTreeMap<&str, Vec<Entry<'_>>> = BTreeMap::new();
for entry in &entries {
by_subject.entry(entry.subject).or_default().push(entry.clone());
}
let name_width = entries.iter().map(|e| e.tail.len()).max().unwrap_or(0).max(20);
for (subject, mut rows) in by_subject {
rows.sort_by(|a, b| a.tail.cmp(b.tail));
let _ = writeln!(out);
let _ = writeln!(out, "[{subject}]");
for row in rows {
let value_str = format_value(row.value, row.meta.unit);
let _ = writeln!(out, " {:<name_width$} {value_str}", row.tail);
}
}
out
}
#[derive(Clone)]
struct Entry<'a> {
subject: &'a str,
tail: &'a str,
value: f64,
meta: Meta,
}
fn collect_entries(stats: &Stats) -> Vec<Entry<'_>> {
let mut entries = Vec::new();
for (&stat, meta) in &stats.meta {
let Some(value) = stats.get(stat) else { continue };
let path = stat.path();
let (subject, tail) = path.split_once('.').unwrap_or((path, ""));
entries.push(Entry { subject, tail, value, meta: *meta });
}
entries
}
fn format_value(value: f64, unit: Unit) -> String {
match unit {
Unit::Events | Unit::Cycles | Unit::Bytes => format!("{}", value as u64),
Unit::Ratio => format!("{value:.4}"),
Unit::Rate => format!("{value:.2}"),
Unit::Percent => format!("{value:.2}%"),
}
}
#[cfg(test)]
mod tests {
use super::super::{Formula, Meta};
use super::*;
#[test]
fn empty_stats_emits_header_only() {
let stats = Stats::new();
let s = format(&stats, 0, 0);
assert!(s.contains("cycles"));
assert!(s.contains("instructions_retired"));
}
#[test]
fn groups_by_subject_and_includes_derived() {
let mut stats = Stats::new();
stats.register("core0.commit.op.load", Meta::events("integer load retired"));
stats.register("core0.commit.op.store", Meta::events("integer store retired"));
stats.derive(
"core0.ipc",
Formula::Div("core0.commit.op.load".into(), "core0.commit.op.store".into()),
Meta::ratio("instructions per cycle"),
);
stats.counter("core0.commit.op.load").add(10);
stats.counter("core0.commit.op.store").add(5);
let out = format(&stats, 100, 42);
assert!(out.contains("[core0]"));
assert!(out.contains("commit.op.load"));
assert!(out.contains("ipc"));
}
}