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