use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use crate::errors::TaleError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryPressure {
Low,
Moderate,
High,
Critical,
}
impl MemoryPressure {
pub fn chunk_size_factor(&self) -> f64 {
match self {
MemoryPressure::Low => 1.0, MemoryPressure::Moderate => 0.8, MemoryPressure::High => 0.5, MemoryPressure::Critical => 0.25, }
}
pub fn requires_optimization(&self) -> bool {
matches!(self, MemoryPressure::High | MemoryPressure::Critical)
}
}
#[derive(Debug)]
pub struct MemoryAllocation {
size: usize,
reader_id: String,
allocated_at: Instant,
budget: Arc<RwLock<MemoryBudgetInner>>,
}
impl MemoryAllocation {
fn new(size: usize, reader_id: String, budget: Arc<RwLock<MemoryBudgetInner>>) -> Self {
Self {
size,
reader_id,
allocated_at: Instant::now(),
budget,
}
}
pub fn size(&self) -> usize {
self.size
}
pub fn age(&self) -> Duration {
self.allocated_at.elapsed()
}
pub fn deallocate(self) {
drop(self); }
}
impl Drop for MemoryAllocation {
fn drop(&mut self) {
if let Ok(mut budget) = self.budget.write() {
budget.deallocate(self.size, &self.reader_id);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ReaderMemoryStats {
pub current_allocation: usize,
pub peak_allocation: usize,
pub allocation_count: usize,
pub allocation_failures: usize,
pub total_allocations: usize,
}
#[derive(Debug)]
struct MemoryBudgetInner {
total_limit: usize,
current_usage: usize,
peak_usage: usize,
reader_stats: HashMap<String, ReaderMemoryStats>,
last_system_check: Instant,
system_memory_available: usize,
}
impl MemoryBudgetInner {
fn new(total_limit: usize) -> Result<Self, TaleError> {
let system_memory = get_system_memory_available()?;
Ok(Self {
total_limit,
current_usage: 0,
peak_usage: 0,
reader_stats: HashMap::new(),
last_system_check: Instant::now(),
system_memory_available: system_memory,
})
}
fn try_allocate(&mut self, size: usize, reader_id: &str) -> Result<bool, TaleError> {
if self.last_system_check.elapsed() > Duration::from_secs(1) {
self.system_memory_available = get_system_memory_available()?;
self.last_system_check = Instant::now();
}
let new_usage = self.current_usage + size;
if new_usage > self.total_limit {
let stats = self.reader_stats.entry(reader_id.to_string()).or_default();
stats.allocation_failures += 1;
return Ok(false);
}
let system_safety_margin = self.system_memory_available / 4; if size > system_safety_margin {
let stats = self.reader_stats.entry(reader_id.to_string()).or_default();
stats.allocation_failures += 1;
return Ok(false);
}
self.current_usage = new_usage;
self.peak_usage = self.peak_usage.max(new_usage);
let stats = self.reader_stats.entry(reader_id.to_string()).or_default();
stats.current_allocation += size;
stats.peak_allocation = stats.peak_allocation.max(stats.current_allocation);
stats.allocation_count += 1;
stats.total_allocations += 1;
Ok(true)
}
fn deallocate(&mut self, size: usize, reader_id: &str) {
self.current_usage = self.current_usage.saturating_sub(size);
if let Some(stats) = self.reader_stats.get_mut(reader_id) {
stats.current_allocation = stats.current_allocation.saturating_sub(size);
stats.allocation_count = stats.allocation_count.saturating_sub(1);
}
}
fn current_pressure(&self) -> MemoryPressure {
use crate::defaults::SystemDefaults;
let usage_ratio = self.current_usage as f64 / self.total_limit as f64;
match usage_ratio {
r if r < SystemDefaults::MEMORY_PRESSURE_LOW_THRESHOLD => MemoryPressure::Low,
r if r < SystemDefaults::MEMORY_PRESSURE_MODERATE_THRESHOLD => MemoryPressure::Moderate,
r if r < SystemDefaults::MEMORY_PRESSURE_HIGH_THRESHOLD => MemoryPressure::High,
_ => MemoryPressure::Critical,
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryBudget {
inner: Arc<RwLock<MemoryBudgetInner>>,
}
impl MemoryBudget {
pub fn new(total_limit: usize) -> Result<Self, TaleError> {
let inner = Arc::new(RwLock::new(MemoryBudgetInner::new(total_limit)?));
Ok(Self { inner })
}
pub fn from_system_memory(percentage: f64) -> Result<Self, TaleError> {
let system_memory = get_system_memory_available()?;
let limit = (system_memory as f64 * percentage / 100.0) as usize;
Self::new(limit)
}
pub fn try_allocate(&self, size: usize, reader_id: &str) -> Result<Option<MemoryAllocation>, TaleError> {
let mut inner = self
.inner
.write()
.map_err(|_| TaleError::MemoryError("Failed to acquire budget lock for allocation".to_string()))?;
if inner.try_allocate(size, reader_id)? {
let allocation = MemoryAllocation::new(size, reader_id.to_string(), self.inner.clone());
Ok(Some(allocation))
} else {
Ok(None)
}
}
pub fn current_pressure(&self) -> Result<MemoryPressure, TaleError> {
let inner = self
.inner
.read()
.map_err(|_| TaleError::MemoryError("Failed to acquire budget lock for pressure check".to_string()))?;
Ok(inner.current_pressure())
}
pub fn usage_stats(&self) -> Result<MemoryBudgetStats, TaleError> {
let inner = self
.inner
.read()
.map_err(|_| TaleError::MemoryError("Failed to acquire budget lock for stats".to_string()))?;
Ok(MemoryBudgetStats {
total_limit: inner.total_limit,
current_usage: inner.current_usage,
peak_usage: inner.peak_usage,
pressure: inner.current_pressure(),
reader_count: inner.reader_stats.len(),
system_memory_available: inner.system_memory_available,
})
}
pub fn reader_stats(&self, reader_id: &str) -> Result<Option<ReaderMemoryStats>, TaleError> {
let inner = self
.inner
.read()
.map_err(|_| TaleError::MemoryError("Failed to acquire budget lock for reader stats".to_string()))?;
Ok(inner.reader_stats.get(reader_id).cloned())
}
pub fn recommended_chunk_size(&self, base_size: usize) -> Result<usize, TaleError> {
let pressure = self.current_pressure()?;
let factor = pressure.chunk_size_factor();
Ok((base_size as f64 * factor) as usize)
}
pub fn requires_emergency_measures(&self) -> Result<bool, TaleError> {
let pressure = self.current_pressure()?;
Ok(matches!(pressure, MemoryPressure::Critical))
}
}
#[derive(Debug, Clone)]
pub struct MemoryBudgetStats {
pub total_limit: usize,
pub current_usage: usize,
pub peak_usage: usize,
pub pressure: MemoryPressure,
pub reader_count: usize,
pub system_memory_available: usize,
}
impl MemoryBudgetStats {
pub fn usage_percentage(&self) -> f64 {
if self.total_limit > 0 {
(self.current_usage as f64 / self.total_limit as f64) * 100.0
} else {
0.0
}
}
pub fn available_memory(&self) -> usize {
self.total_limit.saturating_sub(self.current_usage)
}
pub fn print_report(&self) {
println!("Memory Budget Report:");
println!("====================");
println!("Total Limit: {} MB", self.total_limit / (1024 * 1024));
println!(
"Current Usage: {} MB ({:.1}%)",
self.current_usage / (1024 * 1024),
self.usage_percentage()
);
println!("Peak Usage: {} MB", self.peak_usage / (1024 * 1024));
println!("Available: {} MB", self.available_memory() / (1024 * 1024));
println!("Pressure Level: {:?}", self.pressure);
println!("Active Readers: {}", self.reader_count);
println!("System Memory: {} MB", self.system_memory_available / (1024 * 1024));
}
}
fn get_system_memory_available() -> Result<usize, TaleError> {
if let Some(stats) = memory_stats::memory_stats() {
Ok(stats.physical_mem)
} else {
Ok(1024 * 1024 * 1024)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn _memory_pressure_levels_work() {
assert_eq!(MemoryPressure::Low.chunk_size_factor(), 1.0);
assert_eq!(MemoryPressure::Moderate.chunk_size_factor(), 0.8);
assert_eq!(MemoryPressure::High.chunk_size_factor(), 0.5);
assert_eq!(MemoryPressure::Critical.chunk_size_factor(), 0.25);
}
#[test]
fn memory_budget_allocation_works() -> Result<(), TaleError> {
let budget = MemoryBudget::new(1000)?;
let alloc1 = budget.try_allocate(500, "reader1")?;
assert!(alloc1.is_some());
let alloc2 = budget.try_allocate(400, "reader2")?;
assert!(alloc2.is_some());
let alloc3 = budget.try_allocate(200, "reader3")?;
assert!(alloc3.is_none());
drop(alloc1);
let alloc4 = budget.try_allocate(300, "reader4")?;
assert!(alloc4.is_some());
Ok(())
}
#[test]
fn memory_pressure_calculation_works() -> Result<(), TaleError> {
let budget = MemoryBudget::new(1000)?;
let _alloc1 = budget.try_allocate(500, "reader1")?;
assert_eq!(budget.current_pressure()?, MemoryPressure::Low);
let _alloc2 = budget.try_allocate(150, "reader2")?;
assert_eq!(budget.current_pressure()?, MemoryPressure::Moderate);
let _alloc3 = budget.try_allocate(200, "reader3")?;
assert_eq!(budget.current_pressure()?, MemoryPressure::High);
let _alloc4 = budget.try_allocate(100, "reader4")?;
assert_eq!(budget.current_pressure()?, MemoryPressure::Critical);
Ok(())
}
#[test]
fn can_recommend_chunk_size() -> Result<(), TaleError> {
let budget = MemoryBudget::new(1000)?;
assert_eq!(budget.recommended_chunk_size(1000)?, 1000);
let _alloc = budget.try_allocate(700, "reader1")?;
assert_eq!(budget.recommended_chunk_size(1000)?, 800);
Ok(())
}
#[test]
fn allocation_automatic_cleanup_works() -> Result<(), TaleError> {
let budget = MemoryBudget::new(1000)?;
{
let _alloc1 = budget.try_allocate(500, "reader1")?;
let _alloc2 = budget.try_allocate(400, "reader2")?;
let stats = budget.usage_stats()?;
assert_eq!(stats.current_usage, 900);
}
let stats = budget.usage_stats()?;
assert_eq!(stats.current_usage, 0);
Ok(())
}
}