use std::collections::{HashMap, VecDeque};
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: VecDeque<u128>,
}
impl Histogram {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self { samples: VecDeque::with_capacity(capacity) }
}
#[must_use]
pub fn new_default() -> Self {
Self::new(100)
}
const MAX_SAMPLES_PER_HISTOGRAM: usize = 65_536;
pub fn record(&mut self, duration: Duration) {
if self.samples.len() >= Self::MAX_SAMPLES_PER_HISTOGRAM {
self.samples.pop_front();
}
self.samples.push_back(duration.as_nanos());
}
pub fn record_batch(&mut self, durations: &[Duration]) {
let headroom = Self::MAX_SAMPLES_PER_HISTOGRAM.saturating_sub(self.samples.len());
self.samples.reserve(durations.len().min(headroom));
for &duration in durations {
self.record(duration);
}
}
#[must_use]
pub fn count(&self) -> usize {
self.samples.len()
}
pub fn clear(&mut self) {
self.samples.clear();
}
#[expect(
clippy::arithmetic_side_effects,
reason = "arithmetic bounded by callsite invariants; overflow impossible at this site"
)]
#[expect(
clippy::cast_precision_loss,
reason = "precision loss is intentional in this measurement/heuristic path"
)]
#[must_use]
pub fn calculate_statistics(&self) -> TimingStatistics {
if self.samples.is_empty() {
return TimingStatistics::empty();
}
let mut sorted: Vec<u128> = self.samples.iter().copied().collect();
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
};
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "f64-to-u64 cast on variance.sqrt(): a Duration std-dev exceeding u64 nanoseconds (~584 years) cannot occur on real timing samples; sign-loss is sound because variance is non-negative"
)]
let std_dev = Duration::from_nanos(variance.sqrt() as u64);
TimingStatistics {
count,
min,
max,
average,
median,
percentile_90,
percentile_95,
percentile_99,
std_dev,
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "truncation guarded by callsite preconditions"
)]
#[expect(
clippy::cast_precision_loss,
reason = "precision loss is intentional in this measurement/heuristic path"
)]
#[expect(clippy::cast_sign_loss, reason = "sign loss is intentional in this conversion path")]
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(other.samples.iter().copied());
while self.samples.len() > Self::MAX_SAMPLES_PER_HISTOGRAM {
self.samples.pop_front();
}
}
}
#[derive(Default)]
struct MetricsState {
histograms: HashMap<String, Histogram>,
operation_counts: HashMap<String, usize>,
}
pub struct MetricsCollector {
state: Arc<Mutex<MetricsState>>,
}
impl MetricsCollector {
const MAX_DISTINCT_OPERATIONS: usize = 1024;
#[must_use]
pub fn new() -> Self {
Self { state: Arc::new(Mutex::new(MetricsState::default())) }
}
pub fn record_operation(&self, name: &str, duration: Duration) {
match self.state.lock() {
Ok(mut state) => {
let MetricsState { histograms, operation_counts } = &mut *state;
let histogram_accepted = if let Some(h) = histograms.get_mut(name) {
h.record(duration);
true
} else if histograms.len() < Self::MAX_DISTINCT_OPERATIONS {
let mut h = Histogram::new_default();
h.record(duration);
histograms.insert(name.to_string(), h);
true
} else {
tracing::warn!(
operation = name,
cap = Self::MAX_DISTINCT_OPERATIONS,
"perf::MetricsCollector: distinct-operation cap reached; metric dropped"
);
false
};
if histogram_accepted {
if let Some(c) = operation_counts.get_mut(name) {
*c = c.saturating_add(1);
} else {
operation_counts.insert(name.to_string(), 1);
}
}
}
Err(_) => {
tracing::warn!(
operation = name,
"perf::MetricsCollector: state mutex poisoned; metric dropped"
);
}
}
}
pub fn get_statistics(&self, name: &str) -> TimingStatistics {
if let Ok(state) = self.state.lock() {
state
.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(state) = self.state.lock() {
*state.operation_counts.get(name).unwrap_or(&0)
} else {
0
}
}
#[must_use]
pub fn operation_names(&self) -> Vec<String> {
if let Ok(state) = self.state.lock() {
state.histograms.keys().cloned().collect()
} else {
Vec::new()
}
}
#[must_use]
pub fn get_all_statistics(&self) -> HashMap<String, TimingStatistics> {
if let Ok(state) = self.state.lock() {
state
.histograms
.iter()
.map(|(name, h)| (name.clone(), h.calculate_statistics()))
.collect()
} else {
HashMap::new()
}
}
pub fn clear(&self) {
if let Ok(mut state) = self.state.lock() {
state.histograms.clear();
state.operation_counts.clear();
}
}
#[must_use]
pub fn clone_collector(&self) -> Self {
Self { state: Arc::clone(&self.state) }
}
}
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());
}
}