use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub struct Timer {
start_time: Option<Instant>,
elapsed: Duration,
}
impl Timer {
#[inline]
#[must_use]
pub fn start() -> Self {
Self { start_time: Some(Instant::now()), elapsed: Duration::ZERO }
}
#[inline]
#[must_use]
pub fn new() -> Self {
Self { start_time: None, elapsed: Duration::ZERO }
}
#[inline]
pub fn start_now(&mut self) {
self.start_time = Some(Instant::now());
self.elapsed = Duration::ZERO;
}
#[inline]
pub fn stop(&mut self) -> Duration {
if let Some(start) = self.start_time.take() {
self.elapsed = start.elapsed();
}
self.elapsed
}
#[inline]
#[must_use]
pub fn elapsed(&self) -> Duration {
if let Some(start) = self.start_time { start.elapsed() } else { self.elapsed }
}
#[inline]
#[must_use]
pub fn is_running(&self) -> bool {
self.start_time.is_some()
}
}
impl Default for Timer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TimingStatistics {
pub count: usize,
pub min: Duration,
pub max: Duration,
pub average: Duration,
pub median: Duration,
pub percentile_90: Duration,
pub percentile_95: Duration,
pub percentile_99: Duration,
pub std_dev: Duration,
}
impl TimingStatistics {
#[must_use]
pub fn empty() -> Self {
Self {
count: 0,
min: Duration::ZERO,
max: Duration::ZERO,
average: Duration::ZERO,
median: Duration::ZERO,
percentile_90: Duration::ZERO,
percentile_95: Duration::ZERO,
percentile_99: Duration::ZERO,
std_dev: Duration::ZERO,
}
}
}
impl Default for TimingStatistics {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug, Clone)]
pub struct Histogram {
samples: Vec<u128>, }
impl Histogram {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self { samples: Vec::with_capacity(capacity) }
}
#[must_use]
pub fn new_default() -> Self {
Self::new(100)
}
pub fn record(&mut self, duration: Duration) {
self.samples.push(duration.as_nanos());
}
pub fn record_batch(&mut self, durations: &[Duration]) {
self.samples.reserve(durations.len());
for &duration in durations {
self.samples.push(duration.as_nanos());
}
}
#[must_use]
pub fn count(&self) -> usize {
self.samples.len()
}
pub fn clear(&mut self) {
self.samples.clear();
}
#[allow(clippy::arithmetic_side_effects)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_lossless)]
#[must_use]
pub fn calculate_statistics(&self) -> TimingStatistics {
if self.samples.is_empty() {
return TimingStatistics::empty();
}
let mut sorted = self.samples.clone();
sorted.sort_unstable();
let count = sorted.len();
let min = Duration::from_nanos(
sorted.first().copied().and_then(|x| u64::try_from(x).ok()).unwrap_or(0),
);
let max = Duration::from_nanos(
sorted
.get(count.saturating_sub(1))
.copied()
.and_then(|x| u64::try_from(x).ok())
.unwrap_or(0),
);
let sum: u128 = sorted.iter().sum();
let average = if count > 0 {
Duration::from_nanos(
u64::try_from(sum / u128::try_from(count).unwrap_or(1)).unwrap_or(0),
)
} else {
Duration::ZERO
};
let median = if count.is_multiple_of(2) {
let half_count = count / 2;
let mid_left = sorted.get(half_count.saturating_sub(1)).copied().unwrap_or(0);
let mid_right = sorted.get(half_count).copied().unwrap_or(0);
Duration::from_nanos(u64::try_from((mid_left + mid_right) / 2).unwrap_or(0))
} else {
let half_count = count / 2;
Duration::from_nanos(
u64::try_from(sorted.get(half_count).copied().unwrap_or(0)).unwrap_or(0),
)
};
let percentile_90 = Self::percentile(&sorted, 90.0);
let percentile_95 = Self::percentile(&sorted, 95.0);
let percentile_99 = Self::percentile(&sorted, 99.0);
let mean = average.as_nanos() as f64;
let variance = if count > 0 {
sorted
.iter()
.map(|&x| {
let x_f64 = x as f64;
let diff = x_f64 - mean;
diff * diff
})
.sum::<f64>()
/ count as f64
} else {
0.0
};
let std_dev = Duration::from_nanos(variance.sqrt().to_bits());
TimingStatistics {
count,
min,
max,
average,
median,
percentile_90,
percentile_95,
percentile_99,
std_dev,
}
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_sign_loss)]
fn percentile(sorted: &[u128], percentile: f64) -> Duration {
if sorted.is_empty() {
return Duration::ZERO;
}
let len = sorted.len();
let float_index = (percentile / 100.0) * (len.saturating_sub(1) as f64);
let index = float_index as usize;
let safe_index = index.min(len.saturating_sub(1));
Duration::from_nanos(
sorted.get(safe_index).copied().and_then(|x| u64::try_from(x).ok()).unwrap_or(0),
)
}
pub fn merge(&mut self, other: &Histogram) {
self.samples.extend_from_slice(&other.samples);
}
}
pub struct MetricsCollector {
histograms: Arc<Mutex<HashMap<String, Histogram>>>,
operation_counts: Arc<Mutex<HashMap<String, usize>>>,
}
impl MetricsCollector {
#[must_use]
pub fn new() -> Self {
Self {
histograms: Arc::new(Mutex::new(HashMap::new())),
operation_counts: Arc::new(Mutex::new(HashMap::new())),
}
}
#[allow(clippy::arithmetic_side_effects)] pub fn record_operation(&self, name: &str, duration: Duration) {
if let Ok(mut histograms) = self.histograms.lock() {
histograms
.entry(name.to_string())
.or_insert_with(Histogram::new_default)
.record(duration);
}
if let Ok(mut counts) = self.operation_counts.lock() {
let current_count = counts.entry(name.to_string()).or_insert(0);
*current_count = current_count.saturating_add(1);
}
}
pub fn get_statistics(&self, name: &str) -> TimingStatistics {
if let Ok(histograms) = self.histograms.lock() {
histograms
.get(name)
.map(Histogram::calculate_statistics)
.unwrap_or_else(TimingStatistics::empty)
} else {
TimingStatistics::empty()
}
}
#[must_use]
pub fn get_count(&self, name: &str) -> usize {
if let Ok(counts) = self.operation_counts.lock() {
*counts.get(name).unwrap_or(&0)
} else {
0
}
}
#[must_use]
pub fn operation_names(&self) -> Vec<String> {
if let Ok(histograms) = self.histograms.lock() {
histograms.keys().cloned().collect()
} else {
Vec::new()
}
}
#[must_use]
pub fn get_all_statistics(&self) -> HashMap<String, TimingStatistics> {
if let Ok(histograms) = self.histograms.lock() {
histograms.iter().map(|(name, h)| (name.clone(), h.calculate_statistics())).collect()
} else {
HashMap::new()
}
}
pub fn clear(&self) {
if let Ok(mut histograms) = self.histograms.lock() {
histograms.clear();
}
if let Ok(mut counts) = self.operation_counts.lock() {
counts.clear();
}
}
#[must_use]
pub fn clone_collector(&self) -> Self {
Self {
histograms: Arc::clone(&self.histograms),
operation_counts: Arc::clone(&self.operation_counts),
}
}
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
impl Clone for MetricsCollector {
fn clone(&self) -> Self {
self.clone_collector()
}
}
pub struct ScopedTimer<'a> {
timer: Timer,
collector: Option<&'a MetricsCollector>,
operation_name: Option<&'a str>,
}
impl<'a> ScopedTimer<'a> {
#[must_use]
pub fn new(collector: &'a MetricsCollector, operation_name: &'a str) -> Self {
Self {
timer: Timer::start(),
collector: Some(collector),
operation_name: Some(operation_name),
}
}
#[must_use]
pub fn timing_only() -> Self {
Self { timer: Timer::start(), collector: None, operation_name: None }
}
#[must_use]
pub fn stop(mut self) -> Duration {
let duration = self.timer.stop();
if let (Some(collector), Some(name)) = (self.collector, self.operation_name) {
collector.record_operation(name, duration);
}
duration
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.timer.elapsed()
}
}
impl<'a> Drop for ScopedTimer<'a> {
fn drop(&mut self) {
if let (Some(collector), Some(name)) = (self.collector, self.operation_name)
&& self.timer.is_running()
{
let duration = self.timer.stop();
collector.record_operation(name, duration);
}
}
}
pub fn benchmark<F>(iterations: usize, operation: F) -> TimingStatistics
where
F: Fn(),
{
let mut histogram = Histogram::new(iterations);
for _ in 0..10.min(iterations / 10) {
operation();
}
for _ in 0..iterations {
let mut timer = Timer::start();
operation();
histogram.record(timer.stop());
}
histogram.calculate_statistics()
}
pub fn time_operation<F>(operation: F) -> Duration
where
F: FnOnce(),
{
let mut timer = Timer::start();
operation();
timer.stop()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timer_basic_is_correct() {
let mut timer = Timer::new();
assert!(!timer.is_running());
timer.start_now();
assert!(timer.is_running());
std::thread::sleep(Duration::from_millis(10));
let elapsed = timer.stop();
assert!(!timer.is_running());
assert!(elapsed >= Duration::from_millis(10));
}
#[test]
fn test_timer_elapsed_while_running_is_correct() {
let timer = Timer::start();
std::thread::sleep(Duration::from_millis(5));
let elapsed = timer.elapsed();
assert!(elapsed >= Duration::from_millis(5));
assert!(timer.is_running());
}
#[test]
fn test_histogram_basic_is_correct() {
let mut histogram = Histogram::new(10);
histogram.record(Duration::from_millis(10));
histogram.record(Duration::from_millis(20));
histogram.record(Duration::from_millis(30));
assert_eq!(histogram.count(), 3);
let stats = histogram.calculate_statistics();
assert_eq!(stats.count, 3);
assert_eq!(stats.min, Duration::from_millis(10));
assert_eq!(stats.max, Duration::from_millis(30));
assert_eq!(stats.median, Duration::from_millis(20));
}
#[test]
fn test_histogram_empty_has_zero_count_succeeds() {
let histogram = Histogram::new(10);
let stats = histogram.calculate_statistics();
assert_eq!(stats.count, 0);
assert_eq!(stats.min, Duration::ZERO);
}
#[test]
fn test_histogram_merge_is_correct() {
let mut hist1 = Histogram::new(5);
hist1.record(Duration::from_millis(10));
hist1.record(Duration::from_millis(20));
let mut hist2 = Histogram::new(5);
hist2.record(Duration::from_millis(30));
hist2.record(Duration::from_millis(40));
hist1.merge(&hist2);
assert_eq!(hist1.count(), 4);
let stats = hist1.calculate_statistics();
assert_eq!(stats.min, Duration::from_millis(10));
assert_eq!(stats.max, Duration::from_millis(40));
}
#[test]
fn test_metrics_collector_is_correct() {
let collector = MetricsCollector::new();
collector.record_operation("test", Duration::from_millis(10));
collector.record_operation("test", Duration::from_millis(20));
collector.record_operation("other", Duration::from_millis(30));
assert_eq!(collector.get_count("test"), 2);
assert_eq!(collector.get_count("other"), 1);
let stats = collector.get_statistics("test");
assert_eq!(stats.count, 2);
assert_eq!(stats.min, Duration::from_millis(10));
assert_eq!(stats.max, Duration::from_millis(20));
}
#[test]
fn test_metrics_collector_clone_is_correct() {
let collector1 = MetricsCollector::new();
let collector2 = collector1.clone();
collector1.record_operation("test", Duration::from_millis(10));
assert_eq!(collector2.get_count("test"), 1);
}
#[test]
fn test_benchmark_is_correct() {
let stats = benchmark(100, || {
let _ = 1 + 1;
});
assert_eq!(stats.count, 100);
assert!(stats.average > Duration::ZERO);
assert!(stats.min <= stats.average);
assert!(stats.max >= stats.average);
}
#[test]
fn test_time_operation_returns_elapsed_duration_succeeds() {
let duration = time_operation(|| {
std::thread::sleep(Duration::from_millis(5));
});
assert!(duration >= Duration::from_millis(5));
}
#[test]
fn test_scoped_timer_basic_is_correct() {
let collector = MetricsCollector::new();
{
let _timer = ScopedTimer::new(&collector, "test_operation");
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(collector.get_count("test_operation"), 1);
let stats = collector.get_statistics("test_operation");
assert!(stats.average >= Duration::from_millis(5));
}
#[test]
fn test_scoped_timer_timing_only_returns_elapsed_duration_succeeds() {
let timer = ScopedTimer::timing_only();
std::thread::sleep(Duration::from_millis(5));
let elapsed = timer.elapsed();
assert!(elapsed >= Duration::from_millis(5));
}
#[test]
fn test_percentile_calculation_is_correct() {
let mut histogram = Histogram::new(100);
for i in 0..100 {
histogram.record(Duration::from_nanos(i));
}
let stats = histogram.calculate_statistics();
assert!(stats.median.as_nanos() >= 45 && stats.median.as_nanos() <= 55);
assert!(stats.percentile_90.as_nanos() >= 85 && stats.percentile_90.as_nanos() <= 95);
assert!(stats.percentile_99.as_nanos() >= 95 && stats.percentile_99.as_nanos() <= 99);
}
#[test]
fn test_timing_statistics_default_has_zero_values_succeeds() {
let stats = TimingStatistics::default();
assert_eq!(stats.count, 0);
assert_eq!(stats.min, Duration::ZERO);
assert_eq!(stats.max, Duration::ZERO);
}
#[test]
fn test_histogram_clear_succeeds() {
let mut histogram = Histogram::new(10);
histogram.record(Duration::from_millis(10));
histogram.record(Duration::from_millis(20));
assert_eq!(histogram.count(), 2);
histogram.clear();
assert_eq!(histogram.count(), 0);
}
#[test]
fn test_metrics_collector_clear_succeeds() {
let collector = MetricsCollector::new();
collector.record_operation("test", Duration::from_millis(10));
collector.record_operation("other", Duration::from_millis(20));
assert_eq!(collector.get_count("test"), 1);
assert_eq!(collector.get_count("other"), 1);
collector.clear();
assert_eq!(collector.get_count("test"), 0);
assert_eq!(collector.get_count("other"), 0);
assert!(collector.operation_names().is_empty());
}
}