use core::time::Duration;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
#[ derive( Debug, Clone ) ]
pub struct MetricsConfig
{
pub max_samples : usize,
pub window_duration : Duration,
}
impl Default for MetricsConfig
{
#[ inline ]
fn default() -> Self
{
Self
{
max_samples : 10000,
window_duration : Duration::from_secs( 60 ),
}
}
}
#[ derive( Debug, Clone ) ]
struct RequestMeasurement
{
timestamp : Instant,
latency : Duration,
success : bool,
bytes_transferred : u64,
}
#[ derive( Debug, Clone, Copy, Default ) ]
pub struct LatencyStats
{
pub min : Duration,
pub max : Duration,
pub mean : Duration,
pub p50 : Duration,
pub p95 : Duration,
pub p99 : Duration,
}
#[ derive( Debug, Clone ) ]
pub struct MetricsSnapshot
{
pub total_requests : u64,
pub successful_requests : u64,
pub failed_requests : u64,
pub total_bytes : u64,
pub latency : LatencyStats,
pub snapshot_time : Instant,
pub duration : Duration,
}
impl MetricsSnapshot
{
#[ inline ]
#[ must_use ]
pub fn error_rate( &self ) -> f64
{
if self.total_requests == 0
{
0.0
}
else
{
self.failed_requests as f64 / self.total_requests as f64
}
}
#[ inline ]
#[ must_use ]
pub fn success_rate( &self ) -> f64
{
if self.total_requests == 0
{
0.0
}
else
{
self.successful_requests as f64 / self.total_requests as f64
}
}
#[ inline ]
#[ must_use ]
pub fn requests_per_second( &self ) -> f64
{
let duration_secs = self.duration.as_secs_f64( );
if duration_secs == 0.0
{
0.0
}
else
{
self.total_requests as f64 / duration_secs
}
}
#[ inline ]
#[ must_use ]
pub fn bytes_per_second( &self ) -> f64
{
let duration_secs = self.duration.as_secs_f64( );
if duration_secs == 0.0
{
0.0
}
else
{
self.total_bytes as f64 / duration_secs
}
}
#[ inline ]
#[ must_use ]
pub fn mean_latency( &self ) -> Duration
{
self.latency.mean
}
#[ inline ]
#[ must_use ]
pub fn median_latency( &self ) -> Duration
{
self.latency.p50
}
#[ inline ]
#[ must_use ]
pub fn p95_latency( &self ) -> Duration
{
self.latency.p95
}
#[ inline ]
#[ must_use ]
pub fn p99_latency( &self ) -> Duration
{
self.latency.p99
}
#[ inline ]
#[ must_use ]
pub fn min_latency( &self ) -> Duration
{
self.latency.min
}
#[ inline ]
#[ must_use ]
pub fn max_latency( &self ) -> Duration
{
self.latency.max
}
}
struct MetricsState
{
config : MetricsConfig,
measurements : Vec< RequestMeasurement >,
start_time : Instant,
}
impl MetricsState
{
fn new( config : MetricsConfig ) -> Self
{
Self
{
config,
measurements : Vec::new( ),
start_time : Instant::now( ),
}
}
fn cleanup_old_measurements( &mut self )
{
let now = Instant::now( );
let cutoff = now.checked_sub( self.config.window_duration )
.unwrap_or( Instant::now( ) );
self.measurements.retain( | m | m.timestamp >= cutoff );
if self.measurements.len( ) > self.config.max_samples
{
let excess = self.measurements.len( ) - self.config.max_samples;
self.measurements.drain( 0..excess );
}
}
fn calculate_latency_stats( &self ) -> LatencyStats
{
if self.measurements.is_empty( )
{
return LatencyStats::default( );
}
let mut latencies : Vec< Duration > = self.measurements
.iter( )
.map( | m | m.latency )
.collect( );
latencies.sort( );
let min = latencies.first( ).copied( ).unwrap_or_default( );
let max = latencies.last( ).copied( ).unwrap_or_default( );
let sum : Duration = latencies.iter( ).sum( );
let count = u32::try_from( latencies.len( ) ).unwrap_or( u32::MAX );
let mean = sum / count;
let p50 = Self::percentile( &latencies, 0.50 );
let p95 = Self::percentile( &latencies, 0.95 );
let p99 = Self::percentile( &latencies, 0.99 );
LatencyStats
{
min,
max,
mean,
p50,
p95,
p99,
}
}
fn percentile( sorted_latencies : &[ Duration ], percentile : f64 ) -> Duration
{
if sorted_latencies.is_empty( )
{
return Duration::default( );
}
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
let index = ( sorted_latencies.len( ) as f64 * percentile ) as usize;
let index = index.min( sorted_latencies.len( ) - 1 );
sorted_latencies[ index ]
}
fn snapshot( &self ) -> MetricsSnapshot
{
let now = Instant::now( );
let duration = now.duration_since( self.start_time );
let total_requests = self.measurements.len( ) as u64;
let successful_requests = self.measurements.iter( ).filter( | m | m.success ).count( ) as u64;
let failed_requests = total_requests - successful_requests;
let total_bytes = self.measurements.iter( ).map( | m | m.bytes_transferred ).sum( );
let latency = self.calculate_latency_stats( );
MetricsSnapshot
{
total_requests,
successful_requests,
failed_requests,
total_bytes,
latency,
snapshot_time : now,
duration,
}
}
}
#[ derive( Clone ) ]
pub struct PerformanceMetrics
{
state : Arc< RwLock< MetricsState > >,
}
impl PerformanceMetrics
{
#[ inline ]
#[ must_use ]
pub fn new( config : MetricsConfig ) -> Self
{
Self
{
state : Arc::new( RwLock::new( MetricsState::new( config ) ) ),
}
}
#[ inline ]
pub async fn record_request( &self, latency : Duration, success : bool, bytes_transferred : u64 )
{
let mut state = self.state.write( ).await;
state.measurements.push( RequestMeasurement
{
timestamp : Instant::now( ),
latency,
success,
bytes_transferred,
} );
if state.measurements.len( ) % 100 == 0
{
state.cleanup_old_measurements( );
}
}
#[ inline ]
pub async fn snapshot( &self ) -> MetricsSnapshot
{
let state = self.state.read( ).await;
state.snapshot( )
}
#[ inline ]
pub async fn snapshot_window( &self, window : Duration ) -> MetricsSnapshot
{
let state = self.state.read( ).await;
let now = Instant::now( );
let cutoff = now.checked_sub( window )
.unwrap_or( Instant::now( ) );
let filtered : Vec< RequestMeasurement > = state.measurements
.iter( )
.filter( | m | m.timestamp >= cutoff )
.cloned( )
.collect( );
let total_requests = filtered.len( ) as u64;
let successful_requests = filtered.iter( ).filter( | m | m.success ).count( ) as u64;
let failed_requests = total_requests - successful_requests;
let total_bytes = filtered.iter( ).map( | m | m.bytes_transferred ).sum( );
let mut latencies : Vec< Duration > = filtered.iter( ).map( | m | m.latency ).collect( );
latencies.sort( );
let latency = if latencies.is_empty( )
{
LatencyStats::default( )
}
else
{
let min = latencies.first( ).copied( ).unwrap_or_default( );
let max = latencies.last( ).copied( ).unwrap_or_default( );
let sum : Duration = latencies.iter( ).sum( );
let count = u32::try_from( latencies.len( ) ).unwrap_or( u32::MAX );
let mean = sum / count;
let p50 = MetricsState::percentile( &latencies, 0.50 );
let p95 = MetricsState::percentile( &latencies, 0.95 );
let p99 = MetricsState::percentile( &latencies, 0.99 );
LatencyStats { min, max, mean, p50, p95, p99 }
};
MetricsSnapshot
{
total_requests,
successful_requests,
failed_requests,
total_bytes,
latency,
snapshot_time : now,
duration : window,
}
}
#[ inline ]
pub async fn reset( &self )
{
let mut state = self.state.write( ).await;
state.measurements.clear( );
state.start_time = Instant::now( );
}
#[ inline ]
pub async fn cleanup( &self ) -> usize
{
let mut state = self.state.write( ).await;
let before = state.measurements.len( );
state.cleanup_old_measurements( );
let after = state.measurements.len( );
before - after
}
#[ inline ]
pub async fn measurement_count( &self ) -> usize
{
let state = self.state.read( ).await;
state.measurements.len( )
}
#[ inline ]
pub async fn config( &self ) -> MetricsConfig
{
let state = self.state.read( ).await;
state.config.clone( )
}
}
impl core::fmt::Debug for PerformanceMetrics
{
#[ inline ]
fn fmt( &self, f : &mut core::fmt::Formatter< '_ > ) -> core::fmt::Result
{
f.debug_struct( "PerformanceMetrics" )
.field( "state", &"< MetricsState >" )
.finish( )
}
}