use async_trait::async_trait; use cm_dashboard_shared::{registry, Metric, MetricValue, Status, StatusTracker, HysteresisThresholds}; use tracing::debug; use super::{utils, Collector, CollectorError}; use crate::config::MemoryConfig; /// Extremely efficient memory metrics collector /// /// EFFICIENCY OPTIMIZATIONS: /// - Single /proc/meminfo read for all memory metrics /// - Minimal string parsing with split operations /// - Pre-calculated KB to GB conversion /// - No regex or complex parsing /// - <0.1ms collection time target pub struct MemoryCollector { name: String, usage_thresholds: HysteresisThresholds, } /// Memory information parsed from /proc/meminfo #[derive(Debug, Default)] struct MemoryInfo { total_kb: u64, available_kb: u64, free_kb: u64, buffers_kb: u64, cached_kb: u64, swap_total_kb: u64, swap_free_kb: u64, } impl MemoryCollector { pub fn new(config: MemoryConfig) -> Self { // Create hysteresis thresholds with 5% gap for memory usage let usage_thresholds = HysteresisThresholds::with_custom_gaps( config.usage_warning_percent, 5.0, // 5% gap for warning recovery config.usage_critical_percent, 5.0, // 5% gap for critical recovery ); Self { name: "memory".to_string(), usage_thresholds, } } /// Calculate memory usage status using hysteresis thresholds fn calculate_usage_status(&self, metric_name: &str, usage_percent: f32, status_tracker: &mut StatusTracker) -> Status { status_tracker.calculate_with_hysteresis(metric_name, usage_percent, &self.usage_thresholds) } /// Parse /proc/meminfo efficiently /// Format: "MemTotal: 16384000 kB" async fn parse_meminfo(&self) -> Result { let content = utils::read_proc_file("/proc/meminfo")?; let mut info = MemoryInfo::default(); // Parse each line efficiently - only extract what we need for line in content.lines() { if let Some(colon_pos) = line.find(':') { let key = &line[..colon_pos]; let value_part = &line[colon_pos + 1..]; // Extract number from value part (format: " 12345 kB") if let Some(number_str) = value_part.split_whitespace().next() { if let Ok(value_kb) = utils::parse_u64(number_str) { match key { "MemTotal" => info.total_kb = value_kb, "MemAvailable" => info.available_kb = value_kb, "MemFree" => info.free_kb = value_kb, "Buffers" => info.buffers_kb = value_kb, "Cached" => info.cached_kb = value_kb, "SwapTotal" => info.swap_total_kb = value_kb, "SwapFree" => info.swap_free_kb = value_kb, _ => {} // Skip other fields for efficiency } } } } } // Validate that we got essential fields if info.total_kb == 0 { return Err(CollectorError::Parse { value: "MemTotal".to_string(), error: "MemTotal not found or zero in /proc/meminfo".to_string(), }); } // If MemAvailable is not available (older kernels), calculate it if info.available_kb == 0 { info.available_kb = info.free_kb + info.buffers_kb + info.cached_kb; } Ok(info) } /// Convert KB to GB efficiently (avoiding floating point in hot path) fn kb_to_gb(kb: u64) -> f32 { kb as f32 / 1_048_576.0 // 1024 * 1024 } /// Calculate memory metrics from parsed info fn calculate_metrics(&self, info: &MemoryInfo, status_tracker: &mut StatusTracker) -> Vec { let mut metrics = Vec::with_capacity(6); // Calculate derived values let used_kb = info.total_kb - info.available_kb; let usage_percent = (used_kb as f32 / info.total_kb as f32) * 100.0; let usage_status = self.calculate_usage_status(registry::MEMORY_USAGE_PERCENT, usage_percent, status_tracker); let swap_used_kb = info.swap_total_kb - info.swap_free_kb; // Convert to GB for metrics let total_gb = Self::kb_to_gb(info.total_kb); let used_gb = Self::kb_to_gb(used_kb); let available_gb = Self::kb_to_gb(info.available_kb); let swap_total_gb = Self::kb_to_gb(info.swap_total_kb); let swap_used_gb = Self::kb_to_gb(swap_used_kb); // Memory usage percentage (primary metric with status) metrics.push( Metric::new( registry::MEMORY_USAGE_PERCENT.to_string(), MetricValue::Float(usage_percent), usage_status, ) .with_description("Memory usage percentage".to_string()) .with_unit("%".to_string()), ); // Total memory metrics.push( Metric::new( registry::MEMORY_TOTAL_GB.to_string(), MetricValue::Float(total_gb), Status::Ok, // Total memory doesn't have status ) .with_description("Total system memory".to_string()) .with_unit("GB".to_string()), ); // Used memory metrics.push( Metric::new( registry::MEMORY_USED_GB.to_string(), MetricValue::Float(used_gb), Status::Ok, // Used memory absolute value doesn't have status ) .with_description("Used system memory".to_string()) .with_unit("GB".to_string()), ); // Available memory metrics.push( Metric::new( registry::MEMORY_AVAILABLE_GB.to_string(), MetricValue::Float(available_gb), Status::Ok, // Available memory absolute value doesn't have status ) .with_description("Available system memory".to_string()) .with_unit("GB".to_string()), ); // Swap metrics (only if swap exists) if info.swap_total_kb > 0 { metrics.push( Metric::new( registry::MEMORY_SWAP_TOTAL_GB.to_string(), MetricValue::Float(swap_total_gb), Status::Ok, ) .with_description("Total swap space".to_string()) .with_unit("GB".to_string()), ); metrics.push( Metric::new( registry::MEMORY_SWAP_USED_GB.to_string(), MetricValue::Float(swap_used_gb), Status::Ok, ) .with_description("Used swap space".to_string()) .with_unit("GB".to_string()), ); } // Monitor tmpfs (/tmp) usage if let Ok(tmpfs_metrics) = self.get_tmpfs_metrics() { metrics.extend(tmpfs_metrics); } metrics } /// Get tmpfs (/tmp) usage metrics fn get_tmpfs_metrics(&self) -> Result, CollectorError> { use std::process::Command; let output = Command::new("df") .arg("--block-size=1") .arg("/tmp") .output() .map_err(|e| CollectorError::SystemRead { path: "/tmp".to_string(), error: e.to_string(), })?; if !output.status.success() { return Ok(Vec::new()); // Return empty if /tmp not available } let output_str = String::from_utf8(output.stdout) .map_err(|e| CollectorError::Parse { value: "df output".to_string(), error: e.to_string(), })?; let lines: Vec<&str> = output_str.lines().collect(); if lines.len() < 2 { return Ok(Vec::new()); } let fields: Vec<&str> = lines[1].split_whitespace().collect(); if fields.len() < 4 { return Ok(Vec::new()); } let total_bytes: u64 = fields[1].parse() .map_err(|e: std::num::ParseIntError| CollectorError::Parse { value: fields[1].to_string(), error: e.to_string(), })?; let used_bytes: u64 = fields[2].parse() .map_err(|e: std::num::ParseIntError| CollectorError::Parse { value: fields[2].to_string(), error: e.to_string(), })?; let total_gb = total_bytes as f32 / (1024.0 * 1024.0 * 1024.0); let used_gb = used_bytes as f32 / (1024.0 * 1024.0 * 1024.0); let usage_percent = if total_bytes > 0 { (used_bytes as f32 / total_bytes as f32) * 100.0 } else { 0.0 }; let mut metrics = Vec::new(); let timestamp = chrono::Utc::now().timestamp() as u64; metrics.push(Metric { name: "memory_tmp_usage_percent".to_string(), value: MetricValue::Float(usage_percent), unit: Some("%".to_string()), description: Some("tmpfs /tmp usage percentage".to_string()), status: Status::Ok, timestamp, }); metrics.push(Metric { name: "memory_tmp_used_gb".to_string(), value: MetricValue::Float(used_gb), unit: Some("GB".to_string()), description: Some("tmpfs /tmp used space".to_string()), status: Status::Ok, timestamp, }); metrics.push(Metric { name: "memory_tmp_total_gb".to_string(), value: MetricValue::Float(total_gb), unit: Some("GB".to_string()), description: Some("tmpfs /tmp total space".to_string()), status: Status::Ok, timestamp, }); Ok(metrics) } } #[async_trait] impl Collector for MemoryCollector { fn name(&self) -> &str { &self.name } async fn collect(&self, status_tracker: &mut StatusTracker) -> Result, CollectorError> { debug!("Collecting memory metrics"); let start = std::time::Instant::now(); // Parse memory info from /proc/meminfo let info = self.parse_meminfo().await?; // Calculate all metrics from parsed info let metrics = self.calculate_metrics(&info, status_tracker); let duration = start.elapsed(); debug!( "Memory collection completed in {:?} with {} metrics", duration, metrics.len() ); // Efficiency check: warn if collection takes too long if duration.as_millis() > 1 { debug!( "Memory collection took {}ms - consider optimization", duration.as_millis() ); } // Store performance metrics // Performance tracking handled by cache system Ok(metrics) } }