Skip to main content

reference_query/
profile.rs

1//! Phase timing for `--profile`.
2//!
3//! [`trace`](crate::trace) answers "what did this run decide?" — the resolved
4//! root, coverage, what got warmed. This answers "where did the time go?", and
5//! keeps the two apart: trace lines are prose meant to be read as they happen,
6//! phases are a table meant to be compared against another run.
7//!
8//! Off by default and free when off, on the same terms as trace: a span reads
9//! no clock, takes no lock and allocates nothing unless profiling is on, so the
10//! only cost left on the search path is a relaxed atomic load per phase.
11//!
12//! Streaming makes one measurement matter more than the total: `first result`
13//! is the number the sub-50 ms budget is about, and a change that improves the
14//! total while delaying the first answer is a regression here.
15
16use std::sync::Mutex;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::time::{Duration, Instant};
19
20static ENABLED: AtomicBool = AtomicBool::new(false);
21static PHASES: Mutex<Vec<Phase>> = Mutex::new(Vec::new());
22
23/// One measured phase.
24pub struct Phase {
25    pub name: &'static str,
26    pub elapsed: Duration,
27    /// What the phase did — candidate counts, symbols scored, a cache verdict.
28    pub note: Option<String>,
29}
30
31/// Enable profiling from the `--profile` flag; `RQ_PROFILE` in the environment
32/// also enables it, so a shipped binary can be measured in place — the same
33/// affordance `RQ_LOG` gives trace.
34pub fn enable_from(flag: bool) {
35    let on = flag || std::env::var_os("RQ_PROFILE").is_some();
36    ENABLED.store(on, Ordering::Relaxed);
37}
38
39pub fn enabled() -> bool {
40    ENABLED.load(Ordering::Relaxed)
41}
42
43/// Start timing a phase. The returned span records it when dropped; with
44/// profiling off it is inert.
45pub fn span(name: &'static str) -> Span {
46    Span {
47        name,
48        start: enabled().then(Instant::now),
49        note: None,
50    }
51}
52
53/// Record a phase whose duration was measured elsewhere — for the timings the
54/// search path already takes for its own trace lines.
55pub fn record(name: &'static str, elapsed: Duration, note: impl FnOnce() -> String) {
56    if !enabled() {
57        return;
58    }
59    if let Ok(mut phases) = PHASES.lock() {
60        phases.push(Phase {
61            name,
62            elapsed,
63            note: Some(note()),
64        });
65    }
66}
67
68pub struct Span {
69    name: &'static str,
70    start: Option<Instant>,
71    note: Option<String>,
72}
73
74impl Span {
75    /// Attach detail to this phase. The closure runs only when profiling is on,
76    /// so formatting a count is never paid for in a normal run.
77    pub fn note(&mut self, f: impl FnOnce() -> String) {
78        if self.start.is_some() {
79            self.note = Some(f());
80        }
81    }
82}
83
84impl Drop for Span {
85    fn drop(&mut self) {
86        let Some(start) = self.start else { return };
87        if let Ok(mut phases) = PHASES.lock() {
88            phases.push(Phase {
89                name: self.name,
90                elapsed: start.elapsed(),
91                note: self.note.take(),
92            });
93        }
94    }
95}
96
97/// Every phase recorded so far, in the order they finished. Drains.
98pub fn phases() -> Vec<Phase> {
99    PHASES
100        .lock()
101        .map(|mut p| std::mem::take(&mut *p))
102        .unwrap_or_default()
103}
104
105/// The report as stderr-ready lines. Empty when nothing was measured.
106pub fn report(total: Duration) -> Vec<String> {
107    let phases = phases();
108    if phases.is_empty() {
109        return Vec::new();
110    }
111    let w = phases
112        .iter()
113        .map(|p| p.name.len())
114        .max()
115        .unwrap_or(5)
116        .max(5);
117    let mut out: Vec<String> = phases
118        .iter()
119        .map(|p| {
120            let note = p.note.as_deref().unwrap_or_default();
121            format!("  {:<w$}  {:>8}  {note}", p.name, ms(p.elapsed), w = w)
122                .trim_end()
123                .to_string()
124        })
125        .collect();
126    out.push(format!("  {:<w$}  {:>8}", "─".repeat(w.min(20)), "", w = w));
127    out.push(format!("  {:<w$}  {:>8}", "total", ms(total), w = w));
128    out
129}
130
131/// Phases as JSON, for storing a baseline and diffing runs.
132pub fn json(total: Duration) -> String {
133    let phases = phases();
134    let body: Vec<String> = phases
135        .iter()
136        .map(|p| {
137            let note = match &p.note {
138                Some(n) => format!("\"{}\"", n.replace('"', "'")),
139                None => "null".to_string(),
140            };
141            format!(
142                "{{\"name\":\"{}\",\"ms\":{:.3},\"note\":{note}}}",
143                p.name,
144                p.elapsed.as_secs_f64() * 1000.0
145            )
146        })
147        .collect();
148    format!(
149        "{{\"total_ms\":{:.3},\"phases\":[{}]}}",
150        total.as_secs_f64() * 1000.0,
151        body.join(",")
152    )
153}
154
155fn ms(d: Duration) -> String {
156    format!("{:.1}ms", d.as_secs_f64() * 1000.0)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn a_span_is_inert_when_profiling_is_off() {
165        let mut s = span("off");
166        s.note(|| panic!("the note closure must not run when disabled"));
167        drop(s);
168        record("also off", Duration::from_millis(1), || {
169            panic!("nor this one")
170        });
171        assert!(phases().is_empty());
172    }
173
174    #[test]
175    fn an_enabled_span_records_its_name_and_note() {
176        enable_from(true);
177        {
178            let mut s = span("on");
179            s.note(|| "9 candidates".to_string());
180        }
181        let recorded = phases();
182        assert_eq!(recorded.len(), 1);
183        assert_eq!(recorded[0].name, "on");
184        assert_eq!(recorded[0].note.as_deref(), Some("9 candidates"));
185        assert!(phases().is_empty(), "phases() drains");
186        ENABLED.store(false, Ordering::Relaxed);
187    }
188}