Skip to main content

rucc_driver/
trace.rs

1//! What `-frucc-trace=<file>` writes: one line of JSON per file compiled, saying where the time
2//! went.
3//!
4//! The reader is a build harness, not a person. `rucc-postgres` runs every compile of a Postgres
5//! build through a shim and wants to know, for each translation unit, which phase and which
6//! optimizer pass took the time, so that a file that got slow names its cause without anybody
7//! rebuilding it under a profiler. `cargo xtask cost` already collects the same numbers for our
8//! own benchmarks, and this is those numbers on the command line.
9//!
10//! One line per file, appended with a single write, so that a parallel make with many compilers
11//! writing to one trace file gets whole lines in some order rather than lines cut into each other.
12
13use std::fmt::Write as _;
14use std::io::Write as _;
15use std::time::{Duration, Instant};
16
17/// Where the time went in one compile.
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct Timing {
20    /// Each phase that ran, in the order it ran. A compile that stopped early has fewer.
21    pub phases: Vec<(&'static str, Duration)>,
22    /// Each optimizer pass across the whole module, in the order the passes first ran.
23    pub passes: Vec<(&'static str, Duration)>,
24}
25
26/// Times phases one after another, each from where the one before it stopped.
27#[derive(Debug)]
28pub struct Clock {
29    last: Instant,
30    timing: Timing,
31}
32
33impl Clock {
34    /// Starts the clock.
35    #[must_use]
36    pub fn start() -> Clock {
37        Clock { last: Instant::now(), timing: Timing::default() }
38    }
39
40    /// Records a phase as everything since the last one.
41    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    /// Runs `f` and records it as a phase, with whatever came before it since the last lap.
48    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    /// Hands over what the optimizer said about its passes.
55    pub fn passes(&mut self, passes: Vec<(&'static str, Duration)>) {
56        self.timing.passes = passes;
57    }
58
59    /// What was recorded.
60    #[must_use]
61    pub fn finish(self) -> Timing {
62        self.timing
63    }
64}
65
66/// What one line of the trace says about one file.
67#[derive(Debug)]
68pub struct Record<'a> {
69    /// The input as the command line named it.
70    pub input: &'a str,
71    /// Where the output went, or `-` for standard output.
72    pub output: &'a str,
73    /// Whether the compile succeeded.
74    pub ok: bool,
75    /// The wall time for the whole file, which is a little more than the phases added up.
76    pub total: Duration,
77    /// The phases and passes.
78    pub timing: &'a Timing,
79}
80
81impl Record<'_> {
82    /// The line, with its newline.
83    #[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
110/// Appends one record to the trace file, creating it if it is not there.
111///
112/// # Errors
113///
114/// The message to print, naming the file.
115pub 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
125/// Seconds with microseconds, which is finer than anything here is worth measuring.
126fn seconds(time: Duration) -> String {
127    format!("{:.6}", time.as_secs_f64())
128}
129
130/// A list of names and times as one JSON object, in the order given.
131fn 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
142/// A JSON string. Paths are the only text in here that can hold anything unusual.
143fn 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
163/// The most memory the process has held so far, in kilobytes, where the system says.
164///
165/// Linux keeps it in `/proc/self/status` as `VmHWM`. Other systems want a call into the C
166/// library, which the driver does not link, so the trace says `null` there and the shim that
167/// runs the compiler measures it from outside. A compiler usually compiles one file, so for most
168/// lines this is that file's peak. With several files on one command line it is the peak up to
169/// the end of this one.
170fn 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}