1use crate::grid::Value;
44use crate::json;
45use crate::runner::{ConditionResult, RunInfo, RunReport};
46use crate::stats::Estimate;
47use comnum_core::Result;
48use std::fs::File;
49use std::io::{BufWriter, Write};
50use std::path::{Path, PathBuf};
51
52pub trait Logger: Send {
54 fn start(&mut self, _info: &RunInfo) -> Result<()> {
59 Ok(())
60 }
61 fn log(&mut self, result: &ConditionResult, confidence: f64) -> Result<()>;
66 fn finish(&mut self, _report: &RunReport) -> Result<()> {
71 Ok(())
72 }
73}
74
75pub fn csv_field(s: &str) -> String {
85 if s.contains([',', '"', '\n']) { format!("\"{}\"", s.replace('"', "\"\"")) } else { s.to_string() }
86}
87
88pub fn create_parent_dir(path: &Path) -> Result<()> {
93 if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
94 std::fs::create_dir_all(dir)?;
95 }
96 Ok(())
97}
98
99pub struct CsvLogger {
108 path: PathBuf,
109 out: Option<BufWriter<File>>,
110 header: bool,
111 keys: Vec<String>,
112 columns: Vec<String>,
113 pending: Vec<(ConditionResult, f64)>,
114}
115
116impl CsvLogger {
117 pub fn new(path: impl Into<PathBuf>) -> CsvLogger {
120 CsvLogger {
121 path: path.into(),
122 out: None,
123 header: false,
124 keys: Vec::new(),
125 columns: Vec::new(),
126 pending: Vec::new(),
127 }
128 }
129
130 fn write_header(&mut self, r: &ConditionResult, confidence: f64) -> Result<()> {
131 self.keys = r.condition.keys().map(str::to_string).collect();
132 self.columns = r.row(confidence).into_iter().map(|(k, _)| k).collect();
133 let header: Vec<String> = self.columns.iter().map(|k| csv_field(k)).collect();
134 if let Some(out) = self.out.as_mut() {
135 writeln!(out, "{}", header.join(","))?;
136 }
137 self.header = true;
138 Ok(())
139 }
140
141 fn cell(column: &str, v: &Value, metric: bool) -> String {
144 match v {
145 Value::F64(x) if !metric && column == "wall_s" => format!("{x:.6}"),
146 Value::F64(x) if metric && x.is_finite() => format!("{x:e}"),
147 Value::F64(_) if metric => String::new(),
148 other => csv_field(&other.to_string()),
149 }
150 }
151
152 fn write_row(&mut self, r: &ConditionResult, confidence: f64) -> Result<()> {
153 let cells = r.row(confidence);
154 let (head, metrics) = cells.split_at(4 + r.condition.entries().len());
158 let (columns_head, columns_metrics) = self.columns.split_at(4 + self.keys.len());
159 let condition = &head[1..head.len() - 3];
160 let mut row = vec![Self::cell("index", &head[0].1, false)];
161 row.extend(
162 self.keys.iter().map(|k| {
163 condition.iter().find(|(c, _)| c == k).map(|(_, v)| Self::cell(k, v, false)).unwrap_or_default()
164 }),
165 );
166 row.extend(head[head.len() - 3..].iter().map(|(k, v)| Self::cell(k, v, false)));
167 debug_assert_eq!(row.len(), columns_head.len());
168 row.extend(
169 columns_metrics
170 .iter()
171 .map(|c| metrics.iter().find(|(k, _)| k == c).map(|(_, v)| Self::cell(c, v, true)).unwrap_or_default()),
172 );
173 if let Some(out) = self.out.as_mut() {
174 writeln!(out, "{}", row.join(","))?;
175 out.flush()?;
176 }
177 Ok(())
178 }
179}
180
181impl Logger for CsvLogger {
182 fn start(&mut self, _: &RunInfo) -> Result<()> {
183 create_parent_dir(&self.path)?;
184 self.out = Some(BufWriter::new(File::create(&self.path)?));
185 self.header = false;
186 self.pending.clear();
187 Ok(())
188 }
189
190 fn log(&mut self, r: &ConditionResult, confidence: f64) -> Result<()> {
191 if !self.header {
192 if r.trials == 0 {
193 self.pending.push((r.clone(), confidence));
194 return Ok(());
195 }
196 self.write_header(r, confidence)?;
197 for (p, c) in std::mem::take(&mut self.pending) {
198 self.write_row(&p, c)?;
199 }
200 }
201 self.write_row(r, confidence)
202 }
203
204 fn finish(&mut self, _: &RunReport) -> Result<()> {
205 if !self.header {
206 if let Some((first, c)) = self.pending.first().cloned() {
207 self.write_header(&first, c)?;
208 }
209 for (p, c) in std::mem::take(&mut self.pending) {
210 self.write_row(&p, c)?;
211 }
212 }
213 if let Some(out) = self.out.as_mut() {
214 out.flush()?;
215 }
216 Ok(())
217 }
218}
219
220pub struct JsonLogger {
222 path: PathBuf,
223}
224
225impl JsonLogger {
226 pub fn new(path: impl Into<PathBuf>) -> JsonLogger {
228 JsonLogger { path: path.into() }
229 }
230}
231
232pub fn value_json(v: &Value) -> String {
241 match v {
242 Value::F64(x) => json::number(*x),
243 Value::I64(x) => x.to_string(),
244 Value::Str(s) => json::string(s),
245 Value::Bool(b) => b.to_string(),
246 }
247}
248
249fn estimate_json(e: &Estimate, confidence: f64) -> String {
250 let (lo, hi) = e.interval(confidence);
251 let mut f = vec![("value", json::number(e.value())), ("lo", json::number(lo)), ("hi", json::number(hi))];
252 match e {
253 Estimate::Rate(r) => {
254 f.push(("kind", json::string("rate")));
255 f.push(("errors", r.errors.to_string()));
256 f.push(("total", r.total.to_string()));
257 }
258 Estimate::Mean(m) => {
259 f.push(("kind", json::string("mean")));
260 f.push(("count", m.count().to_string()));
261 f.push(("std", json::number(m.std())));
262 }
263 }
264 json::object(&f)
265}
266
267pub fn run_header(i: &RunInfo) -> Vec<(&'static str, String)> {
272 vec![
273 ("scenario", json::string(&i.scenario)),
274 ("seed", i.seed.to_string()),
275 ("batch", i.batch.to_string()),
276 ("threads", i.threads.to_string()),
277 ("stop", json::string(&i.stop)),
278 ("confidence", json::number(i.confidence)),
279 ("version", json::string(&i.version)),
280 ("commit", json::string(&i.commit)),
281 ("config", i.config.clone().unwrap_or_else(|| "null".into())),
282 ("started_unix_s", i.started_unix_s.to_string()),
283 ]
284}
285
286pub fn write_json(path: &Path, doc: &str) -> Result<()> {
291 create_parent_dir(path)?;
292 std::fs::write(path, format!("{doc}\n"))?;
293 Ok(())
294}
295
296impl Logger for JsonLogger {
297 fn log(&mut self, _: &ConditionResult, _: f64) -> Result<()> {
298 Ok(())
299 }
300
301 fn finish(&mut self, rep: &RunReport) -> Result<()> {
302 let i = &rep.info;
303 let conditions: Vec<String> = rep
304 .results
305 .iter()
306 .map(|r| {
307 let cond: Vec<(&str, String)> =
308 r.condition.entries().iter().map(|(k, v)| (k.as_str(), value_json(v))).collect();
309 let metrics: Vec<(&str, String)> =
310 r.record.iter().map(|(k, e)| (k.as_str(), estimate_json(e, i.confidence))).collect();
311 let dropped: Vec<(&str, String)> =
312 r.record.dropped_iter().map(|(k, n)| (k.as_str(), n.to_string())).collect();
313 json::object(&[
314 ("index", r.index.to_string()),
315 ("condition", json::object(&cond)),
316 ("status", json::string(r.status.as_str())),
317 ("trials", r.trials.to_string()),
318 ("wall_s", json::number(r.wall_s)),
319 ("metrics", json::object(&metrics)),
320 ("dropped", json::object(&dropped)),
321 ])
322 })
323 .collect();
324 let mut fields = run_header(i);
325 fields.push(("wall_s", json::number(rep.wall_s)));
326 fields.push(("conditions", json::array(&conditions)));
327 write_json(&self.path, &json::object(&fields))
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::prelude::*;
335 use crate::scenario::from_fn;
336
337 #[test]
338 fn the_json_report_lists_the_dropped_values() {
339 let dir = std::env::temp_dir().join(format!("comnum-dropped-{}", std::process::id()));
340 let path = dir.join("r.json");
341 let s = from_fn("drops", |_, _, rec| {
342 rec.value("snr_db", f64::INFINITY);
343 rec.value("evm", 0.1);
344 Ok(())
345 });
346 Runner::new().batch(4).stop(Stop::max_trials(6)).logger(JsonLogger::new(&path)).run(&s, &Grid::new()).unwrap();
347 let json = std::fs::read_to_string(&path).unwrap();
348 assert!(json.contains("\"dropped\": {\"snr_db\": 6}"), "{json}");
349 std::fs::remove_dir_all(&dir).unwrap();
350 }
351}