1use std::fmt::Write as _;
14use std::io::Write as _;
15use std::time::{Duration, Instant};
16
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct Timing {
20 pub phases: Vec<(&'static str, Duration)>,
22 pub passes: Vec<(&'static str, Duration)>,
24}
25
26#[derive(Debug)]
28pub struct Clock {
29 last: Instant,
30 timing: Timing,
31}
32
33impl Clock {
34 #[must_use]
36 pub fn start() -> Clock {
37 Clock { last: Instant::now(), timing: Timing::default() }
38 }
39
40 pub fn lap(&mut self, phase: &'static str) {
42 let now = Instant::now();
43 self.timing.phases.push((phase, now - self.last));
44 self.last = now;
45 }
46
47 pub fn time<T>(&mut self, phase: &'static str, f: impl FnOnce() -> T) -> T {
49 let out = f();
50 self.lap(phase);
51 out
52 }
53
54 pub fn passes(&mut self, passes: Vec<(&'static str, Duration)>) {
56 self.timing.passes = passes;
57 }
58
59 #[must_use]
61 pub fn finish(self) -> Timing {
62 self.timing
63 }
64}
65
66#[derive(Debug)]
68pub struct Record<'a> {
69 pub input: &'a str,
71 pub output: &'a str,
73 pub ok: bool,
75 pub total: Duration,
77 pub timing: &'a Timing,
79}
80
81impl Record<'_> {
82 #[must_use]
84 pub fn render(&self) -> String {
85 let mut out = String::new();
86 let _ = write!(
87 out,
88 "{{\"rucc\":\"{}\",\"input\":{},\"output\":{},\"ok\":{},\"seconds\":{}",
89 env!("CARGO_PKG_VERSION"),
90 quoted(self.input),
91 quoted(self.output),
92 self.ok,
93 seconds(self.total)
94 );
95 match peak_kb() {
96 Some(kb) => {
97 let _ = write!(out, ",\"peak-kb\":{kb}");
98 }
99 None => out.push_str(",\"peak-kb\":null"),
100 }
101 out.push_str(",\"phases\":");
102 object(&mut out, &self.timing.phases);
103 out.push_str(",\"passes\":");
104 object(&mut out, &self.timing.passes);
105 out.push_str("}\n");
106 out
107 }
108}
109
110pub fn append(path: &str, record: &Record<'_>) -> Result<(), String> {
116 let line = record.render();
117 std::fs::OpenOptions::new()
118 .create(true)
119 .append(true)
120 .open(path)
121 .and_then(|mut file| file.write_all(line.as_bytes()))
122 .map_err(|e| format!("{path}: {e}"))
123}
124
125fn seconds(time: Duration) -> String {
127 format!("{:.6}", time.as_secs_f64())
128}
129
130fn object(out: &mut String, times: &[(&'static str, Duration)]) {
132 out.push('{');
133 for (index, (name, time)) in times.iter().enumerate() {
134 if index > 0 {
135 out.push(',');
136 }
137 let _ = write!(out, "{}:{}", quoted(name), seconds(*time));
138 }
139 out.push('}');
140}
141
142fn quoted(text: &str) -> String {
144 let mut out = String::with_capacity(text.len() + 2);
145 out.push('"');
146 for ch in text.chars() {
147 match ch {
148 '"' => out.push_str("\\\""),
149 '\\' => out.push_str("\\\\"),
150 '\n' => out.push_str("\\n"),
151 '\r' => out.push_str("\\r"),
152 '\t' => out.push_str("\\t"),
153 ch if u32::from(ch) < 0x20 => {
154 let _ = write!(out, "\\u{:04x}", u32::from(ch));
155 }
156 ch => out.push(ch),
157 }
158 }
159 out.push('"');
160 out
161}
162
163fn peak_kb() -> Option<u64> {
171 let status = std::fs::read_to_string("/proc/self/status").ok()?;
172 let line = status.lines().find(|line| line.starts_with("VmHWM:"))?;
173 line["VmHWM:".len()..].trim().trim_end_matches("kB").trim().parse().ok()
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn a_record_is_one_line_of_json_with_the_phases_in_order() {
182 let timing = Timing {
183 phases: vec![
184 ("preprocess", Duration::from_millis(3)),
185 ("parse", Duration::from_micros(1500)),
186 ],
187 passes: vec![("simplify", Duration::from_micros(20))],
188 };
189 let line = Record {
190 input: "src/a \"b\".c",
191 output: "a.o",
192 ok: true,
193 total: Duration::from_millis(5),
194 timing: &timing,
195 }
196 .render();
197 assert!(line.ends_with("}\n"));
198 assert_eq!(line.lines().count(), 1);
199 assert!(line.contains("\"input\":\"src/a \\\"b\\\".c\""));
200 assert!(line.contains("\"seconds\":0.005000"));
201 assert!(line.contains("\"phases\":{\"preprocess\":0.003000,\"parse\":0.001500}"));
202 assert!(line.contains("\"passes\":{\"simplify\":0.000020}"));
203 }
204
205 #[test]
206 fn control_characters_in_a_name_are_escaped() {
207 assert_eq!(quoted("a\tb\u{1}"), "\"a\\tb\\u0001\"");
208 }
209
210 #[test]
211 fn records_are_appended_to_the_file() {
212 let dir = std::env::temp_dir().join(format!("rucc-trace-{}", std::process::id()));
213 std::fs::create_dir_all(&dir).unwrap();
214 let path = dir.join("t.jsonl");
215 let path = path.to_str().unwrap();
216 let timing = Timing::default();
217 let record = Record {
218 input: "a.c",
219 output: "a.o",
220 ok: true,
221 total: Duration::ZERO,
222 timing: &timing,
223 };
224 append(path, &record).unwrap();
225 append(path, &record).unwrap();
226 let text = std::fs::read_to_string(path).unwrap();
227 std::fs::remove_dir_all(&dir).unwrap();
228 assert_eq!(text.lines().count(), 2);
229 }
230}