use crate::grid::Value;
use crate::json;
use crate::runner::{ConditionResult, RunInfo, RunReport};
use crate::stats::Estimate;
use comnum_core::Result;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
pub trait Logger: Send {
fn start(&mut self, _info: &RunInfo) -> Result<()> {
Ok(())
}
fn log(&mut self, result: &ConditionResult, confidence: f64) -> Result<()>;
fn finish(&mut self, _report: &RunReport) -> Result<()> {
Ok(())
}
}
pub fn csv_field(s: &str) -> String {
if s.contains([',', '"', '\n']) { format!("\"{}\"", s.replace('"', "\"\"")) } else { s.to_string() }
}
pub fn create_parent_dir(path: &Path) -> Result<()> {
if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
std::fs::create_dir_all(dir)?;
}
Ok(())
}
pub struct CsvLogger {
path: PathBuf,
out: Option<BufWriter<File>>,
header: bool,
keys: Vec<String>,
columns: Vec<String>,
pending: Vec<(ConditionResult, f64)>,
}
impl CsvLogger {
pub fn new(path: impl Into<PathBuf>) -> CsvLogger {
CsvLogger {
path: path.into(),
out: None,
header: false,
keys: Vec::new(),
columns: Vec::new(),
pending: Vec::new(),
}
}
fn write_header(&mut self, r: &ConditionResult, confidence: f64) -> Result<()> {
self.keys = r.condition.keys().map(str::to_string).collect();
self.columns = r.row(confidence).into_iter().map(|(k, _)| k).collect();
let header: Vec<String> = self.columns.iter().map(|k| csv_field(k)).collect();
if let Some(out) = self.out.as_mut() {
writeln!(out, "{}", header.join(","))?;
}
self.header = true;
Ok(())
}
fn cell(column: &str, v: &Value, metric: bool) -> String {
match v {
Value::F64(x) if !metric && column == "wall_s" => format!("{x:.6}"),
Value::F64(x) if metric && x.is_finite() => format!("{x:e}"),
Value::F64(_) if metric => String::new(),
other => csv_field(&other.to_string()),
}
}
fn write_row(&mut self, r: &ConditionResult, confidence: f64) -> Result<()> {
let cells = r.row(confidence);
let (head, metrics) = cells.split_at(4 + r.condition.entries().len());
let (columns_head, columns_metrics) = self.columns.split_at(4 + self.keys.len());
let condition = &head[1..head.len() - 3];
let mut row = vec![Self::cell("index", &head[0].1, false)];
row.extend(
self.keys.iter().map(|k| {
condition.iter().find(|(c, _)| c == k).map(|(_, v)| Self::cell(k, v, false)).unwrap_or_default()
}),
);
row.extend(head[head.len() - 3..].iter().map(|(k, v)| Self::cell(k, v, false)));
debug_assert_eq!(row.len(), columns_head.len());
row.extend(
columns_metrics
.iter()
.map(|c| metrics.iter().find(|(k, _)| k == c).map(|(_, v)| Self::cell(c, v, true)).unwrap_or_default()),
);
if let Some(out) = self.out.as_mut() {
writeln!(out, "{}", row.join(","))?;
out.flush()?;
}
Ok(())
}
}
impl Logger for CsvLogger {
fn start(&mut self, _: &RunInfo) -> Result<()> {
create_parent_dir(&self.path)?;
self.out = Some(BufWriter::new(File::create(&self.path)?));
self.header = false;
self.pending.clear();
Ok(())
}
fn log(&mut self, r: &ConditionResult, confidence: f64) -> Result<()> {
if !self.header {
if r.trials == 0 {
self.pending.push((r.clone(), confidence));
return Ok(());
}
self.write_header(r, confidence)?;
for (p, c) in std::mem::take(&mut self.pending) {
self.write_row(&p, c)?;
}
}
self.write_row(r, confidence)
}
fn finish(&mut self, _: &RunReport) -> Result<()> {
if !self.header {
if let Some((first, c)) = self.pending.first().cloned() {
self.write_header(&first, c)?;
}
for (p, c) in std::mem::take(&mut self.pending) {
self.write_row(&p, c)?;
}
}
if let Some(out) = self.out.as_mut() {
out.flush()?;
}
Ok(())
}
}
pub struct JsonLogger {
path: PathBuf,
}
impl JsonLogger {
pub fn new(path: impl Into<PathBuf>) -> JsonLogger {
JsonLogger { path: path.into() }
}
}
pub fn value_json(v: &Value) -> String {
match v {
Value::F64(x) => json::number(*x),
Value::I64(x) => x.to_string(),
Value::Str(s) => json::string(s),
Value::Bool(b) => b.to_string(),
}
}
fn estimate_json(e: &Estimate, confidence: f64) -> String {
let (lo, hi) = e.interval(confidence);
let mut f = vec![("value", json::number(e.value())), ("lo", json::number(lo)), ("hi", json::number(hi))];
match e {
Estimate::Rate(r) => {
f.push(("kind", json::string("rate")));
f.push(("errors", r.errors.to_string()));
f.push(("total", r.total.to_string()));
}
Estimate::Mean(m) => {
f.push(("kind", json::string("mean")));
f.push(("count", m.count().to_string()));
f.push(("std", json::number(m.std())));
}
}
json::object(&f)
}
pub fn run_header(i: &RunInfo) -> Vec<(&'static str, String)> {
vec![
("scenario", json::string(&i.scenario)),
("seed", i.seed.to_string()),
("batch", i.batch.to_string()),
("threads", i.threads.to_string()),
("stop", json::string(&i.stop)),
("confidence", json::number(i.confidence)),
("version", json::string(&i.version)),
("commit", json::string(&i.commit)),
("config", i.config.clone().unwrap_or_else(|| "null".into())),
("started_unix_s", i.started_unix_s.to_string()),
]
}
pub fn write_json(path: &Path, doc: &str) -> Result<()> {
create_parent_dir(path)?;
std::fs::write(path, format!("{doc}\n"))?;
Ok(())
}
impl Logger for JsonLogger {
fn log(&mut self, _: &ConditionResult, _: f64) -> Result<()> {
Ok(())
}
fn finish(&mut self, rep: &RunReport) -> Result<()> {
let i = &rep.info;
let conditions: Vec<String> = rep
.results
.iter()
.map(|r| {
let cond: Vec<(&str, String)> =
r.condition.entries().iter().map(|(k, v)| (k.as_str(), value_json(v))).collect();
let metrics: Vec<(&str, String)> =
r.record.iter().map(|(k, e)| (k.as_str(), estimate_json(e, i.confidence))).collect();
let dropped: Vec<(&str, String)> =
r.record.dropped_iter().map(|(k, n)| (k.as_str(), n.to_string())).collect();
json::object(&[
("index", r.index.to_string()),
("condition", json::object(&cond)),
("status", json::string(r.status.as_str())),
("trials", r.trials.to_string()),
("wall_s", json::number(r.wall_s)),
("metrics", json::object(&metrics)),
("dropped", json::object(&dropped)),
])
})
.collect();
let mut fields = run_header(i);
fields.push(("wall_s", json::number(rep.wall_s)));
fields.push(("conditions", json::array(&conditions)));
write_json(&self.path, &json::object(&fields))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use crate::scenario::from_fn;
#[test]
fn the_json_report_lists_the_dropped_values() {
let dir = std::env::temp_dir().join(format!("comnum-dropped-{}", std::process::id()));
let path = dir.join("r.json");
let s = from_fn("drops", |_, _, rec| {
rec.value("snr_db", f64::INFINITY);
rec.value("evm", 0.1);
Ok(())
});
Runner::new().batch(4).stop(Stop::max_trials(6)).logger(JsonLogger::new(&path)).run(&s, &Grid::new()).unwrap();
let json = std::fs::read_to_string(&path).unwrap();
assert!(json.contains("\"dropped\": {\"snr_db\": 6}"), "{json}");
std::fs::remove_dir_all(&dir).unwrap();
}
}