// SDD-120 ยง5 โ€” JSON post-processor binary. // // Reads criterion's per-bench `target/criterion///estimates.json` // outputs, plus the `capture_alloc_count.sidecar` value written by // `realtime_capture.rs`, and projects them into the SRS-217 schema: // { // "host": "x86_64-unknown-linux-gnu", // "toolchain": "", // "git_sha": "", // "timestamp": "", // "metrics": [ // { "metric": "", "value": , "unit": "", // "tolerance_pct": , ... }, // ... // ] // } // // Writes the document to `./current.json`. // // Run as: `cargo run --bin emit_baseline -p chanora_audio`. use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; use serde_json::{json, Value}; /// Per-metric SRS-219 tolerance entries. Zero-tolerance metrics /// per SRS-219 clause a use 0.0 here; the comparator collapses /// them to ๐Ÿ”ด/๐ŸŸข only. fn metric_table() -> Vec<(&'static str, &'static str, f64)> { // (metric_name, unit, tolerance_pct) vec![ ("capture_alloc_count", "blocks", 0.0), ("capture_callback_wall_clock", "ns", 0.10), ("opus_encode_latency", "ns", 0.10), ("opus_decode_latency", "ns", 0.10), ("resampler_44100_to_48000", "samples_per_sec", 0.10), ("resampler_16000_to_48000", "samples_per_sec", 0.10), ("resampler_48000_passthrough", "samples_per_sec", 0.10), ] } fn target_dir() -> PathBuf { env::var("CARGO_TARGET_DIR") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("target")) } /// Read criterion `estimates.json` for a given bench function /// name (criterion writes one per non-grouped bench under /// `target/criterion//new/estimates.json`). fn read_estimate_ns(name: &str) -> Option { let path = target_dir() .join("criterion") .join(name) .join("new") .join("estimates.json"); let text = fs::read_to_string(&path).ok()?; let v: Value = serde_json::from_str(&text).ok()?; // criterion estimates.json has `mean.point_estimate` (in ns). v.get("mean") .and_then(|m| m.get("point_estimate")) .and_then(|p| p.as_f64()) } /// Read criterion `estimates.json` for a sub-bench inside a group /// (criterion writes group sub-benches under /// `target/criterion///new/estimates.json`). fn read_estimate_group_ns(group: &str, sub: &str) -> Option { let path = target_dir() .join("criterion") .join(group) .join(sub) .join("new") .join("estimates.json"); let text = fs::read_to_string(&path).ok()?; let v: Value = serde_json::from_str(&text).ok()?; v.get("mean") .and_then(|m| m.get("point_estimate")) .and_then(|p| p.as_f64()) } /// Resampler reports throughput; convert ns-per-iter to samples /// per second using the per-iter input length (1s @ rate). fn ns_to_throughput(ns_per_iter: f64, samples_per_iter: f64) -> f64 { if ns_per_iter <= 0.0 { return 0.0; } samples_per_iter * 1_000_000_000.0 / ns_per_iter } fn read_alloc_sidecar() -> Option { let path = target_dir() .join("criterion") .join("capture_alloc_count.sidecar"); let s = fs::read_to_string(&path).ok()?; s.trim().parse::().ok() } fn rustc_version() -> String { Command::new("rustc") .arg("-V") .output() .ok() .and_then(|o| { if o.status.success() { Some(String::from_utf8_lossy(&o.stdout).trim().to_string()) } else { None } }) .unwrap_or_else(|| "unknown".to_string()) } fn git_sha() -> String { Command::new("git") .args(["rev-parse", "HEAD"]) .output() .ok() .and_then(|o| { if o.status.success() { Some(String::from_utf8_lossy(&o.stdout).trim().to_string()) } else { None } }) .unwrap_or_else(|| "unknown".to_string()) } fn rfc3339_now() -> String { let secs = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); // Minimal RFC3339-style UTC encoder; avoids pulling chrono. // Algorithm: civil-from-days (Howard Hinnant). let z = secs / 86_400; let day_secs = secs % 86_400; let hh = day_secs / 3600; let mm = (day_secs % 3600) / 60; let ss = day_secs % 60; let z_shift = z as i64 + 719_468; let era = z_shift.div_euclid(146_097); let doe = z_shift.rem_euclid(146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let year = if m <= 2 { y + 1 } else { y }; format!( "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", year, m, d, hh, mm, ss ) } fn main() { let mut metrics: Vec = Vec::new(); for (name, unit, tol) in metric_table() { let value = match name { "capture_alloc_count" => read_alloc_sidecar(), "capture_callback_wall_clock" => read_estimate_ns(name), "opus_encode_latency" => read_estimate_ns(name), "opus_decode_latency" => read_estimate_ns(name), "resampler_44100_to_48000" => { read_estimate_group_ns("resampler_throughput", "44100_to_48000") .map(|ns| ns_to_throughput(ns, 44_100.0)) } "resampler_16000_to_48000" => { read_estimate_group_ns("resampler_throughput", "16000_to_48000") .map(|ns| ns_to_throughput(ns, 16_000.0)) } "resampler_48000_passthrough" => { read_estimate_group_ns("resampler_throughput", "48000_passthrough") .map(|ns| ns_to_throughput(ns, 48_000.0)) } _ => None, }; let v = value.unwrap_or(f64::NAN); metrics.push(json!({ "metric": name, "value": if v.is_finite() { Value::from(v) } else { Value::Null }, "unit": unit, "tolerance_pct": tol, })); } let doc = json!({ "host": "x86_64-unknown-linux-gnu", "toolchain": rustc_version(), "git_sha": git_sha(), "timestamp": rfc3339_now(), "metrics": metrics, }); let out_path = Path::new("current.json"); fs::write(out_path, serde_json::to_string_pretty(&doc).unwrap()).expect("write current.json"); eprintln!("emit_baseline: wrote {}", out_path.display()); }