use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
static ENABLED: AtomicBool = AtomicBool::new(false);
static PHASES: Mutex<Vec<Phase>> = Mutex::new(Vec::new());
pub struct Phase {
pub name: &'static str,
pub elapsed: Duration,
pub note: Option<String>,
}
pub fn enable() {
ENABLED.store(true, Ordering::Relaxed);
}
pub fn enabled() -> bool {
ENABLED.load(Ordering::Relaxed)
}
pub fn span(name: &'static str) -> Span {
Span {
name,
start: enabled().then(Instant::now),
note: None,
}
}
pub struct Span {
name: &'static str,
start: Option<Instant>,
note: Option<String>,
}
impl Span {
pub fn note(&mut self, f: impl FnOnce() -> String) {
if self.start.is_some() {
self.note = Some(f());
}
}
}
impl Drop for Span {
fn drop(&mut self) {
let Some(start) = self.start else { return };
if let Ok(mut phases) = PHASES.lock() {
phases.push(Phase {
name: self.name,
elapsed: start.elapsed(),
note: self.note.take(),
});
}
}
}
pub fn phases() -> Vec<Phase> {
PHASES
.lock()
.map(|mut p| std::mem::take(&mut *p))
.unwrap_or_default()
}
pub fn report(total: Duration) -> Vec<String> {
let phases = phases();
if phases.is_empty() {
return Vec::new();
}
let width = phases
.iter()
.map(|p| p.name.len())
.max()
.unwrap_or(0)
.max(5);
let mut out: Vec<String> = phases
.iter()
.map(|p| {
let note = p.note.as_deref().unwrap_or_default();
format!(
" {:<width$} {:>8} {note}",
p.name,
ms(p.elapsed),
width = width
)
.trim_end()
.to_string()
})
.collect();
out.push(format!(
" {:<width$} {:>8}",
"─".repeat(width.min(20)),
"",
width = width
));
out.push(format!(
" {:<width$} {:>8}",
"total",
ms(total),
width = width
));
out
}
pub fn json(total: Duration) -> serde_json::Value {
let phases = phases();
serde_json::json!({
"total_ms": total.as_secs_f64() * 1000.0,
"phases": phases
.iter()
.map(|p| serde_json::json!({
"name": p.name,
"ms": p.elapsed.as_secs_f64() * 1000.0,
"note": p.note,
}))
.collect::<Vec<_>>(),
})
}
fn ms(d: Duration) -> String {
format!("{:.1}ms", d.as_secs_f64() * 1000.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_span_is_inert_when_profiling_is_off() {
let mut s = span("off");
s.note(|| panic!("the note closure must not run when disabled"));
drop(s);
assert!(phases().is_empty());
}
#[test]
fn an_enabled_span_records_its_name_and_note() {
enable();
{
let mut s = span("on");
s.note(|| "42 records".to_string());
}
let recorded = phases();
assert_eq!(recorded.len(), 1);
assert_eq!(recorded[0].name, "on");
assert_eq!(recorded[0].note.as_deref(), Some("42 records"));
assert!(phases().is_empty());
ENABLED.store(false, Ordering::Relaxed);
}
}