use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct MetricsConfig {
pub detailed_timing: bool,
pub track_memory: bool,
pub track_throughput: bool,
pub max_records: usize,
pub sampling_rate: f64,
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
detailed_timing: true,
track_memory: true,
track_throughput: true,
max_records: 10000,
sampling_rate: 1.0,
}
}
}
impl MetricsConfig {
pub fn lightweight() -> Self {
Self {
detailed_timing: false,
track_memory: false,
track_throughput: true,
max_records: 1000,
sampling_rate: 0.1,
}
}
pub fn debug() -> Self {
Self {
detailed_timing: true,
track_memory: true,
track_throughput: true,
max_records: 50000,
sampling_rate: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct OperationRecord {
pub operation: String,
pub duration: Duration,
pub timestamp: u64,
pub memory_allocated: usize,
pub elements_processed: usize,
}
#[derive(Debug, Clone, Default)]
pub struct OperationStats {
pub call_count: u64,
pub total_time: Duration,
pub min_time: Duration,
pub max_time: Duration,
pub total_elements: u64,
pub total_memory: u64,
}
impl OperationStats {
pub fn avg_time(&self) -> Duration {
if self.call_count == 0 {
Duration::ZERO
} else {
self.total_time / self.call_count as u32
}
}
pub fn ops_per_sec(&self) -> f64 {
let secs = self.total_time.as_secs_f64();
if secs == 0.0 {
0.0
} else {
self.call_count as f64 / secs
}
}
pub fn elements_per_sec(&self) -> f64 {
let secs = self.total_time.as_secs_f64();
if secs == 0.0 {
0.0
} else {
self.total_elements as f64 / secs
}
}
pub fn update(&mut self, duration: Duration, elements: usize, memory: usize) {
if self.call_count == 0 {
self.min_time = duration;
self.max_time = duration;
} else {
self.min_time = self.min_time.min(duration);
self.max_time = self.max_time.max(duration);
}
self.call_count += 1;
self.total_time += duration;
self.total_elements += elements as u64;
self.total_memory += memory as u64;
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryStats {
pub current_usage: usize,
pub peak_usage: usize,
pub total_allocations: u64,
pub total_deallocations: u64,
pub total_bytes_allocated: u64,
pub total_bytes_deallocated: u64,
}
impl MemoryStats {
pub fn record_allocation(&mut self, bytes: usize) {
self.current_usage = self.current_usage.saturating_add(bytes);
self.peak_usage = self.peak_usage.max(self.current_usage);
self.total_allocations += 1;
self.total_bytes_allocated += bytes as u64;
}
pub fn record_deallocation(&mut self, bytes: usize) {
self.current_usage = self.current_usage.saturating_sub(bytes);
self.total_deallocations += 1;
self.total_bytes_deallocated += bytes as u64;
}
pub fn efficiency(&self) -> f64 {
if self.total_bytes_allocated == 0 {
1.0
} else {
self.total_bytes_deallocated as f64 / self.total_bytes_allocated as f64
}
}
pub fn format_current(&self) -> String {
format_bytes(self.current_usage)
}
pub fn format_peak(&self) -> String {
format_bytes(self.peak_usage)
}
}
#[derive(Debug, Clone, Default)]
pub struct ThroughputStats {
pub total_operations: u64,
pub total_time: Duration,
pub total_elements: u64,
pub start_time: Option<Instant>,
}
impl ThroughputStats {
pub fn ops_per_sec(&self) -> f64 {
let secs = self.total_time.as_secs_f64();
if secs == 0.0 {
0.0
} else {
self.total_operations as f64 / secs
}
}
pub fn elements_per_sec(&self) -> f64 {
let secs = self.total_time.as_secs_f64();
if secs == 0.0 {
0.0
} else {
self.total_elements as f64 / secs
}
}
pub fn wall_clock_ops_per_sec(&self) -> f64 {
match self.start_time {
Some(start) => {
let elapsed = start.elapsed().as_secs_f64();
if elapsed == 0.0 {
0.0
} else {
self.total_operations as f64 / elapsed
}
}
None => 0.0,
}
}
}
#[derive(Debug)]
pub struct MetricsCollector {
config: MetricsConfig,
operation_stats: HashMap<String, OperationStats>,
memory_stats: MemoryStats,
throughput_stats: ThroughputStats,
records: Vec<OperationRecord>,
start_time: Instant,
}
impl MetricsCollector {
pub fn new(config: MetricsConfig) -> Self {
Self {
config,
operation_stats: HashMap::new(),
memory_stats: MemoryStats::default(),
throughput_stats: ThroughputStats {
start_time: Some(Instant::now()),
..Default::default()
},
records: Vec::new(),
start_time: Instant::now(),
}
}
pub fn time_operation<F, T>(&mut self, operation: &str, f: F) -> T
where
F: FnOnce() -> T,
{
self.time_operation_with_elements(operation, 0, f)
}
pub fn time_operation_with_elements<F, T>(
&mut self,
operation: &str,
elements: usize,
f: F,
) -> T
where
F: FnOnce() -> T,
{
let start = Instant::now();
let result = f();
let duration = start.elapsed();
self.record_operation(operation, duration, elements, 0);
result
}
pub fn record_operation(
&mut self,
operation: &str,
duration: Duration,
elements: usize,
memory: usize,
) {
let stats = self
.operation_stats
.entry(operation.to_string())
.or_default();
stats.update(duration, elements, memory);
self.throughput_stats.total_operations += 1;
self.throughput_stats.total_time += duration;
self.throughput_stats.total_elements += elements as u64;
if self.config.detailed_timing && self.records.len() < self.config.max_records {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
self.records.push(OperationRecord {
operation: operation.to_string(),
duration,
timestamp,
memory_allocated: memory,
elements_processed: elements,
});
}
}
pub fn record_allocation(&mut self, bytes: usize) {
if self.config.track_memory {
self.memory_stats.record_allocation(bytes);
}
}
pub fn record_deallocation(&mut self, bytes: usize) {
if self.config.track_memory {
self.memory_stats.record_deallocation(bytes);
}
}
pub fn operation_stats(&self) -> &HashMap<String, OperationStats> {
&self.operation_stats
}
pub fn memory_stats(&self) -> &MemoryStats {
&self.memory_stats
}
pub fn throughput_stats(&self) -> &ThroughputStats {
&self.throughput_stats
}
pub fn records(&self) -> &[OperationRecord] {
&self.records
}
pub fn summary(&self) -> MetricsSummary {
let mut slowest_ops: Vec<_> = self.operation_stats.iter().collect();
slowest_ops.sort_by_key(|entry| std::cmp::Reverse(entry.1.avg_time()));
let mut most_called: Vec<_> = self.operation_stats.iter().collect();
most_called.sort_by_key(|entry| std::cmp::Reverse(entry.1.call_count));
MetricsSummary {
total_operations: self.throughput_stats.total_operations,
total_time: self.throughput_stats.total_time,
ops_per_sec: self.throughput_stats.ops_per_sec(),
elements_per_sec: self.throughput_stats.elements_per_sec(),
peak_memory: self.memory_stats.peak_usage,
current_memory: self.memory_stats.current_usage,
memory_efficiency: self.memory_stats.efficiency(),
unique_operations: self.operation_stats.len(),
slowest_operation: slowest_ops
.first()
.map(|(k, v)| ((*k).clone(), v.avg_time())),
most_called_operation: most_called
.first()
.map(|(k, v)| ((*k).clone(), v.call_count)),
collection_duration: self.start_time.elapsed(),
}
}
pub fn export_json(&self) -> Result<String, serde_json::Error> {
let export = MetricsExport {
summary: self.summary(),
operation_stats: self
.operation_stats
.iter()
.map(|(k, v)| (k.clone(), OperationStatsExport::from(v)))
.collect(),
memory_stats: MemoryStatsExport::from(&self.memory_stats),
throughput_stats: ThroughputStatsExport::from(&self.throughput_stats),
};
serde_json::to_string_pretty(&export)
}
pub fn export_csv(&self) -> String {
let mut csv = String::from(
"operation,call_count,total_time_ms,avg_time_us,min_time_us,max_time_us,ops_per_sec,elements_per_sec\n",
);
for (name, stats) in &self.operation_stats {
csv.push_str(&format!(
"{},{},{:.3},{:.3},{:.3},{:.3},{:.2},{:.2}\n",
name,
stats.call_count,
stats.total_time.as_secs_f64() * 1000.0,
stats.avg_time().as_secs_f64() * 1_000_000.0,
stats.min_time.as_secs_f64() * 1_000_000.0,
stats.max_time.as_secs_f64() * 1_000_000.0,
stats.ops_per_sec(),
stats.elements_per_sec(),
));
}
csv
}
pub fn reset(&mut self) {
self.operation_stats.clear();
self.memory_stats = MemoryStats::default();
self.throughput_stats = ThroughputStats {
start_time: Some(Instant::now()),
..Default::default()
};
self.records.clear();
self.start_time = Instant::now();
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new(MetricsConfig::default())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MetricsSummary {
pub total_operations: u64,
#[serde(with = "duration_serde")]
pub total_time: Duration,
pub ops_per_sec: f64,
pub elements_per_sec: f64,
pub peak_memory: usize,
pub current_memory: usize,
pub memory_efficiency: f64,
pub unique_operations: usize,
pub slowest_operation: Option<(String, Duration)>,
pub most_called_operation: Option<(String, u64)>,
#[serde(with = "duration_serde")]
pub collection_duration: Duration,
}
impl std::fmt::Display for MetricsSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Metrics Summary ===")?;
writeln!(f, "Total Operations: {}", self.total_operations)?;
writeln!(f, "Total Time: {:.3}s", self.total_time.as_secs_f64())?;
writeln!(f, "Throughput: {:.2} ops/sec", self.ops_per_sec)?;
writeln!(f, "Elements: {:.2}/sec", self.elements_per_sec)?;
writeln!(f, "Peak Memory: {}", format_bytes(self.peak_memory))?;
writeln!(f, "Current Memory: {}", format_bytes(self.current_memory))?;
writeln!(
f,
"Memory Efficiency: {:.1}%",
self.memory_efficiency * 100.0
)?;
writeln!(f, "Unique Operations: {}", self.unique_operations)?;
if let Some((name, time)) = &self.slowest_operation {
writeln!(
f,
"Slowest: {} ({:.3}ms)",
name,
time.as_secs_f64() * 1000.0
)?;
}
if let Some((name, count)) = &self.most_called_operation {
writeln!(f, "Most Called: {} ({} calls)", name, count)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct AtomicMetrics {
pub operations: AtomicU64,
pub time_nanos: AtomicU64,
pub peak_memory: AtomicUsize,
pub current_memory: AtomicUsize,
}
impl AtomicMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn record_operation(&self, duration: Duration) {
self.operations.fetch_add(1, Ordering::Relaxed);
self.time_nanos
.fetch_add(duration.as_nanos() as u64, Ordering::Relaxed);
}
pub fn record_allocation(&self, bytes: usize) {
let current = self.current_memory.fetch_add(bytes, Ordering::Relaxed) + bytes;
let mut peak = self.peak_memory.load(Ordering::Relaxed);
while current > peak {
match self.peak_memory.compare_exchange_weak(
peak,
current,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(p) => peak = p,
}
}
}
pub fn record_deallocation(&self, bytes: usize) {
self.current_memory.fetch_sub(bytes, Ordering::Relaxed);
}
pub fn snapshot(&self) -> (u64, Duration, usize) {
let ops = self.operations.load(Ordering::Relaxed);
let nanos = self.time_nanos.load(Ordering::Relaxed);
let peak = self.peak_memory.load(Ordering::Relaxed);
(ops, Duration::from_nanos(nanos), peak)
}
pub fn reset(&self) {
self.operations.store(0, Ordering::Relaxed);
self.time_nanos.store(0, Ordering::Relaxed);
self.peak_memory.store(0, Ordering::Relaxed);
self.current_memory.store(0, Ordering::Relaxed);
}
}
pub type SharedMetrics = Arc<AtomicMetrics>;
pub fn shared_metrics() -> SharedMetrics {
Arc::new(AtomicMetrics::new())
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct MetricsExport {
summary: MetricsSummary,
operation_stats: HashMap<String, OperationStatsExport>,
memory_stats: MemoryStatsExport,
throughput_stats: ThroughputStatsExport,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct OperationStatsExport {
call_count: u64,
total_time_ms: f64,
avg_time_us: f64,
min_time_us: f64,
max_time_us: f64,
ops_per_sec: f64,
elements_per_sec: f64,
total_elements: u64,
total_memory_bytes: u64,
}
impl From<&OperationStats> for OperationStatsExport {
fn from(stats: &OperationStats) -> Self {
Self {
call_count: stats.call_count,
total_time_ms: stats.total_time.as_secs_f64() * 1000.0,
avg_time_us: stats.avg_time().as_secs_f64() * 1_000_000.0,
min_time_us: stats.min_time.as_secs_f64() * 1_000_000.0,
max_time_us: stats.max_time.as_secs_f64() * 1_000_000.0,
ops_per_sec: stats.ops_per_sec(),
elements_per_sec: stats.elements_per_sec(),
total_elements: stats.total_elements,
total_memory_bytes: stats.total_memory,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct MemoryStatsExport {
current_usage_bytes: usize,
peak_usage_bytes: usize,
total_allocations: u64,
total_deallocations: u64,
total_bytes_allocated: u64,
total_bytes_deallocated: u64,
efficiency: f64,
}
impl From<&MemoryStats> for MemoryStatsExport {
fn from(stats: &MemoryStats) -> Self {
Self {
current_usage_bytes: stats.current_usage,
peak_usage_bytes: stats.peak_usage,
total_allocations: stats.total_allocations,
total_deallocations: stats.total_deallocations,
total_bytes_allocated: stats.total_bytes_allocated,
total_bytes_deallocated: stats.total_bytes_deallocated,
efficiency: stats.efficiency(),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ThroughputStatsExport {
total_operations: u64,
total_time_ms: f64,
total_elements: u64,
ops_per_sec: f64,
elements_per_sec: f64,
}
impl From<&ThroughputStats> for ThroughputStatsExport {
fn from(stats: &ThroughputStats) -> Self {
Self {
total_operations: stats.total_operations,
total_time_ms: stats.total_time.as_secs_f64() * 1000.0,
total_elements: stats.total_elements,
ops_per_sec: stats.ops_per_sec(),
elements_per_sec: stats.elements_per_sec(),
}
}
}
mod duration_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
duration.as_secs_f64().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs = f64::deserialize(deserializer)?;
Ok(Duration::from_secs_f64(secs))
}
}
pub fn format_bytes(bytes: usize) -> String {
if bytes < 1024 {
format!("{} B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.2} KB", bytes as f64 / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_metrics_config_default() {
let config = MetricsConfig::default();
assert!(config.detailed_timing);
assert!(config.track_memory);
assert!(config.track_throughput);
assert_eq!(config.max_records, 10000);
assert_eq!(config.sampling_rate, 1.0);
}
#[test]
fn test_metrics_config_lightweight() {
let config = MetricsConfig::lightweight();
assert!(!config.detailed_timing);
assert!(!config.track_memory);
assert!(config.track_throughput);
assert_eq!(config.sampling_rate, 0.1);
}
#[test]
fn test_operation_stats_update() {
let mut stats = OperationStats::default();
stats.update(Duration::from_millis(10), 100, 1024);
assert_eq!(stats.call_count, 1);
assert_eq!(stats.total_elements, 100);
assert_eq!(stats.min_time, Duration::from_millis(10));
assert_eq!(stats.max_time, Duration::from_millis(10));
stats.update(Duration::from_millis(20), 200, 2048);
assert_eq!(stats.call_count, 2);
assert_eq!(stats.total_elements, 300);
assert_eq!(stats.min_time, Duration::from_millis(10));
assert_eq!(stats.max_time, Duration::from_millis(20));
}
#[test]
fn test_operation_stats_throughput() {
let mut stats = OperationStats::default();
stats.update(Duration::from_secs(1), 1000, 0);
stats.update(Duration::from_secs(1), 1000, 0);
assert_eq!(stats.ops_per_sec(), 1.0);
assert_eq!(stats.elements_per_sec(), 1000.0);
}
#[test]
fn test_memory_stats() {
let mut stats = MemoryStats::default();
stats.record_allocation(1024);
assert_eq!(stats.current_usage, 1024);
assert_eq!(stats.peak_usage, 1024);
stats.record_allocation(2048);
assert_eq!(stats.current_usage, 3072);
assert_eq!(stats.peak_usage, 3072);
stats.record_deallocation(1024);
assert_eq!(stats.current_usage, 2048);
assert_eq!(stats.peak_usage, 3072);
}
#[test]
fn test_memory_stats_efficiency() {
let stats = MemoryStats {
total_bytes_allocated: 1000,
total_bytes_deallocated: 800,
..Default::default()
};
assert!((stats.efficiency() - 0.8).abs() < 0.001);
}
#[test]
fn test_metrics_collector_basic() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
let result = collector.time_operation("test_op", || 42);
assert_eq!(result, 42);
let stats = collector.operation_stats();
assert!(stats.contains_key("test_op"));
assert_eq!(stats["test_op"].call_count, 1);
}
#[test]
fn test_metrics_collector_with_elements() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
collector.time_operation_with_elements("einsum", 1000, || {
thread::sleep(Duration::from_micros(100));
});
let stats = collector.operation_stats();
assert_eq!(stats["einsum"].total_elements, 1000);
}
#[test]
fn test_metrics_collector_memory() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
collector.record_allocation(1024);
collector.record_allocation(2048);
collector.record_deallocation(1024);
let memory = collector.memory_stats();
assert_eq!(memory.current_usage, 2048);
assert_eq!(memory.peak_usage, 3072);
}
#[test]
fn test_metrics_collector_summary() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
for i in 0..5 {
collector.time_operation(&format!("op_{}", i), || {
thread::sleep(Duration::from_micros(10));
});
}
let summary = collector.summary();
assert_eq!(summary.total_operations, 5);
assert_eq!(summary.unique_operations, 5);
}
#[test]
fn test_metrics_collector_export_json() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
collector.time_operation("test", || {});
let json = collector.export_json().expect("unwrap");
assert!(json.contains("test"));
assert!(json.contains("total_operations"));
}
#[test]
fn test_metrics_collector_export_csv() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
collector.time_operation("op1", || {});
collector.time_operation("op2", || {});
let csv = collector.export_csv();
assert!(csv.contains("op1"));
assert!(csv.contains("op2"));
assert!(csv.contains("operation,call_count"));
}
#[test]
fn test_metrics_collector_reset() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
collector.time_operation("test", || {});
assert!(!collector.operation_stats().is_empty());
collector.reset();
assert!(collector.operation_stats().is_empty());
}
#[test]
fn test_atomic_metrics() {
let metrics = AtomicMetrics::new();
metrics.record_operation(Duration::from_millis(10));
metrics.record_operation(Duration::from_millis(20));
metrics.record_allocation(1024);
let (ops, time, peak) = metrics.snapshot();
assert_eq!(ops, 2);
assert_eq!(time, Duration::from_millis(30));
assert_eq!(peak, 1024);
}
#[test]
fn test_shared_metrics() {
let metrics = shared_metrics();
let metrics_clone = Arc::clone(&metrics);
metrics.record_operation(Duration::from_millis(10));
metrics_clone.record_operation(Duration::from_millis(20));
let (ops, _, _) = metrics.snapshot();
assert_eq!(ops, 2);
}
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(512), "512 B");
assert_eq!(format_bytes(1536), "1.50 KB");
assert_eq!(format_bytes(1572864), "1.50 MB");
assert_eq!(format_bytes(1610612736), "1.50 GB");
}
#[test]
fn test_metrics_summary_display() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
collector.time_operation("test", || {});
let summary = collector.summary();
let display = format!("{}", summary);
assert!(display.contains("Metrics Summary"));
assert!(display.contains("Total Operations"));
}
#[test]
fn test_detailed_records() {
let mut collector = MetricsCollector::new(MetricsConfig::default());
collector.time_operation("op1", || {});
collector.time_operation("op2", || {});
let records = collector.records();
assert_eq!(records.len(), 2);
assert_eq!(records[0].operation, "op1");
assert_eq!(records[1].operation, "op2");
}
#[test]
fn test_max_records_limit() {
let config = MetricsConfig {
max_records: 2,
..Default::default()
};
let mut collector = MetricsCollector::new(config);
collector.time_operation("op1", || {});
collector.time_operation("op2", || {});
collector.time_operation("op3", || {});
assert_eq!(collector.records().len(), 2);
}
}