use wasmtime::ResourceLimiter;
pub struct MemoryTracker {
max_memory_bytes: usize,
current_memory_bytes: usize,
peak_memory_bytes: usize,
max_tables: usize,
current_tables: usize,
}
impl MemoryTracker {
pub fn new(max_memory_mb: usize) -> Self {
Self::with_byte_limit(max_memory_mb * 1024 * 1024)
}
pub fn with_byte_limit(max_memory_bytes: usize) -> Self {
Self {
max_memory_bytes,
current_memory_bytes: 0,
peak_memory_bytes: 0,
max_tables: 10,
current_tables: 0,
}
}
pub fn current_memory(&self) -> usize {
self.current_memory_bytes
}
pub fn peak_memory(&self) -> usize {
self.peak_memory_bytes
}
pub fn max_memory(&self) -> usize {
self.max_memory_bytes
}
pub fn memory_usage_percent(&self) -> f64 {
if self.max_memory_bytes == 0 {
0.0
} else {
(self.current_memory_bytes as f64 / self.max_memory_bytes as f64) * 100.0
}
}
pub fn is_memory_exceeded(&self) -> bool {
self.current_memory_bytes > self.max_memory_bytes
}
fn update_peak(&mut self) {
if self.current_memory_bytes > self.peak_memory_bytes {
self.peak_memory_bytes = self.current_memory_bytes;
}
}
}
impl ResourceLimiter for MemoryTracker {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
_maximum: Option<usize>,
) -> anyhow::Result<bool> {
if desired > self.max_memory_bytes {
tracing::warn!(
"Memory growth denied: {} bytes requested, {} bytes allowed",
desired,
self.max_memory_bytes
);
return Ok(false);
}
self.current_memory_bytes = desired;
self.update_peak();
tracing::debug!(
"Memory growth allowed: {} -> {} bytes ({:.1}% of limit)",
current,
desired,
self.memory_usage_percent()
);
Ok(true)
}
fn table_growing(
&mut self,
_current: usize,
_desired: usize,
_maximum: Option<usize>,
) -> anyhow::Result<bool> {
if self.current_tables >= self.max_tables {
tracing::warn!(
"Table creation denied: {} tables exist, {} allowed",
self.current_tables,
self.max_tables
);
return Ok(false);
}
self.current_tables += 1;
Ok(true)
}
fn tables(&self) -> usize {
self.current_tables
}
fn memories(&self) -> usize {
1
}
}
#[derive(Debug, Clone)]
pub struct MemoryStats {
pub current_bytes: usize,
pub peak_bytes: usize,
pub limit_bytes: usize,
pub usage_percent: f64,
}
impl MemoryStats {
pub fn from_tracker(tracker: &MemoryTracker) -> Self {
Self {
current_bytes: tracker.current_memory(),
peak_bytes: tracker.peak_memory(),
limit_bytes: tracker.max_memory(),
usage_percent: tracker.memory_usage_percent(),
}
}
pub fn is_critical(&self) -> bool {
self.usage_percent > 90.0
}
pub fn is_high(&self) -> bool {
self.usage_percent > 75.0
}
pub fn summary(&self) -> String {
format!(
"{} / {} MB ({:.1}%), peak: {} MB",
self.current_bytes / (1024 * 1024),
self.limit_bytes / (1024 * 1024),
self.usage_percent,
self.peak_bytes / (1024 * 1024)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_tracker_creation() {
let tracker = MemoryTracker::new(10);
assert_eq!(tracker.current_memory(), 0);
assert_eq!(tracker.peak_memory(), 0);
assert_eq!(tracker.max_memory(), 10 * 1024 * 1024);
assert_eq!(tracker.memory_usage_percent(), 0.0);
}
#[test]
fn test_with_byte_limit_is_exact() {
let tracker = MemoryTracker::with_byte_limit(5_242_880); assert_eq!(tracker.max_memory(), 5_242_880);
}
#[test]
fn test_memory_tracker_growing() {
let mut tracker = MemoryTracker::new(10);
let ok = tracker.memory_growing(0, 5 * 1024 * 1024, None).unwrap();
assert!(ok);
assert_eq!(tracker.current_memory(), 5 * 1024 * 1024);
assert_eq!(tracker.peak_memory(), 5 * 1024 * 1024);
let denied = tracker.memory_growing(5 * 1024 * 1024, 15 * 1024 * 1024, None).unwrap();
assert!(!denied);
}
#[test]
fn test_memory_stats() {
let mut tracker = MemoryTracker::new(10);
tracker.memory_growing(0, 8 * 1024 * 1024, None).unwrap();
let stats = MemoryStats::from_tracker(&tracker);
assert_eq!(stats.current_bytes, 8 * 1024 * 1024);
assert_eq!(stats.limit_bytes, 10 * 1024 * 1024);
assert!(stats.is_high());
assert!(!stats.is_critical());
}
#[test]
fn test_memory_tracker_peak() {
let mut tracker = MemoryTracker::new(10);
tracker.memory_growing(0, 5 * 1024 * 1024, None).unwrap();
assert_eq!(tracker.peak_memory(), 5 * 1024 * 1024);
tracker.memory_growing(5 * 1024 * 1024, 8 * 1024 * 1024, None).unwrap();
assert_eq!(tracker.peak_memory(), 8 * 1024 * 1024);
tracker.current_memory_bytes = 6 * 1024 * 1024;
assert_eq!(tracker.peak_memory(), 8 * 1024 * 1024);
}
#[test]
fn test_table_limits() {
let mut tracker = MemoryTracker::new(10);
tracker.max_tables = 2;
assert!(tracker.table_growing(0, 1, None).unwrap());
assert_eq!(tracker.current_tables, 1);
assert!(tracker.table_growing(1, 2, None).unwrap());
assert_eq!(tracker.current_tables, 2);
assert!(!tracker.table_growing(2, 3, None).unwrap());
}
}