use std::collections::VecDeque;
use std::fmt;
use super::device::DeviceId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PressureLevel {
Ok,
Elevated,
Warning,
Critical,
}
impl PressureLevel {
#[must_use]
pub fn from_available_percent(percent: f64) -> Self {
match percent {
x if x >= 50.0 => Self::Ok,
x if x >= 30.0 => Self::Elevated,
x if x >= 15.0 => Self::Warning,
_ => Self::Critical,
}
}
#[must_use]
pub fn recommendation(&self) -> &'static str {
match self {
Self::Ok => "System healthy - normal operation",
Self::Elevated => "Memory usage elevated - monitor closely",
Self::Warning => "High memory usage - reduce parallel jobs",
Self::Critical => "Critical memory pressure - block new allocations",
}
}
#[must_use]
pub fn should_block_allocations(&self) -> bool {
matches!(self, Self::Critical)
}
#[must_use]
pub fn ansi_color(&self) -> &'static str {
match self {
Self::Ok => "\x1b[32m", Self::Elevated => "\x1b[33m", Self::Warning => "\x1b[38;5;208m", Self::Critical => "\x1b[31m", }
}
}
impl fmt::Display for PressureLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ok => write!(f, "OK"),
Self::Elevated => write!(f, "ELEVATED"),
Self::Warning => write!(f, "WARNING"),
Self::Critical => write!(f, "CRITICAL"),
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryMetrics {
pub ram_used_bytes: u64,
pub ram_total_bytes: u64,
pub ram_available_bytes: u64,
pub ram_cached_bytes: u64,
pub ram_buffers_bytes: u64,
pub swap_used_bytes: u64,
pub swap_total_bytes: u64,
pub gpu_vram: Vec<GpuVramMetrics>,
pub pressure_level: PressureLevel,
pub safe_parallel_jobs: u32,
pub ram_read_bandwidth_gbps: Option<f64>,
pub ram_write_bandwidth_gbps: Option<f64>,
pub ram_history: VecDeque<f64>,
pub swap_history: VecDeque<f64>,
}
impl MemoryMetrics {
pub const MAX_HISTORY_POINTS: usize = 60;
#[must_use]
pub fn new() -> Self {
let mut metrics = Self::default();
metrics.refresh();
metrics
}
pub fn refresh(&mut self) {
self.read_meminfo();
self.read_swapinfo();
self.calculate_pressure();
self.update_history();
}
fn read_meminfo(&mut self) {
#[cfg(target_os = "linux")]
{
if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let value_kb: u64 = parts[1].parse().unwrap_or(0);
let value_bytes = value_kb * 1024;
match parts[0] {
"MemTotal:" => self.ram_total_bytes = value_bytes,
"MemAvailable:" => self.ram_available_bytes = value_bytes,
"Cached:" => self.ram_cached_bytes = value_bytes,
"Buffers:" => self.ram_buffers_bytes = value_bytes,
_ => {}
}
}
}
self.ram_used_bytes = self.ram_total_bytes.saturating_sub(self.ram_available_bytes);
}
}
}
fn read_swapinfo(&mut self) {
#[cfg(target_os = "linux")]
{
if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
for line in content.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let value_kb: u64 = parts[1].parse().unwrap_or(0);
let value_bytes = value_kb * 1024;
match parts[0] {
"SwapTotal:" => self.swap_total_bytes = value_bytes,
"SwapFree:" => {
self.swap_used_bytes =
self.swap_total_bytes.saturating_sub(value_bytes);
}
_ => {}
}
}
}
}
}
}
fn calculate_pressure(&mut self) {
let available_pct = self.ram_available_percent();
self.pressure_level = PressureLevel::from_available_percent(available_pct);
let available_gb = self.ram_available_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let cpu_cores = std::thread::available_parallelism().map(|n| n.get() as u32).unwrap_or(1);
self.safe_parallel_jobs = ((available_gb / 3.0) as u32).min(cpu_cores).max(1);
}
fn update_history(&mut self) {
self.ram_history.push_back(self.ram_usage_percent());
if self.ram_history.len() > Self::MAX_HISTORY_POINTS {
self.ram_history.pop_front();
}
self.swap_history.push_back(self.swap_usage_percent());
if self.swap_history.len() > Self::MAX_HISTORY_POINTS {
self.swap_history.pop_front();
}
}
#[must_use]
pub fn ram_usage_percent(&self) -> f64 {
if self.ram_total_bytes == 0 {
return 0.0;
}
(self.ram_used_bytes as f64 / self.ram_total_bytes as f64) * 100.0
}
#[must_use]
pub fn ram_available_percent(&self) -> f64 {
if self.ram_total_bytes == 0 {
return 100.0;
}
(self.ram_available_bytes as f64 / self.ram_total_bytes as f64) * 100.0
}
#[must_use]
pub fn swap_usage_percent(&self) -> f64 {
batuta_common::math::usage_percent(self.swap_used_bytes, self.swap_total_bytes)
}
#[must_use]
pub fn ram_used_gb(&self) -> f64 {
self.ram_used_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
#[must_use]
pub fn ram_total_gb(&self) -> f64 {
self.ram_total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
#[must_use]
pub fn swap_used_gb(&self) -> f64 {
self.swap_used_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
#[must_use]
pub fn swap_total_gb(&self) -> f64 {
self.swap_total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
#[must_use]
pub fn total_vram_used_bytes(&self) -> u64 {
self.gpu_vram.iter().map(|v| v.used_bytes).sum()
}
#[must_use]
pub fn total_vram_total_bytes(&self) -> u64 {
self.gpu_vram.iter().map(|v| v.total_bytes).sum()
}
}
impl Default for MemoryMetrics {
fn default() -> Self {
Self {
ram_used_bytes: 0,
ram_total_bytes: 0,
ram_available_bytes: 0,
ram_cached_bytes: 0,
ram_buffers_bytes: 0,
swap_used_bytes: 0,
swap_total_bytes: 0,
gpu_vram: Vec::new(),
pressure_level: PressureLevel::Ok,
safe_parallel_jobs: 1,
ram_read_bandwidth_gbps: None,
ram_write_bandwidth_gbps: None,
ram_history: VecDeque::with_capacity(Self::MAX_HISTORY_POINTS),
swap_history: VecDeque::with_capacity(Self::MAX_HISTORY_POINTS),
}
}
}
#[derive(Debug, Clone)]
pub struct GpuVramMetrics {
pub device_id: DeviceId,
pub used_bytes: u64,
pub total_bytes: u64,
pub reserved_bytes: u64,
pub bar1_used_bytes: u64,
pub history: VecDeque<f64>,
}
impl GpuVramMetrics {
pub const MAX_HISTORY_POINTS: usize = 60;
#[must_use]
pub fn new(device_id: DeviceId, used: u64, total: u64) -> Self {
Self {
device_id,
used_bytes: used,
total_bytes: total,
reserved_bytes: 0,
bar1_used_bytes: 0,
history: VecDeque::with_capacity(Self::MAX_HISTORY_POINTS),
}
}
#[must_use]
pub fn usage_percent(&self) -> f64 {
if self.total_bytes == 0 {
return 0.0;
}
(self.used_bytes as f64 / self.total_bytes as f64) * 100.0
}
#[must_use]
pub fn available_bytes(&self) -> u64 {
self.total_bytes.saturating_sub(self.used_bytes)
}
#[must_use]
pub fn used_gb(&self) -> f64 {
self.used_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
#[must_use]
pub fn total_gb(&self) -> f64 {
self.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
pub fn update(&mut self, used: u64) {
self.used_bytes = used;
self.history.push_back(self.usage_percent());
if self.history.len() > Self::MAX_HISTORY_POINTS {
self.history.pop_front();
}
}
}
#[derive(Debug, Clone)]
pub struct PressureAnalysis {
pub level: PressureLevel,
pub available_percent: f64,
pub available_gb: f64,
pub safe_jobs: u32,
pub block_builds: bool,
pub recommendation: String,
}
impl PressureAnalysis {
#[must_use]
pub fn from_metrics(metrics: &MemoryMetrics) -> Self {
let available_pct = metrics.ram_available_percent();
let available_gb = metrics.ram_available_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let level = metrics.pressure_level;
Self {
level,
available_percent: available_pct,
available_gb,
safe_jobs: metrics.safe_parallel_jobs,
block_builds: level.should_block_allocations(),
recommendation: level.recommendation().to_string(),
}
}
}
#[cfg(test)]
mod tests;