use std::fmt::Write as _;
use std::io::Write as _;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Timing {
pub phases: Vec<(&'static str, Duration)>,
pub passes: Vec<(&'static str, Duration)>,
}
#[derive(Debug)]
pub struct Clock {
last: Instant,
timing: Timing,
}
impl Clock {
#[must_use]
pub fn start() -> Clock {
Clock { last: Instant::now(), timing: Timing::default() }
}
pub fn lap(&mut self, phase: &'static str) {
let now = Instant::now();
self.timing.phases.push((phase, now - self.last));
self.last = now;
}
pub fn time<T>(&mut self, phase: &'static str, f: impl FnOnce() -> T) -> T {
let out = f();
self.lap(phase);
out
}
pub fn passes(&mut self, passes: Vec<(&'static str, Duration)>) {
self.timing.passes = passes;
}
#[must_use]
pub fn finish(self) -> Timing {
self.timing
}
}
#[derive(Debug)]
pub struct Record<'a> {
pub input: &'a str,
pub output: &'a str,
pub ok: bool,
pub total: Duration,
pub timing: &'a Timing,
}
impl Record<'_> {
#[must_use]
pub fn render(&self) -> String {
let mut out = String::new();
let _ = write!(
out,
"{{\"rucc\":\"{}\",\"input\":{},\"output\":{},\"ok\":{},\"seconds\":{}",
env!("CARGO_PKG_VERSION"),
quoted(self.input),
quoted(self.output),
self.ok,
seconds(self.total)
);
match peak_kb() {
Some(kb) => {
let _ = write!(out, ",\"peak-kb\":{kb}");
}
None => out.push_str(",\"peak-kb\":null"),
}
out.push_str(",\"phases\":");
object(&mut out, &self.timing.phases);
out.push_str(",\"passes\":");
object(&mut out, &self.timing.passes);
out.push_str("}\n");
out
}
}
pub fn append(path: &str, record: &Record<'_>) -> Result<(), String> {
let line = record.render();
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.and_then(|mut file| file.write_all(line.as_bytes()))
.map_err(|e| format!("{path}: {e}"))
}
fn seconds(time: Duration) -> String {
format!("{:.6}", time.as_secs_f64())
}
fn object(out: &mut String, times: &[(&'static str, Duration)]) {
out.push('{');
for (index, (name, time)) in times.iter().enumerate() {
if index > 0 {
out.push(',');
}
let _ = write!(out, "{}:{}", quoted(name), seconds(*time));
}
out.push('}');
}
fn quoted(text: &str) -> String {
let mut out = String::with_capacity(text.len() + 2);
out.push('"');
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
ch if u32::from(ch) < 0x20 => {
let _ = write!(out, "\\u{:04x}", u32::from(ch));
}
ch => out.push(ch),
}
}
out.push('"');
out
}
fn peak_kb() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let line = status.lines().find(|line| line.starts_with("VmHWM:"))?;
line["VmHWM:".len()..].trim().trim_end_matches("kB").trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_record_is_one_line_of_json_with_the_phases_in_order() {
let timing = Timing {
phases: vec![
("preprocess", Duration::from_millis(3)),
("parse", Duration::from_micros(1500)),
],
passes: vec![("simplify", Duration::from_micros(20))],
};
let line = Record {
input: "src/a \"b\".c",
output: "a.o",
ok: true,
total: Duration::from_millis(5),
timing: &timing,
}
.render();
assert!(line.ends_with("}\n"));
assert_eq!(line.lines().count(), 1);
assert!(line.contains("\"input\":\"src/a \\\"b\\\".c\""));
assert!(line.contains("\"seconds\":0.005000"));
assert!(line.contains("\"phases\":{\"preprocess\":0.003000,\"parse\":0.001500}"));
assert!(line.contains("\"passes\":{\"simplify\":0.000020}"));
}
#[test]
fn control_characters_in_a_name_are_escaped() {
assert_eq!(quoted("a\tb\u{1}"), "\"a\\tb\\u0001\"");
}
#[test]
fn records_are_appended_to_the_file() {
let dir = std::env::temp_dir().join(format!("rucc-trace-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("t.jsonl");
let path = path.to_str().unwrap();
let timing = Timing::default();
let record = Record {
input: "a.c",
output: "a.o",
ok: true,
total: Duration::ZERO,
timing: &timing,
};
append(path, &record).unwrap();
append(path, &record).unwrap();
let text = std::fs::read_to_string(path).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
assert_eq!(text.lines().count(), 2);
}
}