use std::sync::atomic::{AtomicUsize, Ordering};
use parking_lot::Mutex;
use tracing::{debug, warn};
pub type MemoryGrowthCallback = Box<dyn Fn(MemoryGrowthEvent) + Send + Sync>;
#[derive(Debug, Clone)]
pub struct MemoryGrowthEvent {
pub from_bytes: usize,
pub to_bytes: usize,
pub max_bytes: usize,
}
#[derive(Debug, Clone)]
pub struct LimiterConfig {
pub max_memory_bytes: usize,
pub max_table_elements: u32,
pub max_memories: u32,
pub max_tables: u32,
}
impl Default for LimiterConfig {
fn default() -> Self {
Self {
max_memory_bytes: 64 * 1024 * 1024, max_table_elements: 10_000,
max_memories: 1,
max_tables: 10,
}
}
}
impl LimiterConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_memory(mut self, bytes: usize) -> Self {
self.max_memory_bytes = bytes;
self
}
pub fn with_max_table_elements(mut self, elements: u32) -> Self {
self.max_table_elements = elements;
self
}
}
pub struct AegisResourceLimiter {
config: LimiterConfig,
current_memory: AtomicUsize,
peak_memory: AtomicUsize,
allocation_count: AtomicUsize,
on_memory_grow: Mutex<Option<MemoryGrowthCallback>>,
}
impl AegisResourceLimiter {
pub fn new(config: LimiterConfig) -> Self {
Self {
config,
current_memory: AtomicUsize::new(0),
peak_memory: AtomicUsize::new(0),
allocation_count: AtomicUsize::new(0),
on_memory_grow: Mutex::new(None),
}
}
pub fn with_defaults() -> Self {
Self::new(LimiterConfig::default())
}
pub fn set_memory_growth_callback(&self, callback: MemoryGrowthCallback) {
*self.on_memory_grow.lock() = Some(callback);
}
pub fn current_memory(&self) -> usize {
self.current_memory.load(Ordering::Relaxed)
}
pub fn peak_memory(&self) -> usize {
self.peak_memory.load(Ordering::Relaxed)
}
pub fn allocation_count(&self) -> usize {
self.allocation_count.load(Ordering::Relaxed)
}
pub fn remaining_memory(&self) -> usize {
self.config
.max_memory_bytes
.saturating_sub(self.current_memory())
}
pub fn max_memory(&self) -> usize {
self.config.max_memory_bytes
}
pub fn check_memory_growth(&self, current: usize, desired: usize) -> bool {
if desired > self.config.max_memory_bytes {
warn!(
current_bytes = current,
desired_bytes = desired,
max_bytes = self.config.max_memory_bytes,
"Memory growth denied: exceeds limit"
);
return false;
}
self.current_memory.store(desired, Ordering::Relaxed);
self.allocation_count.fetch_add(1, Ordering::Relaxed);
let mut peak = self.peak_memory.load(Ordering::Relaxed);
while desired > peak {
match self.peak_memory.compare_exchange_weak(
peak,
desired,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(current_peak) => peak = current_peak,
}
}
if let Some(callback) = self.on_memory_grow.lock().as_ref() {
callback(MemoryGrowthEvent {
from_bytes: current,
to_bytes: desired,
max_bytes: self.config.max_memory_bytes,
});
}
debug!(
from_bytes = current,
to_bytes = desired,
peak_bytes = self.peak_memory(),
"Memory growth permitted"
);
true
}
pub fn check_table_growth(&self, current: u32, desired: u32) -> bool {
if desired > self.config.max_table_elements {
warn!(
current_elements = current,
desired_elements = desired,
max_elements = self.config.max_table_elements,
"Table growth denied: exceeds limit"
);
return false;
}
debug!(
from_elements = current,
to_elements = desired,
"Table growth permitted"
);
true
}
pub fn reset(&self) {
self.current_memory.store(0, Ordering::Relaxed);
self.peak_memory.store(0, Ordering::Relaxed);
self.allocation_count.store(0, Ordering::Relaxed);
}
pub fn stats(&self) -> LimiterStats {
LimiterStats {
current_memory: self.current_memory(),
peak_memory: self.peak_memory(),
allocation_count: self.allocation_count(),
max_memory: self.config.max_memory_bytes,
}
}
}
#[derive(Debug, Clone)]
pub struct LimiterStats {
pub current_memory: usize,
pub peak_memory: usize,
pub allocation_count: usize,
pub max_memory: usize,
}
impl LimiterStats {
pub fn utilization_percent(&self) -> f64 {
if self.max_memory == 0 {
0.0
} else {
(self.peak_memory as f64 / self.max_memory as f64) * 100.0
}
}
}
impl std::fmt::Debug for AegisResourceLimiter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AegisResourceLimiter")
.field("config", &self.config)
.field("current_memory", &self.current_memory())
.field("peak_memory", &self.peak_memory())
.field("allocation_count", &self.allocation_count())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn test_limiter_creation() {
let limiter = AegisResourceLimiter::new(LimiterConfig::default());
assert_eq!(limiter.current_memory(), 0);
assert_eq!(limiter.peak_memory(), 0);
}
#[test]
fn test_memory_growth_allowed() {
let config = LimiterConfig::default().with_max_memory(1024 * 1024);
let limiter = AegisResourceLimiter::new(config);
assert!(limiter.check_memory_growth(0, 512 * 1024));
assert_eq!(limiter.current_memory(), 512 * 1024);
}
#[test]
fn test_memory_growth_denied() {
let config = LimiterConfig::default().with_max_memory(1024 * 1024);
let limiter = AegisResourceLimiter::new(config);
assert!(!limiter.check_memory_growth(0, 2 * 1024 * 1024));
}
#[test]
fn test_peak_memory_tracking() {
let config = LimiterConfig::default().with_max_memory(10 * 1024 * 1024);
let limiter = AegisResourceLimiter::new(config);
limiter.check_memory_growth(0, 1024);
limiter.check_memory_growth(1024, 2048);
limiter.check_memory_growth(2048, 1024);
assert_eq!(limiter.peak_memory(), 2048);
assert_eq!(limiter.current_memory(), 1024);
}
#[test]
fn test_memory_growth_callback() {
use std::sync::atomic::AtomicBool;
let callback_called = Arc::new(AtomicBool::new(false));
let callback_called_clone = Arc::clone(&callback_called);
let limiter = AegisResourceLimiter::with_defaults();
limiter.set_memory_growth_callback(Box::new(move |_event| {
callback_called_clone.store(true, Ordering::SeqCst);
}));
limiter.check_memory_growth(0, 1024);
assert!(callback_called.load(Ordering::SeqCst));
}
#[test]
fn test_table_growth() {
let config = LimiterConfig::default().with_max_table_elements(1000);
let limiter = AegisResourceLimiter::new(config);
assert!(limiter.check_table_growth(0, 500));
assert!(!limiter.check_table_growth(500, 1500));
}
#[test]
fn test_stats() {
let config = LimiterConfig::default().with_max_memory(1024);
let limiter = AegisResourceLimiter::new(config);
limiter.check_memory_growth(0, 512);
let stats = limiter.stats();
assert_eq!(stats.current_memory, 512);
assert_eq!(stats.peak_memory, 512);
assert_eq!(stats.max_memory, 1024);
assert!((stats.utilization_percent() - 50.0).abs() < 0.01);
}
}