Skip to main content

Grove/WASM/
MemoryManager.rs

1//! WASM Memory Manager
2//!
3//! Manages memory allocation, deallocation, and limits for WebAssembly
4//! instances. Enforces memory constraints and provides tracking for debugging.
5
6use std::sync::{
7	Arc,
8	atomic::{AtomicU64, Ordering},
9};
10
11use anyhow::{Context, Result};
12use serde::{Deserialize, Serialize};
13use crate::dev_log;
14#[allow(unused_imports)]
15use wasmtime::{Memory, MemoryType};
16
17/// Memory limits for WASM instances
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct MemoryLimits {
20	/// Maximum memory per instance in MB
21	pub max_memory_mb:u64,
22	/// Initial memory allocation per instance in MB
23	pub initial_memory_mb:u64,
24	/// Maximum table size (number of elements)
25	pub max_table_size:u32,
26	/// Maximum number of memory instances
27	pub max_memories:usize,
28	/// Maximum number of table instances
29	pub max_tables:usize,
30	/// Maximum number of instances that can be created
31	pub max_instances:usize,
32}
33
34impl Default for MemoryLimits {
35	fn default() -> Self {
36		Self {
37			max_memory_mb:512,
38			initial_memory_mb:64,
39			max_table_size:1024,
40			max_memories:10,
41			max_tables:10,
42			max_instances:100,
43		}
44	}
45}
46
47impl MemoryLimits {
48	/// Create custom memory limits
49	pub fn new(max_memory_mb:u64, initial_memory_mb:u64, max_instances:usize) -> Self {
50		Self { max_memory_mb, initial_memory_mb, max_instances, ..Default::default() }
51	}
52
53	/// Convert max memory to bytes
54	pub fn max_memory_bytes(&self) -> u64 { self.max_memory_mb * 1024 * 1024 }
55
56	/// Convert initial memory to bytes
57	pub fn initial_memory_bytes(&self) -> u64 { self.initial_memory_mb * 1024 * 1024 }
58
59	/// Validate memory request
60	pub fn validate_request(&self, requested_bytes:u64, current_usage:u64) -> Result<()> {
61		if current_usage + requested_bytes > self.max_memory_bytes() {
62			return Err(anyhow::anyhow!(
63				"Memory request exceeds limit: {} + {} > {} bytes",
64				current_usage,
65				requested_bytes,
66				self.max_memory_bytes()
67			));
68		}
69		Ok(())
70	}
71}
72
73/// Memory allocation record for tracking
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct MemoryAllocation {
76	/// Unique allocation identifier
77	pub id:String,
78	/// Instance ID that owns this memory
79	pub instance_id:String,
80	/// Memory type/identifier
81	pub memory_type:String,
82	/// Amount of memory allocated in bytes
83	pub size_bytes:u64,
84	/// Maximum size this allocation can grow to
85	pub max_size_bytes:u64,
86	/// Allocation timestamp
87	pub allocated_at:u64,
88	/// Whether this memory is shared
89	pub is_shared:bool,
90}
91
92/// Memory statistics
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct MemoryStats {
95	/// Total memory allocated in bytes
96	pub total_allocated:u64,
97	/// Total memory allocated in MB
98	pub total_allocated_mb:f64,
99	/// Number of memory allocations
100	pub allocation_count:usize,
101	/// Number of memory deallocations
102	pub deallocation_count:usize,
103	/// Peak memory usage in bytes
104	pub peak_memory_bytes:u64,
105	/// Peak memory usage in MB
106	pub peak_memory_mb:f64,
107}
108
109impl Default for MemoryStats {
110	fn default() -> Self {
111		Self {
112			total_allocated:0,
113			total_allocated_mb:0.0,
114			allocation_count:0,
115			deallocation_count:0,
116			peak_memory_bytes:0,
117			peak_memory_mb:0.0,
118		}
119	}
120}
121
122impl MemoryStats {
123	/// Update stats with new allocation
124	pub fn record_allocation(&mut self, size_bytes:u64) {
125		self.total_allocated += size_bytes;
126		self.allocation_count += 1;
127		if self.total_allocated > self.peak_memory_bytes {
128			self.peak_memory_bytes = self.total_allocated;
129		}
130		self.total_allocated_mb = self.total_allocated as f64 / (1024.0 * 1024.0);
131		self.peak_memory_mb = self.peak_memory_bytes as f64 / (1024.0 * 1024.0);
132	}
133
134	/// Update stats with deallocation
135	pub fn record_deallocation(&mut self, size_bytes:u64) {
136		self.total_allocated = self.total_allocated.saturating_sub(size_bytes);
137		self.deallocation_count += 1;
138		self.total_allocated_mb = self.total_allocated as f64 / (1024.0 * 1024.0);
139	}
140}
141
142/// WASM Memory Manager
143#[derive(Debug)]
144pub struct MemoryManagerImpl {
145	limits:MemoryLimits,
146	allocations:Vec<MemoryAllocation>,
147	stats:Arc<MemoryStats>,
148	peak_usage:Arc<AtomicU64>,
149}
150
151impl MemoryManagerImpl {
152	/// Create a new memory manager with the given limits
153	pub fn new(limits:MemoryLimits) -> Self {
154		Self {
155			limits,
156			allocations:Vec::new(),
157			stats:Arc::new(MemoryStats::default()),
158			peak_usage:Arc::new(AtomicU64::new(0)),
159		}
160	}
161
162	/// Get the current memory limits
163	pub fn limits(&self) -> &MemoryLimits { &self.limits }
164
165	/// Get current memory statistics
166	pub fn stats(&self) -> &MemoryStats { &self.stats }
167
168	/// Get peak memory usage
169	pub fn peak_usage_bytes(&self) -> u64 { self.peak_usage.load(Ordering::Relaxed) }
170
171	/// Get peak memory usage in MB
172	pub fn peak_usage_mb(&self) -> f64 { self.peak_usage.load(Ordering::Relaxed) as f64 / (1024.0 * 1024.0) }
173
174	/// Get current memory usage in bytes
175	pub fn current_usage_bytes(&self) -> u64 { self.allocations.iter().map(|a| a.size_bytes).sum() }
176
177	/// Get current memory usage in MB
178	pub fn current_usage_mb(&self) -> f64 { self.current_usage_bytes() as f64 / (1024.0 * 1024.0) }
179
180	/// Check if memory can be allocated
181	pub fn can_allocate(&self, requested_bytes:u64) -> bool {
182		let current = self.current_usage_bytes();
183		current + requested_bytes <= self.limits.max_memory_bytes()
184	}
185
186	/// Allocate memory for a WASM instance
187	pub fn allocate_memory(&mut self, instance_id:&str, memory_type:&str, requested_bytes:u64) -> Result<u64> {
188		dev_log!("wasm", "Allocating {} bytes for instance {} (type: {})", requested_bytes, instance_id, memory_type);
189
190		let current_usage = self.current_usage_bytes();
191
192		// Validate against limits
193		self.limits
194			.validate_request(requested_bytes, current_usage)
195			.context("Memory allocation validation failed")?;
196
197		// Check allocation count limit
198		if self.allocations.len() >= self.limits.max_memories {
199			return Err(anyhow::anyhow!(
200				"Maximum number of memory allocations reached: {}",
201				self.limits.max_memories
202			));
203		}
204
205		// Create allocation record
206		let allocation = MemoryAllocation {
207			id:format!("alloc-{}", uuid::Uuid::new_v4()),
208			instance_id:instance_id.to_string(),
209			memory_type:memory_type.to_string(),
210			size_bytes:requested_bytes,
211			max_size_bytes:self.limits.max_memory_bytes() - current_usage,
212			allocated_at:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(),
213			is_shared:false,
214		};
215
216		self.allocations.push(allocation);
217
218		// Update stats
219		Arc::make_mut(&mut self.stats).record_allocation(requested_bytes);
220
221		// Update peak usage
222		let new_peak = self.current_usage_bytes();
223		let current_peak = self.peak_usage.load(Ordering::Relaxed);
224		if new_peak > current_peak {
225			self.peak_usage.store(new_peak, Ordering::Relaxed);
226		}
227
228		dev_log!("wasm", "Memory allocated successfully. Total usage: {} MB", self.current_usage_mb());
229
230		Ok(requested_bytes)
231	}
232
233	/// Deallocate memory for a WASM instance
234	pub fn deallocate_memory(&mut self, instance_id:&str, memory_id:&str) -> Result<bool> {
235		dev_log!("wasm", "Deallocating memory {} for instance {}", memory_id, instance_id);
236
237		let pos = self
238			.allocations
239			.iter()
240			.position(|a| a.instance_id == instance_id && a.id == memory_id);
241
242		if let Some(pos) = pos {
243			let allocation = self.allocations.remove(pos);
244
245			// Update stats
246			Arc::make_mut(&mut self.stats).record_deallocation(allocation.size_bytes);
247
248			dev_log!("wasm", "Memory deallocated successfully. Remaining usage: {} MB", self.current_usage_mb());
249
250			Ok(true)
251		} else {
252			dev_log!("wasm", "warn: memory allocation not found: {} for instance {}", memory_id, instance_id);
253			Ok(false)
254		}
255	}
256
257	/// Deallocate all memory for an instance
258	pub fn deallocate_all_for_instance(&mut self, instance_id:&str) -> usize {
259		dev_log!("wasm", "Deallocating all memory for instance {}", instance_id);
260
261		let initial_count = self.allocations.len();
262
263		self.allocations.retain(|a| a.instance_id != instance_id);
264
265		let deallocated_count = initial_count - self.allocations.len();
266
267		if deallocated_count > 0 {
268			dev_log!("wasm", "Deallocated {} memory allocations for instance {}", deallocated_count, instance_id);
269		}
270
271		deallocated_count
272	}
273
274	/// Grow existing memory allocation
275	pub fn grow_memory(&mut self, instance_id:&str, memory_id:&str, additional_bytes:u64) -> Result<u64> {
276		dev_log!("wasm", "Growing memory {} for instance {} by {} bytes", memory_id, instance_id, additional_bytes);
277
278		// Calculate current usage before mutable borrow
279		let current_usage = self.current_usage_bytes();
280
281		let allocation = self
282			.allocations
283			.iter_mut()
284			.find(|a| a.instance_id == instance_id && a.id == memory_id)
285			.ok_or_else(|| anyhow::anyhow!("Memory allocation not found"))?;
286
287		// Validate against limits
288		self.limits
289			.validate_request(additional_bytes, current_usage)
290			.context("Memory growth validation failed")?;
291
292		allocation.size_bytes += additional_bytes;
293
294		dev_log!("wasm", "Memory grown successfully. New size: {} bytes", allocation.size_bytes);
295
296		Ok(allocation.size_bytes)
297	}
298
299	/// Get all allocations for an instance
300	pub fn get_allocations_for_instance(&self, instance_id:&str) -> Vec<&MemoryAllocation> {
301		self.allocations.iter().filter(|a| a.instance_id == instance_id).collect()
302	}
303
304	/// Check if memory limits are exceeded
305	pub fn is_exceeded(&self) -> bool { self.current_usage_bytes() > self.limits.max_memory_bytes() }
306
307	/// Get memory usage percentage
308	pub fn usage_percentage(&self) -> f64 {
309		(self.current_usage_bytes() as f64 / self.limits.max_memory_bytes() as f64) * 100.0
310	}
311
312	/// Reset all allocations and stats (use with caution)
313	pub fn reset(&mut self) {
314		self.allocations.clear();
315		self.stats = Arc::new(MemoryStats::default());
316		self.peak_usage.store(0, Ordering::Relaxed);
317		dev_log!("wasm", "Memory manager reset");
318	}
319}
320
321#[cfg(test)]
322mod tests {
323	use super::*;
324
325	#[test]
326	fn test_memory_limits_default() {
327		let limits = MemoryLimits::default();
328		assert_eq!(limits.max_memory_mb, 512);
329		assert_eq!(limits.initial_memory_mb, 64);
330	}
331
332	#[test]
333	fn test_memory_limits_custom() {
334		let limits = MemoryLimits::new(1024, 128, 50);
335		assert_eq!(limits.max_memory_mb, 1024);
336		assert_eq!(limits.initial_memory_mb, 128);
337		assert_eq!(limits.max_instances, 50);
338	}
339
340	#[test]
341	fn test_memory_limits_validation() {
342		let limits = MemoryLimits::new(100, 10, 10);
343
344		// Valid request
345		assert!(limits.validate_request(50, 0).is_ok());
346
347		// Exceeds limit
348		assert!(limits.validate_request(150, 0).is_err());
349		assert!(limits.validate_request(50, 60).is_err());
350	}
351
352	#[test]
353	fn test_memory_manager_creation() {
354		let limits = MemoryLimits::default();
355		let manager = MemoryManagerImpl::new(limits);
356		assert_eq!(manager.current_usage_bytes(), 0);
357		assert_eq!(manager.allocations.len(), 0);
358	}
359
360	#[test]
361	fn test_memory_allocation() {
362		let limits = MemoryLimits::default();
363		let mut manager = MemoryManagerImpl::new(limits);
364
365		let result = manager.allocate_memory("test-instance", "heap", 1024);
366		assert!(result.is_ok());
367		assert_eq!(manager.current_usage_bytes(), 1024);
368		assert_eq!(manager.allocations.len(), 1);
369	}
370
371	#[test]
372	fn test_memory_deallocation() {
373		let limits = MemoryLimits::default();
374		let mut manager = MemoryManagerImpl::new(limits);
375
376		manager.allocate_memory("test-instance", "heap", 1024).unwrap();
377		let allocation = &manager.allocations[0];
378		let memory_id = allocation.id.clone();
379
380		let result = manager.deallocate_memory("test-instance", &memory_id);
381		assert!(result.is_ok());
382		assert_eq!(manager.current_usage_bytes(), 0);
383		assert_eq!(manager.allocations.len(), 0);
384	}
385
386	#[test]
387	fn test_memory_stats() {
388		let mut stats = MemoryStats::default();
389		stats.record_allocation(1024);
390		assert_eq!(stats.allocation_count, 1);
391		assert_eq!(stats.total_allocated, 1024);
392
393		stats.record_deallocation(512);
394		assert_eq!(stats.deallocation_count, 1);
395		assert_eq!(stats.total_allocated, 512);
396	}
397
398	#[test]
399	fn test_memory_usage_percentage() {
400		let limits = MemoryLimits::new(1000, 0, 0);
401		let mut manager = MemoryManagerImpl::new(limits);
402
403		manager.allocate_memory("test", "heap", 500).unwrap();
404		assert_eq!(manager.usage_percentage(), 50.0);
405	}
406}