use lazy_static::lazy_static;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
lazy_static! {
static ref MEMORY_METRICS: Arc<MemoryMetrics> = Arc::new(MemoryMetrics::new());
}
pub fn get_memory_metrics() -> Arc<MemoryMetrics> {
MEMORY_METRICS.clone()
}
pub struct MemoryMetrics {
allocation_count: AtomicUsize,
deallocation_count: AtomicUsize,
current_bytes: AtomicU64,
peak_bytes: AtomicU64,
total_allocated_bytes: AtomicU64,
total_deallocated_bytes: AtomicU64,
component_allocations: RwLock<HashMap<String, ComponentMetrics>>,
}
impl MemoryMetrics {
pub fn new() -> Self {
Self {
allocation_count: AtomicUsize::new(0),
deallocation_count: AtomicUsize::new(0),
current_bytes: AtomicU64::new(0),
peak_bytes: AtomicU64::new(0),
total_allocated_bytes: AtomicU64::new(0),
total_deallocated_bytes: AtomicU64::new(0),
component_allocations: RwLock::new(HashMap::new()),
}
}
pub fn record_allocation(&self, component: &str, bytes: u64) {
self.allocation_count.fetch_add(1, Ordering::Relaxed);
self.total_allocated_bytes
.fetch_add(bytes, Ordering::Relaxed);
let current = self.current_bytes.fetch_add(bytes, Ordering::Relaxed) + bytes;
let mut peak = self.peak_bytes.load(Ordering::Relaxed);
while current > peak {
match self.peak_bytes.compare_exchange_weak(
peak,
current,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => peak = actual,
}
}
let mut components = self.component_allocations.write().unwrap();
let entry = components.entry(component.to_string()).or_default();
entry.allocations += 1;
entry.current_bytes += bytes;
entry.total_allocated_bytes += bytes;
entry.peak_bytes = entry.peak_bytes.max(entry.current_bytes);
}
pub fn record_deallocation(&self, component: &str, bytes: u64) {
self.deallocation_count.fetch_add(1, Ordering::Relaxed);
self.total_deallocated_bytes
.fetch_add(bytes, Ordering::Relaxed);
self.current_bytes.fetch_sub(bytes, Ordering::Relaxed);
let mut components = self.component_allocations.write().unwrap();
if let Some(entry) = components.get_mut(component) {
entry.deallocations += 1;
entry.current_bytes = entry.current_bytes.saturating_sub(bytes);
entry.total_deallocated_bytes += bytes;
}
}
pub fn allocation_count(&self) -> usize {
self.allocation_count.load(Ordering::Relaxed)
}
pub fn deallocation_count(&self) -> usize {
self.deallocation_count.load(Ordering::Relaxed)
}
pub fn current_bytes(&self) -> u64 {
self.current_bytes.load(Ordering::Relaxed)
}
pub fn peak_bytes(&self) -> u64 {
self.peak_bytes.load(Ordering::Relaxed)
}
pub fn total_allocated_bytes(&self) -> u64 {
self.total_allocated_bytes.load(Ordering::Relaxed)
}
pub fn total_deallocated_bytes(&self) -> u64 {
self.total_deallocated_bytes.load(Ordering::Relaxed)
}
pub fn current_mb(&self) -> f64 {
self.current_bytes() as f64 / (1024.0 * 1024.0)
}
pub fn peak_mb(&self) -> f64 {
self.peak_bytes() as f64 / (1024.0 * 1024.0)
}
pub fn component_metrics(&self, component: &str) -> Option<ComponentMetrics> {
let components = self.component_allocations.read().unwrap();
components.get(component).cloned()
}
pub fn all_component_metrics(&self) -> HashMap<String, ComponentMetrics> {
let components = self.component_allocations.read().unwrap();
components.clone()
}
pub fn snapshot(&self) -> MemorySnapshot {
MemorySnapshot {
allocation_count: self.allocation_count(),
deallocation_count: self.deallocation_count(),
current_bytes: self.current_bytes(),
peak_bytes: self.peak_bytes(),
total_allocated_bytes: self.total_allocated_bytes(),
total_deallocated_bytes: self.total_deallocated_bytes(),
component_metrics: self.all_component_metrics(),
}
}
pub fn is_within_target(&self, target_mb: f64) -> bool {
self.current_mb() < target_mb
}
pub fn is_approaching_target(&self, target_mb: f64) -> bool {
self.current_mb() / target_mb > 0.8
}
pub fn reset(&self) {
self.allocation_count.store(0, Ordering::Relaxed);
self.deallocation_count.store(0, Ordering::Relaxed);
self.current_bytes.store(0, Ordering::Relaxed);
self.peak_bytes.store(0, Ordering::Relaxed);
self.total_allocated_bytes.store(0, Ordering::Relaxed);
self.total_deallocated_bytes.store(0, Ordering::Relaxed);
let mut components = self.component_allocations.write().unwrap();
components.clear();
}
pub fn update_prometheus(&self) {
let perf_metrics = crate::metrics::get_performance_metrics();
perf_metrics.update_memory_usage(self.current_bytes(), "total");
let components = self.component_allocations.read().unwrap();
for (name, metrics) in components.iter() {
perf_metrics.update_memory_usage(metrics.current_bytes, name);
}
}
}
impl Default for MemoryMetrics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct ComponentMetrics {
pub allocations: usize,
pub deallocations: usize,
pub current_bytes: u64,
pub peak_bytes: u64,
pub total_allocated_bytes: u64,
pub total_deallocated_bytes: u64,
}
impl ComponentMetrics {
pub fn current_mb(&self) -> f64 {
self.current_bytes as f64 / (1024.0 * 1024.0)
}
pub fn peak_mb(&self) -> f64 {
self.peak_bytes as f64 / (1024.0 * 1024.0)
}
pub fn live_allocations(&self) -> usize {
self.allocations.saturating_sub(self.deallocations)
}
pub fn avg_allocation_bytes(&self) -> f64 {
if self.allocations == 0 {
0.0
} else {
self.total_allocated_bytes as f64 / self.allocations as f64
}
}
}
#[derive(Debug, Clone)]
pub struct MemorySnapshot {
pub allocation_count: usize,
pub deallocation_count: usize,
pub current_bytes: u64,
pub peak_bytes: u64,
pub total_allocated_bytes: u64,
pub total_deallocated_bytes: u64,
pub component_metrics: HashMap<String, ComponentMetrics>,
}
impl MemorySnapshot {
pub fn current_mb(&self) -> f64 {
self.current_bytes as f64 / (1024.0 * 1024.0)
}
pub fn peak_mb(&self) -> f64 {
self.peak_bytes as f64 / (1024.0 * 1024.0)
}
pub fn live_allocations(&self) -> usize {
self.allocation_count
.saturating_sub(self.deallocation_count)
}
pub fn is_within_target(&self, target_mb: f64) -> bool {
self.current_mb() < target_mb
}
pub fn utilization_percent(&self, target_mb: f64) -> f64 {
(self.current_mb() / target_mb) * 100.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_allocation() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 1024);
assert_eq!(metrics.allocation_count(), 1);
assert_eq!(metrics.current_bytes(), 1024);
assert_eq!(metrics.peak_bytes(), 1024);
assert_eq!(metrics.total_allocated_bytes(), 1024);
}
#[test]
fn test_allocation_and_deallocation() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 1024);
metrics.record_deallocation("test", 512);
assert_eq!(metrics.allocation_count(), 1);
assert_eq!(metrics.deallocation_count(), 1);
assert_eq!(metrics.current_bytes(), 512);
assert_eq!(metrics.peak_bytes(), 1024); }
#[test]
fn test_peak_tracking() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 1000);
metrics.record_allocation("test", 500);
assert_eq!(metrics.peak_bytes(), 1500);
metrics.record_deallocation("test", 1000);
assert_eq!(metrics.current_bytes(), 500);
assert_eq!(metrics.peak_bytes(), 1500); }
#[test]
fn test_component_tracking() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("indexer", 1024);
metrics.record_allocation("search", 2048);
let indexer_metrics = metrics.component_metrics("indexer").unwrap();
assert_eq!(indexer_metrics.allocations, 1);
assert_eq!(indexer_metrics.current_bytes, 1024);
let search_metrics = metrics.component_metrics("search").unwrap();
assert_eq!(search_metrics.allocations, 1);
assert_eq!(search_metrics.current_bytes, 2048);
}
#[test]
fn test_component_deallocation() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("indexer", 1024);
metrics.record_deallocation("indexer", 512);
let indexer_metrics = metrics.component_metrics("indexer").unwrap();
assert_eq!(indexer_metrics.allocations, 1);
assert_eq!(indexer_metrics.deallocations, 1);
assert_eq!(indexer_metrics.current_bytes, 512);
assert_eq!(indexer_metrics.peak_bytes, 1024);
}
#[test]
fn test_megabyte_conversions() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 5 * 1024 * 1024);
assert_eq!(metrics.current_mb(), 5.0);
assert_eq!(metrics.peak_mb(), 5.0);
}
#[test]
fn test_snapshot() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 1024);
metrics.record_allocation("test", 2048);
let snapshot = metrics.snapshot();
assert_eq!(snapshot.allocation_count, 2);
assert_eq!(snapshot.current_bytes, 3072);
assert_eq!(snapshot.current_mb(), 3072.0 / (1024.0 * 1024.0));
assert_eq!(snapshot.live_allocations(), 2);
}
#[test]
fn test_target_checking() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 300 * 1024 * 1024);
assert!(metrics.is_within_target(500.0)); assert!(!metrics.is_approaching_target(500.0));
metrics.record_allocation("test", 150 * 1024 * 1024);
assert!(metrics.is_approaching_target(500.0)); }
#[test]
fn test_snapshot_target_checking() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 400 * 1024 * 1024);
let snapshot = metrics.snapshot();
assert!(snapshot.is_within_target(500.0));
assert_eq!(snapshot.utilization_percent(500.0), 80.0);
}
#[test]
fn test_component_metrics_calculations() {
let mut component = ComponentMetrics::default();
component.allocations = 10;
component.deallocations = 3;
component.total_allocated_bytes = 10240;
component.current_bytes = 7168;
component.peak_bytes = 10 * 1024 * 1024;
assert_eq!(component.live_allocations(), 7);
assert_eq!(component.avg_allocation_bytes(), 1024.0);
assert_eq!(component.current_mb(), 7168.0 / (1024.0 * 1024.0));
assert_eq!(component.peak_mb(), 10.0);
}
#[test]
fn test_reset() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("test", 1024);
assert_eq!(metrics.allocation_count(), 1);
metrics.reset();
assert_eq!(metrics.allocation_count(), 0);
assert_eq!(metrics.current_bytes(), 0);
assert_eq!(metrics.peak_bytes(), 0);
assert!(metrics.component_metrics("test").is_none());
}
#[test]
fn test_global_metrics() {
let metrics1 = get_memory_metrics();
let metrics2 = get_memory_metrics();
assert!(Arc::ptr_eq(&metrics1, &metrics2));
}
#[test]
fn test_concurrent_allocations() {
use std::thread;
let metrics = Arc::new(MemoryMetrics::new());
let mut handles = vec![];
for i in 0..10 {
let metrics = metrics.clone();
handles.push(thread::spawn(move || {
for _ in 0..100 {
metrics.record_allocation(&format!("thread_{}", i), 1024);
}
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(metrics.allocation_count(), 1000);
assert_eq!(metrics.current_bytes(), 1024000);
}
#[test]
fn test_all_component_metrics() {
let metrics = MemoryMetrics::new();
metrics.record_allocation("indexer", 1024);
metrics.record_allocation("search", 2048);
metrics.record_allocation("cache", 4096);
let all_metrics = metrics.all_component_metrics();
assert_eq!(all_metrics.len(), 3);
assert!(all_metrics.contains_key("indexer"));
assert!(all_metrics.contains_key("search"));
assert!(all_metrics.contains_key("cache"));
}
#[test]
fn test_zero_division_safety() {
let component = ComponentMetrics::default();
assert_eq!(component.avg_allocation_bytes(), 0.0);
}
}