use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct Profiler {
name: String,
timings: HashMap<String, Vec<Duration>>,
}
#[derive(Debug, Clone)]
pub struct ProfileStats {
pub operation: String,
pub count: usize,
pub min: Duration,
pub max: Duration,
pub mean: Duration,
pub median: Duration,
pub p95: Duration,
pub p99: Duration,
pub total: Duration,
}
impl ProfileStats {
pub fn ops_per_sec(&self) -> f64 {
if self.mean.as_nanos() == 0 {
return 0.0;
}
1_000_000_000.0 / self.mean.as_nanos() as f64
}
pub fn summary_line(&self) -> String {
format!(
"{:<30} {:>8} samples | mean {:>12.2?} | p95 {:>12.2?} | p99 {:>12.2?} | {:.0} ops/s",
self.operation,
self.count,
self.mean,
self.p95,
self.p99,
self.ops_per_sec()
)
}
}
impl Profiler {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
timings: HashMap::new(),
}
}
pub fn record(&mut self, operation: &str, duration: Duration) {
self.timings
.entry(operation.to_string())
.or_default()
.push(duration);
}
pub fn time<F, R>(&mut self, operation: &str, f: F) -> R
where
F: FnOnce() -> R,
{
let start = Instant::now();
let result = f();
self.record(operation, start.elapsed());
result
}
pub fn time_n<F>(&mut self, operation: &str, iterations: usize, mut f: F)
where
F: FnMut(),
{
for _ in 0..iterations {
let start = Instant::now();
f();
self.record(operation, start.elapsed());
}
}
pub fn stats(&self, operation: &str) -> Option<ProfileStats> {
let timings = self.timings.get(operation)?;
if timings.is_empty() {
return None;
}
let mut sorted: Vec<Duration> = timings.clone();
sorted.sort();
let count = sorted.len();
let total: Duration = sorted.iter().sum();
let mean = total / count as u32;
let percentile = |p: f64| -> Duration {
let idx = ((count as f64 * p) as usize).min(count - 1);
sorted[idx]
};
Some(ProfileStats {
operation: operation.to_string(),
count,
min: sorted[0],
max: sorted[count - 1],
mean,
median: percentile(0.5),
p95: percentile(0.95),
p99: percentile(0.99),
total,
})
}
pub fn all_stats(&self) -> Vec<ProfileStats> {
let mut stats: Vec<ProfileStats> = self
.timings
.keys()
.filter_map(|op| self.stats(op))
.collect();
stats.sort_by_key(|s| s.operation.clone());
stats
}
pub fn report(&self) -> String {
let mut lines = vec![format!("╔══ {} Profiler Report ══╗", self.name)];
for stat in self.all_stats() {
lines.push(stat.summary_line());
}
lines.push(format!("╚══ {} operations profiled ══╝", self.timings.len()));
lines.join("\n")
}
pub fn reset(&mut self) {
self.timings.clear();
}
pub fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profiler_basic() {
let mut profiler = Profiler::new("test");
profiler.record("op1", Duration::from_millis(10));
profiler.record("op1", Duration::from_millis(20));
profiler.record("op1", Duration::from_millis(30));
let stats = profiler.stats("op1").unwrap();
assert_eq!(stats.count, 3);
assert_eq!(stats.min, Duration::from_millis(10));
assert_eq!(stats.max, Duration::from_millis(30));
}
#[test]
fn test_profiler_time_closure() {
let mut profiler = Profiler::new("test");
let result = profiler.time("add", || 2 + 2);
assert_eq!(result, 4);
let stats = profiler.stats("add").unwrap();
assert_eq!(stats.count, 1);
}
#[test]
fn test_profiler_time_n() {
let mut profiler = Profiler::new("test");
let mut counter = 0u64;
profiler.time_n("increment", 100, || {
counter += 1;
});
assert_eq!(counter, 100);
let stats = profiler.stats("increment").unwrap();
assert_eq!(stats.count, 100);
}
#[test]
fn test_profiler_report() {
let mut profiler = Profiler::new("demo");
profiler.record("fast_op", Duration::from_nanos(500));
profiler.record("slow_op", Duration::from_millis(5));
let report = profiler.report();
assert!(report.contains("demo"));
assert!(report.contains("fast_op"));
assert!(report.contains("slow_op"));
}
#[test]
fn test_profiler_reset() {
let mut profiler = Profiler::new("test");
profiler.record("op", Duration::from_millis(1));
assert!(profiler.stats("op").is_some());
profiler.reset();
assert!(profiler.stats("op").is_none());
}
#[test]
fn test_ops_per_sec() {
let stats = ProfileStats {
operation: "test".into(),
count: 100,
min: Duration::from_micros(100),
max: Duration::from_micros(200),
mean: Duration::from_millis(1), median: Duration::from_millis(1),
p95: Duration::from_millis(2),
p99: Duration::from_millis(3),
total: Duration::from_millis(100),
};
let ops = stats.ops_per_sec();
assert!((ops - 1000.0).abs() < 1.0);
}
}