use perfgate_types::{CompareReceipt, Metric, MetricStatus, RunReceipt};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RunExportRow {
pub bench_name: String,
pub wall_ms_median: u64,
pub wall_ms_min: u64,
pub wall_ms_max: u64,
pub binary_bytes_median: Option<u64>,
pub cpu_ms_median: Option<u64>,
pub ctx_switches_median: Option<u64>,
pub energy_uj_median: Option<u64>,
pub max_rss_kb_median: Option<u64>,
pub page_faults_median: Option<u64>,
pub io_read_bytes_median: Option<u64>,
pub io_write_bytes_median: Option<u64>,
pub network_packets_median: Option<u64>,
pub throughput_median: Option<f64>,
pub sample_count: usize,
pub timestamp: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CompareExportRow {
pub bench_name: String,
pub metric: String,
pub baseline_value: f64,
pub current_value: f64,
pub regression_pct: f64,
pub status: String,
pub threshold: f64,
pub warn_threshold: Option<f64>,
pub cv: Option<f64>,
pub noise_threshold: Option<f64>,
}
pub(super) fn run_to_row(receipt: &RunReceipt) -> RunExportRow {
let sample_count = receipt.samples.iter().filter(|s| !s.warmup).count();
RunExportRow {
bench_name: receipt.bench.name.clone(),
wall_ms_median: receipt.stats.wall_ms.median,
wall_ms_min: receipt.stats.wall_ms.min,
wall_ms_max: receipt.stats.wall_ms.max,
binary_bytes_median: receipt.stats.binary_bytes.as_ref().map(|s| s.median),
cpu_ms_median: receipt.stats.cpu_ms.as_ref().map(|s| s.median),
ctx_switches_median: receipt.stats.ctx_switches.as_ref().map(|s| s.median),
energy_uj_median: receipt.stats.energy_uj.as_ref().map(|s| s.median),
max_rss_kb_median: receipt.stats.max_rss_kb.as_ref().map(|s| s.median),
page_faults_median: receipt.stats.page_faults.as_ref().map(|s| s.median),
io_read_bytes_median: receipt.stats.io_read_bytes.as_ref().map(|s| s.median),
io_write_bytes_median: receipt.stats.io_write_bytes.as_ref().map(|s| s.median),
network_packets_median: receipt.stats.network_packets.as_ref().map(|s| s.median),
throughput_median: receipt.stats.throughput_per_s.as_ref().map(|s| s.median),
sample_count,
timestamp: receipt.run.started_at.clone(),
}
}
pub(super) fn compare_to_rows(receipt: &CompareReceipt) -> Vec<CompareExportRow> {
let mut rows: Vec<CompareExportRow> = receipt
.deltas
.iter()
.map(|(metric, delta)| {
let budget = receipt.budgets.get(metric);
let threshold = budget.map(|b| b.threshold).unwrap_or(0.0);
let warn_threshold = budget.map(|b| b.warn_threshold);
CompareExportRow {
bench_name: receipt.bench.name.clone(),
metric: metric_to_string(*metric),
baseline_value: delta.baseline,
current_value: delta.current,
regression_pct: delta.regression * 100.0,
status: status_to_string(delta.status),
threshold: threshold * 100.0,
warn_threshold: warn_threshold.map(|t| t * 100.0),
cv: delta.cv.map(|cv| cv * 100.0),
noise_threshold: delta.noise_threshold.map(|t| t * 100.0),
}
})
.collect();
rows.sort_by(|a, b| a.metric.cmp(&b.metric));
rows
}
fn metric_to_string(metric: Metric) -> String {
metric.as_str().to_string()
}
fn status_to_string(status: MetricStatus) -> String {
match status {
MetricStatus::Pass => "pass".to_string(),
MetricStatus::Warn => "warn".to_string(),
MetricStatus::Fail => "fail".to_string(),
MetricStatus::Skip => "skip".to_string(),
}
}