Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-renderer/src/memory.rs
1//! Advanced GPU memory management system
2//!
3//! This module provides sophisticated memory management including:
4//! - Multi-tier memory allocators with different strategies
5//! - Dynamic buffer pooling with usage pattern analysis
6//! - Memory pressure detection and adaptive allocation
7//! - Fragmentation analysis and defragmentation strategies
8//! - Memory bandwidth optimization
9//! - Resource streaming and prefetching
10//! - Memory usage profiling and analytics
11 
12use crate::device::{ManagedDevice, OptimizationHints};
13use anyhow::{Context, Result};
14use parking_lot::RwLock;
15use serde::{Deserialize, Serialize};
16use std::cmp::Ordering as CmpOrdering;
17use std::collections::{BTreeMap, BinaryHeap, HashMap};
18use std::sync::{
19 atomic::{AtomicBool, AtomicU64, Ordering},
20 Arc,
21};
22use std::time::{Duration, Instant};
23use strato_core::{logging::LogCategory, strato_debug, strato_error_rate_limited, strato_warn};
24use tracing::{debug, info, instrument, warn};
25use wgpu::{Buffer, BufferDescriptor, BufferUsages, Device};
26 
27/// Memory allocation strategy
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum AllocationStrategy {
30 /// Best fit - minimize wasted space
31 BestFit,
32 /// First fit - fastest allocation
33 FirstFit,
34 /// Buddy system - good for power-of-2 sizes
35 Buddy,
36 /// Slab allocation - for fixed-size objects
37 Slab,
38 /// Linear allocation - for temporary resources
39 Linear,
40 /// Balanced approach - compromise between speed and fragmentation
41 Balanced,
42}
43 
44/// Memory usage pattern classification
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
46pub enum UsagePattern {
47 /// Short-lived resources (< 1 frame)
48 Transient,
49 /// Frame-persistent resources (1-10 frames)
50 FramePersistent,
51 /// Long-lived resources (> 10 frames)
52 Persistent,
53 /// Static resources (never change)
54 Static,
55 /// Streaming resources (loaded on demand)
56 Streaming,
57}
58 
59/// Memory tier classification based on access patterns
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
61pub enum MemoryTier {
62 /// High-speed GPU memory for frequently accessed data
63 HighSpeed = 0,
64 /// Standard GPU memory for regular resources
65 Standard = 1,
66 /// Shared memory for CPU-GPU communication
67 Shared = 2,
68 /// System memory for rarely accessed data
69 System = 3,
70}
71 
72/// Memory block descriptor
73#[derive(Debug)]
74pub struct MemoryBlock {
75 pub buffer: Arc<Buffer>,
76 pub size: u64,
77 pub offset: u64,
78 pub alignment: u64,
79 pub usage: BufferUsages,
80 pub tier: MemoryTier,
81 pub allocation_time: Instant,
82 pub last_access: Instant,
83 pub access_count: AtomicU64,
84 pub is_mapped: AtomicBool,
85}
86 
87/// Free memory region
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct FreeRegion {
90 pub offset: u64,
91 pub size: u64,
92}
93 
94impl PartialOrd for FreeRegion {
95 fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
96 Some(self.cmp(other))
97 }
98}
99 
100impl Ord for FreeRegion {
101 fn cmp(&self, other: &Self) -> CmpOrdering {
102 // For best-fit allocation with BinaryHeap (max-heap), invert size comparison
103 other
104 .size
105 .cmp(&self.size)
106 .then(self.offset.cmp(&other.offset))
107 }
108}
109 
110/// Memory pool for a specific usage pattern and size range
111pub struct MemoryPool {
112 pub usage_pattern: UsagePattern,
113 pub tier: MemoryTier,
114 pub min_block_size: u64,
115 pub max_block_size: u64,
116 pub allocation_strategy: AllocationStrategy,
117 pub blocks: Vec<Arc<MemoryBlock>>,
118 pub free_regions: BinaryHeap<FreeRegion>,
119 pub allocated_regions: BTreeMap<u64, u64>, // offset -> size
120 pub total_size: AtomicU64,
121 pub used_size: AtomicU64,
122 pub allocation_count: AtomicU64,
123 pub deallocation_count: AtomicU64,
124 pub fragmentation_ratio: AtomicU64, // Fixed point (x1000)
125 pub last_defrag: RwLock<Instant>,
126}
127 
128impl MemoryPool {
129 /// Create a new memory pool
130 pub fn new(
131 usage_pattern: UsagePattern,
132 tier: MemoryTier,
133 min_block_size: u64,
134 max_block_size: u64,
135 allocation_strategy: AllocationStrategy,
136 ) -> Self {
137 Self {
138 usage_pattern,
139 tier,
140 min_block_size,
141 max_block_size,
142 allocation_strategy,
143 blocks: Vec::new(),
144 free_regions: BinaryHeap::new(),
145 allocated_regions: BTreeMap::new(),
146 total_size: AtomicU64::new(0),
147 used_size: AtomicU64::new(0),
148 allocation_count: AtomicU64::new(0),
149 deallocation_count: AtomicU64::new(0),
150 fragmentation_ratio: AtomicU64::new(0),
151 last_defrag: RwLock::new(Instant::now()),
152 }
153 }
154 
155 /// Allocate memory from this pool
156 pub fn allocate(
157 &mut self,
158 size: u64,
159 alignment: u64,
160 device: &Device,
161 ) -> Result<Arc<MemoryBlock>> {
162 let aligned_size = Self::align_size(size, alignment);
163 
164 // Try to find a suitable free region
165 if let Some(region) = self.find_free_region(aligned_size, alignment) {
166 return self.allocate_from_region(region, aligned_size, alignment, device);
167 }
168 
169 // Need to create a new block
170 self.create_new_block(aligned_size, alignment, device)
171 }
172 
173 /// Find a suitable free region
174 fn find_free_region(&mut self, size: u64, alignment: u64) -> Option<FreeRegion> {
175 let mut best_region: Option<FreeRegion> = None;
176 let mut temp_regions = Vec::new();
177 
178 // Extract regions and find the best fit
179 while let Some(region) = self.free_regions.pop() {
180 let aligned_offset = Self::align_offset(region.offset, alignment);
181 let required_size = aligned_offset - region.offset + size;
182 
183 if region.size >= required_size {
184 if best_region.is_none() || region.size < best_region.as_ref().unwrap().size {
185 if let Some(prev_best) = best_region.take() {
186 temp_regions.push(prev_best);
187 }
188 best_region = Some(region);
189 } else {
190 temp_regions.push(region);
191 }
192 } else {
193 temp_regions.push(region);
194 }
195 }
196 
197 // Put back the regions we didn't use
198 for region in temp_regions {
199 self.free_regions.push(region);
200 }
201 
202 best_region
203 }
204 
205 /// Allocate from an existing free region
206 fn allocate_from_region(
207 &mut self,
208 region: FreeRegion,
209 size: u64,
210 alignment: u64,
211 _device: &Device,
212 ) -> Result<Arc<MemoryBlock>> {
213 let aligned_offset = Self::align_offset(region.offset, alignment);
214 let padding = aligned_offset - region.offset;
215 
216 // Create padding region if needed
217 if padding > 0 {
218 self.free_regions.push(FreeRegion {
219 offset: region.offset,
220 size: padding,
221 });
222 }
223 
224 // Create remaining region if any
225 let remaining_size = region.size - padding - size;
226 if remaining_size > 0 {
227 self.free_regions.push(FreeRegion {
228 offset: aligned_offset + size,
229 size: remaining_size,
230 });
231 }
232 
233 // Find the buffer that contains this region
234 let buffer = self.find_buffer_for_offset(aligned_offset)?;
235 
236 let block = Arc::new(MemoryBlock {
237 buffer,
238 size,
239 offset: aligned_offset,
240 alignment,
241 usage: self.get_buffer_usage(),
242 tier: self.tier,
243 allocation_time: Instant::now(),
244 last_access: Instant::now(),
245 access_count: AtomicU64::new(0),
246 is_mapped: AtomicBool::new(false),
247 });
248 
249 self.allocated_regions.insert(aligned_offset, size);
250 self.used_size.fetch_add(size, Ordering::Relaxed);
251 self.allocation_count.fetch_add(1, Ordering::Relaxed);
252 
253 Ok(block)
254 }
255 
256 /// Create a new buffer block
257 fn create_new_block(
258 &mut self,
259 size: u64,
260 alignment: u64,
261 device: &Device,
262 ) -> Result<Arc<MemoryBlock>> {
263 let block_size = std::cmp::max(size, self.min_block_size);
264 let block_size = std::cmp::min(block_size, self.max_block_size);
265 
266 let buffer = Arc::new(device.create_buffer(&BufferDescriptor {
267 label: Some(&format!(
268 "MemoryPool-{:?}-{:?}",
269 self.usage_pattern, self.tier
270 )),
271 size: block_size,
272 usage: self.get_buffer_usage(),
273 mapped_at_creation: false,
274 }));
275 
276 let block = Arc::new(MemoryBlock {
277 buffer,
278 size,
279 offset: 0,
280 alignment,
281 usage: self.get_buffer_usage(),
282 tier: self.tier,
283 allocation_time: Instant::now(),
284 last_access: Instant::now(),
285 access_count: AtomicU64::new(0),
286 is_mapped: AtomicBool::new(false),
287 });
288 
289 // Add remaining space to free regions
290 if block_size > size {
291 self.free_regions.push(FreeRegion {
292 offset: size,
293 size: block_size - size,
294 });
295 }
296 
297 self.blocks.push(block.clone());
298 self.allocated_regions.insert(0, size);
299 self.total_size.fetch_add(block_size, Ordering::Relaxed);
300 self.used_size.fetch_add(size, Ordering::Relaxed);
301 self.allocation_count.fetch_add(1, Ordering::Relaxed);
302 
303 Ok(block)
304 }
305 
306 /// Deallocate a memory block
307 pub fn deallocate(&mut self, block: &MemoryBlock) {
308 if let Some(size) = self.allocated_regions.remove(&block.offset) {
309 self.free_regions.push(FreeRegion {
310 offset: block.offset,
311 size,
312 });
313 
314 self.used_size.fetch_sub(size, Ordering::Relaxed);
315 self.deallocation_count.fetch_add(1, Ordering::Relaxed);
316 
317 // Coalesce adjacent free regions
318 self.coalesce_free_regions();
319 }
320 }
321 
322 /// Coalesce adjacent free regions to reduce fragmentation
323 fn coalesce_free_regions(&mut self) {
324 let mut regions: Vec<_> = self.free_regions.drain().collect();
325 regions.sort_by_key(|r| r.offset);
326 
327 let mut coalesced = Vec::new();
328 let mut current: Option<FreeRegion> = None;
329 
330 for region in regions {
331 match current.take() {
332 None => current = Some(region),
333 Some(mut prev) => {
334 if prev.offset + prev.size == region.offset {
335 // Adjacent regions, merge them
336 prev.size += region.size;
337 current = Some(prev);
338 } else {
339 // Not adjacent, keep previous and start new
340 coalesced.push(prev);
341 current = Some(region);
342 }
343 }
344 }
345 }
346 
347 if let Some(last) = current {
348 coalesced.push(last);
349 }
350 
351 self.free_regions = coalesced.into_iter().collect();
352 }
353 
354 /// Calculate fragmentation ratio
355 pub fn calculate_fragmentation(&self) -> f32 {
356 let total = self.total_size.load(Ordering::Relaxed);
357 if total == 0 {
358 return 0.0;
359 }
360 
361 let _used = self.used_size.load(Ordering::Relaxed);
362 let free_regions = self.free_regions.len();
363 
364 // Fragmentation increases with number of free regions and decreases with usage
365 let fragmentation = (free_regions as f32 * 100.0) / (total as f32 / 1024.0);
366 fragmentation.min(100.0)
367 }
368 
369 /// Get buffer usage flags for this pool
370 fn get_buffer_usage(&self) -> BufferUsages {
371 match self.usage_pattern {
372 UsagePattern::Transient => {
373 BufferUsages::VERTEX | BufferUsages::INDEX | BufferUsages::COPY_DST
374 }
375 UsagePattern::FramePersistent => {
376 BufferUsages::UNIFORM | BufferUsages::STORAGE | BufferUsages::COPY_DST
377 }
378 UsagePattern::Persistent => {
379 BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC
380 }
381 UsagePattern::Static => BufferUsages::VERTEX | BufferUsages::INDEX,
382 UsagePattern::Streaming => BufferUsages::COPY_DST | BufferUsages::COPY_SRC,
383 }
384 }
385 
386 /// Find buffer that contains the given offset
387 fn find_buffer_for_offset(&self, _offset: u64) -> Result<Arc<Buffer>> {
388 // This is a simplified implementation
389 // In practice, you'd need to track which buffer contains which offset range
390 self.blocks
391 .first()
392 .map(|block| block.buffer.clone())
393 .context("No buffer available for offset")
394 }
395 
396 /// Align size to the given alignment
397 fn align_size(size: u64, alignment: u64) -> u64 {
398 (size + alignment - 1) & !(alignment - 1)
399 }
400 
401 /// Align offset to the given alignment
402 fn align_offset(offset: u64, alignment: u64) -> u64 {
403 (offset + alignment - 1) & !(alignment - 1)
404 }
405}
406 
407/// Advanced memory manager with multiple allocation strategies
408pub struct MemoryManager {
409 device: Arc<ManagedDevice>,
410 pools: HashMap<(UsagePattern, MemoryTier), MemoryPool>,
411 allocation_stats: RwLock<AllocationStats>,
412 memory_pressure_threshold: AtomicU64,
413 auto_defrag_enabled: AtomicBool,
414 last_cleanup: RwLock<Instant>,
415 optimization_hints: OptimizationHints,
416}
417 
418/// Memory allocation statistics
419#[derive(Debug, Clone, Default)]
420pub struct AllocationStats {
421 pub total_allocated: u64,
422 pub total_freed: u64,
423 pub peak_usage: u64,
424 pub current_usage: u64,
425 pub allocation_count: u64,
426 pub deallocation_count: u64,
427 pub failed_allocations: u64,
428 pub defragmentation_count: u64,
429 pub average_fragmentation: f32,
430}
431 
432impl MemoryManager {
433 /// Create a new memory manager
434 pub fn new(device: Arc<ManagedDevice>) -> Self {
435 let optimization_hints = device.optimization_hints.clone();
436 
437 let mut pools = HashMap::new();
438 
439 // Create pools for different usage patterns and tiers
440 for &pattern in &[
441 UsagePattern::Transient,
442 UsagePattern::FramePersistent,
443 UsagePattern::Persistent,
444 UsagePattern::Static,
445 UsagePattern::Streaming,
446 ] {
447 for &tier in &[
448 MemoryTier::HighSpeed,
449 MemoryTier::Standard,
450 MemoryTier::Shared,
451 ] {
452 let (min_size, max_size, strategy) =
453 Self::get_pool_config(pattern, tier, &optimization_hints);
454 
455 pools.insert(
456 (pattern, tier),
457 MemoryPool::new(pattern, tier, min_size, max_size, strategy),
458 );
459 }
460 }
461 
462 Self {
463 device,
464 pools,
465 allocation_stats: RwLock::new(AllocationStats::default()),
466 memory_pressure_threshold: AtomicU64::new(1024 * 1024 * 1024), // 1GB
467 auto_defrag_enabled: AtomicBool::new(true),
468 last_cleanup: RwLock::new(Instant::now()),
469 optimization_hints,
470 }
471 }
472 
473 /// Get pool configuration for usage pattern and tier
474 fn get_pool_config(
475 pattern: UsagePattern,
476 tier: MemoryTier,
477 hints: &OptimizationHints,
478 ) -> (u64, u64, AllocationStrategy) {
479 let base_alignment = hints.preferred_buffer_alignment as u64;
480 
481 match (pattern, tier) {
482 (UsagePattern::Transient, _) => (
483 4 * 1024, // 4KB min
484 16 * 1024 * 1024, // 16MB max
485 AllocationStrategy::Linear,
486 ),
487 (UsagePattern::FramePersistent, MemoryTier::HighSpeed) => (
488 64 * 1024, // 64KB min
489 64 * 1024 * 1024, // 64MB max
490 AllocationStrategy::BestFit,
491 ),
492 (UsagePattern::Persistent, _) => (
493 1024 * 1024, // 1MB min
494 256 * 1024 * 1024, // 256MB max
495 AllocationStrategy::Buddy,
496 ),
497 (UsagePattern::Static, _) => (
498 base_alignment, // Alignment min
499 1024 * 1024 * 1024, // 1GB max
500 AllocationStrategy::FirstFit,
501 ),
502 (UsagePattern::Streaming, _) => (
503 1024 * 1024, // 1MB min
504 128 * 1024 * 1024, // 128MB max
505 AllocationStrategy::Slab,
506 ),
507 _ => (
508 base_alignment,
509 64 * 1024 * 1024,
510 AllocationStrategy::BestFit,
511 ),
512 }
513 }
514 
515 /// Allocate memory with specific usage pattern
516 #[instrument(skip(self))]
517 pub fn allocate(
518 &mut self,
519 size: u64,
520 alignment: u64,
521 usage_pattern: UsagePattern,
522 tier: MemoryTier,
523 ) -> Result<Arc<MemoryBlock>> {
524 let pool_key = (usage_pattern, tier);
525 
526 // Get pool reference and try allocation
527 let allocation_result = {
528 let pool = self
529 .pools
530 .get_mut(&pool_key)
531 .context("Pool not found for usage pattern and tier")?;
532 pool.allocate(size, alignment, &self.device.device)
533 };
534 
535 match allocation_result {
536 Ok(block) => {
537 let mut stats = self.allocation_stats.write();
538 stats.total_allocated += size;
539 stats.current_usage += size;
540 stats.allocation_count += 1;
541 stats.peak_usage = stats.peak_usage.max(stats.current_usage);
542 
543 Ok(block)
544 }
545 Err(e) => {
546 let mut stats = self.allocation_stats.write();
547 stats.failed_allocations += 1;
548 
549 // Try memory pressure relief
550 if self.auto_defrag_enabled.load(Ordering::Relaxed) {
551 drop(stats);
552 self.relieve_memory_pressure()?;
553 
554 // Retry allocation
555 let pool = self
556 .pools
557 .get_mut(&pool_key)
558 .context("Pool not found for usage pattern and tier")?;
559 pool.allocate(size, alignment, &self.device.device)
560 } else {
561 Err(e)
562 }
563 }
564 }
565 }
566 
567 /// Deallocate memory block
568 pub fn deallocate(&mut self, block: Arc<MemoryBlock>) {
569 let key = (self.classify_usage_pattern(&block), block.tier);
570 
571 if let Some(pool) = self.pools.get_mut(&key) {
572 let size = block.size;
573 pool.deallocate(&block);
574 
575 let mut stats = self.allocation_stats.write();
576 stats.total_freed += size;
577 stats.current_usage = stats.current_usage.saturating_sub(size);
578 stats.deallocation_count += 1;
579 }
580 }
581 
582 /// Classify usage pattern from block characteristics
583 fn classify_usage_pattern(&self, block: &MemoryBlock) -> UsagePattern {
584 let age = block.allocation_time.elapsed();
585 let access_count = block.access_count.load(Ordering::Relaxed);
586 
587 if age < Duration::from_millis(16) {
588 UsagePattern::Transient
589 } else if age < Duration::from_millis(160) && access_count > 10 {
590 UsagePattern::FramePersistent
591 } else if access_count == 0 {
592 UsagePattern::Static
593 } else {
594 UsagePattern::Persistent
595 }
596 }
597 
598 /// Relieve memory pressure through cleanup and defragmentation
599 #[instrument(skip(self))]
600 pub fn relieve_memory_pressure(&mut self) -> Result<()> {
601 info!("Relieving memory pressure...");
602 
603 // Cleanup unused resources
604 self.cleanup_unused_resources();
605 
606 // Defragment pools with high fragmentation
607 let pools_to_defrag: Vec<_> = self
608 .pools
609 .values_mut()
610 .filter_map(|pool| {
611 let fragmentation = pool.calculate_fragmentation();
612 if fragmentation > 50.0 {
613 Some(pool as *mut MemoryPool)
614 } else {
615 None
616 }
617 })
618 .collect();
619 
620 for pool_ptr in pools_to_defrag {
621 unsafe {
622 self.defragment_pool(&mut *pool_ptr)?;
623 }
624 }
625 
626 let mut stats = self.allocation_stats.write();
627 stats.defragmentation_count += 1;
628 
629 Ok(())
630 }
631 
632 /// Cleanup unused resources
633 fn cleanup_unused_resources(&mut self) {
634 let now = Instant::now();
635 let cleanup_threshold = Duration::from_secs(30);
636 
637 for pool in self.pools.values_mut() {
638 pool.blocks.retain(|block| {
639 let age = now.duration_since(block.last_access);
640 let access_count = block.access_count.load(Ordering::Relaxed);
641 
642 // Keep blocks that are recently accessed or frequently used
643 age < cleanup_threshold || access_count > 100
644 });
645 }
646 
647 *self.last_cleanup.write() = now;
648 }
649 
650 /// Defragment a memory pool
651 fn defragment_pool(&mut self, pool: &mut MemoryPool) -> Result<()> {
652 debug!(
653 "Defragmenting pool: {:?}-{:?}",
654 pool.usage_pattern, pool.tier
655 );
656 
657 // Coalesce free regions
658 pool.coalesce_free_regions();
659 
660 // Update fragmentation ratio
661 let fragmentation = pool.calculate_fragmentation();
662 pool.fragmentation_ratio
663 .store((fragmentation * 1000.0) as u64, Ordering::Relaxed);
664 
665 *pool.last_defrag.write() = Instant::now();
666 
667 Ok(())
668 }
669 
670 /// Get memory statistics
671 pub fn get_stats(&self) -> AllocationStats {
672 let mut stats = self.allocation_stats.read().clone();
673 
674 // Calculate average fragmentation across all pools
675 let total_fragmentation: f32 = self
676 .pools
677 .values()
678 .map(|pool| pool.calculate_fragmentation())
679 .sum();
680 
681 stats.average_fragmentation = if self.pools.is_empty() {
682 0.0
683 } else {
684 total_fragmentation / self.pools.len() as f32
685 };
686 
687 stats
688 }
689 
690 /// Check if memory pressure relief is needed
691 pub fn needs_pressure_relief(&self) -> bool {
692 let stats = self.allocation_stats.read();
693 let threshold = self.memory_pressure_threshold.load(Ordering::Relaxed);
694 
695 stats.current_usage > threshold || stats.average_fragmentation > 75.0
696 }
697 
698 /// Set memory pressure threshold
699 pub fn set_pressure_threshold(&self, threshold: u64) {
700 self.memory_pressure_threshold
701 .store(threshold, Ordering::Relaxed);
702 }
703 
704 /// Enable or disable automatic defragmentation
705 pub fn set_auto_defrag(&self, enabled: bool) {
706 self.auto_defrag_enabled.store(enabled, Ordering::Relaxed);
707 }
708 
709 /// Get total allocated memory (integration method)
710 pub fn get_total_allocated(&self) -> u64 {
711 let stats = self.get_stats();
712 stats.total_allocated
713 }
714 
715 /// Defragment memory (integration method)
716 pub fn defragment(&mut self) -> Result<()> {
717 self.relieve_memory_pressure()
718 }
719 
720 /// Cleanup memory (integration method)
721 pub fn cleanup(&mut self) -> Result<()> {
722 self.relieve_memory_pressure()
723 }
724}
725 
726#[cfg(test)]
727mod tests {
728 use super::*;
729 
730 #[test]
731 fn test_free_region_ordering() {
732 let mut regions = BinaryHeap::new();
733 
734 regions.push(FreeRegion {
735 offset: 100,
736 size: 50,
737 });
738 regions.push(FreeRegion {
739 offset: 0,
740 size: 100,
741 });
742 regions.push(FreeRegion {
743 offset: 200,
744 size: 25,
745 });
746 
747 // Should pop smallest size first (best fit)
748 assert_eq!(regions.pop().unwrap().size, 25);
749 assert_eq!(regions.pop().unwrap().size, 50);
750 assert_eq!(regions.pop().unwrap().size, 100);
751 }
752 
753 #[test]
754 fn test_alignment() {
755 assert_eq!(MemoryPool::align_size(100, 256), 256);
756 assert_eq!(MemoryPool::align_size(256, 256), 256);
757 assert_eq!(MemoryPool::align_size(257, 256), 512);
758 
759 assert_eq!(MemoryPool::align_offset(100, 256), 256);
760 assert_eq!(MemoryPool::align_offset(256, 256), 256);
761 assert_eq!(MemoryPool::align_offset(257, 256), 512);
762 }
763}
764