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 tracing::{debug, instrument, warn};
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	#[instrument(skip(self, instance_id))]
188	pub fn allocate_memory(&mut self, instance_id:&str, memory_type:&str, requested_bytes:u64) -> Result<u64> {
189		debug!(
190			"Allocating {} bytes for instance {} (type: {})",
191			requested_bytes, instance_id, memory_type
192		);
193
194		let current_usage = self.current_usage_bytes();
195
196		// Validate against limits
197		self.limits
198			.validate_request(requested_bytes, current_usage)
199			.context("Memory allocation validation failed")?;
200
201		// Check allocation count limit
202		if self.allocations.len() >= self.limits.max_memories {
203			return Err(anyhow::anyhow!(
204				"Maximum number of memory allocations reached: {}",
205				self.limits.max_memories
206			));
207		}
208
209		// Create allocation record
210		let allocation = MemoryAllocation {
211			id:format!("alloc-{}", uuid::Uuid::new_v4()),
212			instance_id:instance_id.to_string(),
213			memory_type:memory_type.to_string(),
214			size_bytes:requested_bytes,
215			max_size_bytes:self.limits.max_memory_bytes() - current_usage,
216			allocated_at:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(),
217			is_shared:false,
218		};
219
220		self.allocations.push(allocation);
221
222		// Update stats
223		Arc::make_mut(&mut self.stats).record_allocation(requested_bytes);
224
225		// Update peak usage
226		let new_peak = self.current_usage_bytes();
227		let current_peak = self.peak_usage.load(Ordering::Relaxed);
228		if new_peak > current_peak {
229			self.peak_usage.store(new_peak, Ordering::Relaxed);
230		}
231
232		debug!("Memory allocated successfully. Total usage: {} MB", self.current_usage_mb());
233
234		Ok(requested_bytes)
235	}
236
237	/// Deallocate memory for a WASM instance
238	#[instrument(skip(self, instance_id))]
239	pub fn deallocate_memory(&mut self, instance_id:&str, memory_id:&str) -> Result<bool> {
240		debug!("Deallocating memory {} for instance {}", memory_id, instance_id);
241
242		let pos = self
243			.allocations
244			.iter()
245			.position(|a| a.instance_id == instance_id && a.id == memory_id);
246
247		if let Some(pos) = pos {
248			let allocation = self.allocations.remove(pos);
249
250			// Update stats
251			Arc::make_mut(&mut self.stats).record_deallocation(allocation.size_bytes);
252
253			debug!(
254				"Memory deallocated successfully. Remaining usage: {} MB",
255				self.current_usage_mb()
256			);
257
258			Ok(true)
259		} else {
260			warn!("Memory allocation not found: {} for instance {}", memory_id, instance_id);
261			Ok(false)
262		}
263	}
264
265	/// Deallocate all memory for an instance
266	#[instrument(skip(self, instance_id))]
267	pub fn deallocate_all_for_instance(&mut self, instance_id:&str) -> usize {
268		debug!("Deallocating all memory for instance {}", instance_id);
269
270		let initial_count = self.allocations.len();
271
272		self.allocations.retain(|a| a.instance_id != instance_id);
273
274		let deallocated_count = initial_count - self.allocations.len();
275
276		if deallocated_count > 0 {
277			debug!(
278				"Deallocated {} memory allocations for instance {}",
279				deallocated_count, instance_id
280			);
281		}
282
283		deallocated_count
284	}
285
286	/// Grow existing memory allocation
287	#[instrument(skip(self, instance_id, memory_id))]
288	pub fn grow_memory(&mut self, instance_id:&str, memory_id:&str, additional_bytes:u64) -> Result<u64> {
289		debug!(
290			"Growing memory {} for instance {} by {} bytes",
291			memory_id, instance_id, additional_bytes
292		);
293
294		// Calculate current usage before mutable borrow
295		let current_usage = self.current_usage_bytes();
296
297		let allocation = self
298			.allocations
299			.iter_mut()
300			.find(|a| a.instance_id == instance_id && a.id == memory_id)
301			.ok_or_else(|| anyhow::anyhow!("Memory allocation not found"))?;
302
303		// Validate against limits
304		self.limits
305			.validate_request(additional_bytes, current_usage)
306			.context("Memory growth validation failed")?;
307
308		allocation.size_bytes += additional_bytes;
309
310		debug!("Memory grown successfully. New size: {} bytes", allocation.size_bytes);
311
312		Ok(allocation.size_bytes)
313	}
314
315	/// Get all allocations for an instance
316	pub fn get_allocations_for_instance(&self, instance_id:&str) -> Vec<&MemoryAllocation> {
317		self.allocations.iter().filter(|a| a.instance_id == instance_id).collect()
318	}
319
320	/// Check if memory limits are exceeded
321	pub fn is_exceeded(&self) -> bool { self.current_usage_bytes() > self.limits.max_memory_bytes() }
322
323	/// Get memory usage percentage
324	pub fn usage_percentage(&self) -> f64 {
325		(self.current_usage_bytes() as f64 / self.limits.max_memory_bytes() as f64) * 100.0
326	}
327
328	/// Reset all allocations and stats (use with caution)
329	pub fn reset(&mut self) {
330		self.allocations.clear();
331		self.stats = Arc::new(MemoryStats::default());
332		self.peak_usage.store(0, Ordering::Relaxed);
333		debug!("Memory manager reset");
334	}
335}
336
337#[cfg(test)]
338mod tests {
339	use super::*;
340
341	#[test]
342	fn test_memory_limits_default() {
343		let limits = MemoryLimits::default();
344		assert_eq!(limits.max_memory_mb, 512);
345		assert_eq!(limits.initial_memory_mb, 64);
346	}
347
348	#[test]
349	fn test_memory_limits_custom() {
350		let limits = MemoryLimits::new(1024, 128, 50);
351		assert_eq!(limits.max_memory_mb, 1024);
352		assert_eq!(limits.initial_memory_mb, 128);
353		assert_eq!(limits.max_instances, 50);
354	}
355
356	#[test]
357	fn test_memory_limits_validation() {
358		let limits = MemoryLimits::new(100, 10, 10);
359
360		// Valid request
361		assert!(limits.validate_request(50, 0).is_ok());
362
363		// Exceeds limit
364		assert!(limits.validate_request(150, 0).is_err());
365		assert!(limits.validate_request(50, 60).is_err());
366	}
367
368	#[test]
369	fn test_memory_manager_creation() {
370		let limits = MemoryLimits::default();
371		let manager = MemoryManagerImpl::new(limits);
372		assert_eq!(manager.current_usage_bytes(), 0);
373		assert_eq!(manager.allocations.len(), 0);
374	}
375
376	#[test]
377	fn test_memory_allocation() {
378		let limits = MemoryLimits::default();
379		let mut manager = MemoryManagerImpl::new(limits);
380
381		let result = manager.allocate_memory("test-instance", "heap", 1024);
382		assert!(result.is_ok());
383		assert_eq!(manager.current_usage_bytes(), 1024);
384		assert_eq!(manager.allocations.len(), 1);
385	}
386
387	#[test]
388	fn test_memory_deallocation() {
389		let limits = MemoryLimits::default();
390		let mut manager = MemoryManagerImpl::new(limits);
391
392		manager.allocate_memory("test-instance", "heap", 1024).unwrap();
393		let allocation = &manager.allocations[0];
394		let memory_id = allocation.id.clone();
395
396		let result = manager.deallocate_memory("test-instance", &memory_id);
397		assert!(result.is_ok());
398		assert_eq!(manager.current_usage_bytes(), 0);
399		assert_eq!(manager.allocations.len(), 0);
400	}
401
402	#[test]
403	fn test_memory_stats() {
404		let mut stats = MemoryStats::default();
405		stats.record_allocation(1024);
406		assert_eq!(stats.allocation_count, 1);
407		assert_eq!(stats.total_allocated, 1024);
408
409		stats.record_deallocation(512);
410		assert_eq!(stats.deallocation_count, 1);
411		assert_eq!(stats.total_allocated, 512);
412	}
413
414	#[test]
415	fn test_memory_usage_percentage() {
416		let limits = MemoryLimits::new(1000, 0, 0);
417		let mut manager = MemoryManagerImpl::new(limits);
418
419		manager.allocate_memory("test", "heap", 500).unwrap();
420		assert_eq!(manager.usage_percentage(), 50.0);
421	}
422}