use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct Profiler {
measurements: Arc<Mutex<HashMap<String, Vec<Duration>>>>,
}
impl Profiler {
pub fn new() -> Self {
Self {
measurements: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn start(&self, operation: impl Into<String>) -> ProfilerGuard {
ProfilerGuard {
operation: operation.into(),
start: Instant::now(),
profiler: self.clone(),
}
}
fn record(&self, operation: String, duration: Duration) {
let mut measurements = self.measurements.lock().unwrap_or_else(|e| e.into_inner());
measurements.entry(operation).or_default().push(duration);
}
pub fn stats(&self, operation: &str) -> Option<OperationStats> {
let measurements = self.measurements.lock().unwrap_or_else(|e| e.into_inner());
let durations = measurements.get(operation)?;
if durations.is_empty() {
return None;
}
let total: Duration = durations.iter().sum();
let count = durations.len();
let avg = total / count as u32;
let mut sorted = durations.clone();
sorted.sort();
let min = sorted.first().copied()?;
let max = sorted.last().copied()?;
let median = sorted[count / 2];
let p95 = sorted[(count as f64 * 0.95) as usize];
let p99 = sorted[(count as f64 * 0.99) as usize];
Some(OperationStats {
count,
total,
avg,
min,
max,
median,
p95,
p99,
})
}
pub fn operations(&self) -> Vec<String> {
let measurements = self.measurements.lock().unwrap_or_else(|e| e.into_inner());
measurements.keys().cloned().collect()
}
pub fn report(&self) -> String {
let mut output = String::from("Performance Report\n");
output.push_str("==================\n\n");
let operations = self.operations();
if operations.is_empty() {
output.push_str("No operations recorded.\n");
return output;
}
for op in operations {
if let Some(stats) = self.stats(&op) {
output.push_str(&format!("{}\n", op));
output.push_str(&format!(" Count: {}\n", stats.count));
output.push_str(&format!(" Total: {:?}\n", stats.total));
output.push_str(&format!(" Avg: {:?}\n", stats.avg));
output.push_str(&format!(" Min: {:?}\n", stats.min));
output.push_str(&format!(" Max: {:?}\n", stats.max));
output.push_str(&format!(" Median: {:?}\n", stats.median));
output.push_str(&format!(" P95: {:?}\n", stats.p95));
output.push_str(&format!(" P99: {:?}\n", stats.p99));
output.push('\n');
}
}
output
}
pub fn clear(&self) {
let mut measurements = self.measurements.lock().unwrap_or_else(|e| e.into_inner());
measurements.clear();
}
}
impl Default for Profiler {
fn default() -> Self {
Self::new()
}
}
pub struct ProfilerGuard {
operation: String,
start: Instant,
profiler: Profiler,
}
impl Drop for ProfilerGuard {
fn drop(&mut self) {
let duration = self.start.elapsed();
self.profiler.record(self.operation.clone(), duration);
}
}
#[derive(Debug, Clone)]
pub struct OperationStats {
pub count: usize,
pub total: Duration,
pub avg: Duration,
pub min: Duration,
pub max: Duration,
pub median: Duration,
pub p95: Duration,
pub p99: Duration,
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_profiler_basic() {
let profiler = Profiler::new();
{
let _guard = profiler.start("test_op");
thread::sleep(Duration::from_millis(10));
}
{
let _guard = profiler.start("test_op");
thread::sleep(Duration::from_millis(20));
}
let stats = profiler
.stats("test_op")
.expect("test: stats for test_op should exist");
assert_eq!(stats.count, 2);
assert!(stats.total >= Duration::from_millis(30));
assert!(stats.avg >= Duration::from_millis(15));
}
#[test]
fn test_profiler_multiple_operations() {
let profiler = Profiler::new();
{
let _guard = profiler.start("op1");
thread::sleep(Duration::from_millis(5));
}
{
let _guard = profiler.start("op2");
thread::sleep(Duration::from_millis(10));
}
let ops = profiler.operations();
assert_eq!(ops.len(), 2);
assert!(ops.contains(&"op1".to_string()));
assert!(ops.contains(&"op2".to_string()));
}
#[test]
fn test_profiler_report() {
let profiler = Profiler::new();
{
let _guard = profiler.start("test");
thread::sleep(Duration::from_millis(5));
}
let report = profiler.report();
assert!(report.contains("Performance Report"));
assert!(report.contains("test"));
assert!(report.contains("Count:"));
}
#[test]
fn test_profiler_clear() {
let profiler = Profiler::new();
{
let _guard = profiler.start("test");
thread::sleep(Duration::from_millis(5));
}
assert!(profiler.stats("test").is_some());
profiler.clear();
assert!(profiler.stats("test").is_none());
}
}