#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Bytes4 = 4,
Bytes16 = 16,
Bytes64 = 64,
Bytes256 = 256,
Bytes4096 = 4096,
}
impl Alignment {
#[allow(dead_code)]
#[must_use]
pub const fn as_usize(self) -> usize {
self as usize
}
#[allow(dead_code)]
#[must_use]
pub const fn align_up(self, offset: usize) -> usize {
let align = self as usize;
(offset + align - 1) & !(align - 1)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllocationHandle {
pub block_index: usize,
pub offset: usize,
pub size: usize,
pub alignment: usize,
pub id: u64,
}
#[allow(dead_code)]
#[derive(Debug)]
struct FreeRange {
offset: usize,
size: usize,
}
#[allow(dead_code)]
#[derive(Debug)]
struct Block {
capacity: usize,
free_ranges: Vec<FreeRange>,
live_count: usize,
}
impl Block {
fn new(capacity: usize) -> Self {
Self {
capacity,
free_ranges: vec![FreeRange {
offset: 0,
size: capacity,
}],
live_count: 0,
}
}
fn try_alloc(&mut self, size: usize, alignment: usize) -> Option<usize> {
for range in &mut self.free_ranges {
let aligned_offset = (range.offset + alignment - 1) & !(alignment - 1);
let waste = aligned_offset - range.offset;
if range.size >= waste + size {
let result_offset = aligned_offset;
range.offset += waste + size;
range.size -= waste + size;
self.live_count += 1;
return Some(result_offset);
}
}
self.free_ranges.retain(|r| r.size > 0);
None
}
fn free(&mut self, offset: usize, size: usize) {
self.free_ranges.push(FreeRange { offset, size });
if self.live_count > 0 {
self.live_count -= 1;
}
self.coalesce();
}
fn coalesce(&mut self) {
self.free_ranges.sort_by_key(|r| r.offset);
let mut i = 0;
while i + 1 < self.free_ranges.len() {
let end = self.free_ranges[i].offset + self.free_ranges[i].size;
if end >= self.free_ranges[i + 1].offset {
let merged_size = self.free_ranges[i + 1].offset + self.free_ranges[i + 1].size
- self.free_ranges[i].offset;
self.free_ranges[i].size = merged_size;
self.free_ranges.remove(i + 1);
} else {
i += 1;
}
}
}
fn free_bytes(&self) -> usize {
self.free_ranges.iter().map(|r| r.size).sum()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct PoolStats {
pub total_reserved: usize,
pub total_allocated: usize,
pub block_count: usize,
pub alloc_count: u64,
pub free_count: u64,
pub failures: u64,
}
impl PoolStats {
#[allow(dead_code)]
#[must_use]
pub fn free_bytes(&self) -> usize {
self.total_reserved.saturating_sub(self.total_allocated)
}
#[allow(dead_code)]
#[must_use]
pub fn utilisation(&self) -> f64 {
if self.total_reserved == 0 {
0.0
} else {
self.total_allocated as f64 / self.total_reserved as f64
}
}
}
#[allow(dead_code)]
pub struct GpuMemoryPool {
block_size: usize,
blocks: Vec<Block>,
stats: PoolStats,
next_id: u64,
}
impl GpuMemoryPool {
#[allow(dead_code)]
#[must_use]
pub fn new(block_size: usize) -> Self {
assert!(block_size > 0, "block_size must be > 0");
Self {
block_size,
blocks: Vec::new(),
stats: PoolStats::default(),
next_id: 0,
}
}
#[allow(dead_code)]
pub fn alloc(&mut self, size: usize, alignment: Alignment) -> Option<AllocationHandle> {
if size == 0 {
return None;
}
let align = alignment.as_usize();
for (i, block) in self.blocks.iter_mut().enumerate() {
if let Some(offset) = block.try_alloc(size, align) {
let id = self.next_id;
self.next_id += 1;
self.stats.alloc_count += 1;
self.stats.total_allocated += size;
return Some(AllocationHandle {
block_index: i,
offset,
size,
alignment: align,
id,
});
}
}
let new_block_size = self.block_size.max(size + align);
let mut block = Block::new(new_block_size);
if let Some(offset) = block.try_alloc(size, align) {
self.stats.total_reserved += new_block_size;
self.stats.block_count += 1;
let block_index = self.blocks.len();
self.blocks.push(block);
let id = self.next_id;
self.next_id += 1;
self.stats.alloc_count += 1;
self.stats.total_allocated += size;
Some(AllocationHandle {
block_index,
offset,
size,
alignment: align,
id,
})
} else {
self.stats.failures += 1;
None
}
}
#[allow(dead_code)]
pub fn free(&mut self, handle: &AllocationHandle) {
if handle.block_index < self.blocks.len() {
self.blocks[handle.block_index].free(handle.offset, handle.size);
self.stats.total_allocated = self.stats.total_allocated.saturating_sub(handle.size);
self.stats.free_count += 1;
}
}
#[allow(dead_code)]
#[must_use]
pub fn stats(&self) -> &PoolStats {
&self.stats
}
#[allow(dead_code)]
#[must_use]
pub fn block_count(&self) -> usize {
self.blocks.len()
}
#[allow(dead_code)]
#[must_use]
pub fn free_bytes(&self) -> usize {
self.blocks.iter().map(Block::free_bytes).sum()
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.blocks.clear();
self.stats = PoolStats::default();
self.next_id = 0;
}
#[allow(dead_code)]
pub fn defragment(&mut self) -> DefragResult {
let mut ranges_coalesced = 0u64;
let bytes_before_free = self.free_bytes();
for block in &mut self.blocks {
let ranges_before = block.free_ranges.len();
block.coalesce();
let ranges_after = block.free_ranges.len();
if ranges_before > ranges_after {
ranges_coalesced += (ranges_before - ranges_after) as u64;
}
}
let blocks_before = self.blocks.len();
self.blocks.retain(|b| b.live_count > 0);
let blocks_after = self.blocks.len();
let blocks_removed = (blocks_before - blocks_after) as u64;
if blocks_removed > 0 {
self.stats.block_count = self.blocks.len();
self.stats.total_reserved = self.blocks.iter().map(|b| b.capacity).sum();
}
let bytes_after_free = self.free_bytes();
DefragResult {
ranges_coalesced,
blocks_removed,
bytes_recovered: bytes_after_free.saturating_sub(bytes_before_free),
fragmentation_ratio: self.fragmentation_ratio(),
}
}
#[allow(dead_code)]
pub fn compact(&mut self) -> Option<CompactionPlan> {
let defrag = self.defragment();
if self.blocks.len() <= 1 {
return Some(CompactionPlan {
migrations: Vec::new(),
defrag_result: defrag,
});
}
let mut sparse_blocks: Vec<usize> = Vec::new();
for (i, block) in self.blocks.iter().enumerate() {
let used = block.capacity - block.free_bytes();
let util = if block.capacity > 0 {
used as f64 / block.capacity as f64
} else {
1.0
};
if util < 0.5 && block.live_count > 0 {
sparse_blocks.push(i);
}
}
if sparse_blocks.is_empty() {
return Some(CompactionPlan {
migrations: Vec::new(),
defrag_result: defrag,
});
}
let migrations: Vec<MigrationEntry> = sparse_blocks
.iter()
.map(|&block_idx| {
let used = self.blocks[block_idx].capacity - self.blocks[block_idx].free_bytes();
MigrationEntry {
source_block: block_idx,
estimated_bytes: used,
}
})
.collect();
Some(CompactionPlan {
migrations,
defrag_result: defrag,
})
}
#[allow(dead_code)]
#[must_use]
pub fn fragmentation_ratio(&self) -> f64 {
let total_free = self.free_bytes();
if total_free == 0 {
return 0.0;
}
let largest_contiguous: usize = self
.blocks
.iter()
.flat_map(|b| b.free_ranges.iter().map(|r| r.size))
.max()
.unwrap_or(0);
if total_free == 0 {
0.0
} else {
1.0 - (largest_contiguous as f64 / total_free as f64)
}
}
#[allow(dead_code)]
pub fn shrink(&mut self) -> usize {
let before = self.blocks.len();
self.blocks.retain(|b| b.live_count > 0);
let removed = before - self.blocks.len();
if removed > 0 {
self.stats.block_count = self.blocks.len();
self.stats.total_reserved = self.blocks.iter().map(|b| b.capacity).sum();
}
removed
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DefragResult {
pub ranges_coalesced: u64,
pub blocks_removed: u64,
pub bytes_recovered: usize,
pub fragmentation_ratio: f64,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MigrationEntry {
pub source_block: usize,
pub estimated_bytes: usize,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CompactionPlan {
pub migrations: Vec<MigrationEntry>,
pub defrag_result: DefragResult,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_alignment_align_up() {
assert_eq!(Alignment::Bytes16.align_up(0), 0);
assert_eq!(Alignment::Bytes16.align_up(1), 16);
assert_eq!(Alignment::Bytes16.align_up(16), 16);
assert_eq!(Alignment::Bytes16.align_up(17), 32);
}
#[test]
fn test_alignment_as_usize() {
assert_eq!(Alignment::Bytes4.as_usize(), 4);
assert_eq!(Alignment::Bytes256.as_usize(), 256);
}
#[test]
fn test_simple_alloc() {
let mut pool = GpuMemoryPool::new(1024);
let handle = pool.alloc(64, Alignment::Bytes16);
assert!(handle.is_some());
let h = handle.expect("handle should be valid");
assert_eq!(h.size, 64);
assert_eq!(h.offset % 16, 0);
}
#[test]
fn test_zero_size_alloc_returns_none() {
let mut pool = GpuMemoryPool::new(1024);
assert!(pool.alloc(0, Alignment::Bytes4).is_none());
}
#[test]
fn test_alloc_and_free_stats() {
let mut pool = GpuMemoryPool::new(1024);
let h = pool
.alloc(100, Alignment::Bytes4)
.expect("allocation should succeed");
assert_eq!(pool.stats().total_allocated, 100);
pool.free(&h);
assert_eq!(pool.stats().total_allocated, 0);
}
#[test]
fn test_multiple_allocs_same_block() {
let mut pool = GpuMemoryPool::new(4096);
let h1 = pool
.alloc(128, Alignment::Bytes64)
.expect("allocation should succeed");
let h2 = pool
.alloc(128, Alignment::Bytes64)
.expect("allocation should succeed");
assert_eq!(h1.block_index, h2.block_index);
assert_eq!(pool.block_count(), 1);
}
#[test]
fn test_new_block_created_when_full() {
let mut pool = GpuMemoryPool::new(64);
let _h1 = pool
.alloc(64, Alignment::Bytes4)
.expect("allocation should succeed");
let h2 = pool
.alloc(64, Alignment::Bytes4)
.expect("allocation should succeed");
assert!(h2.block_index >= 1 || pool.block_count() == 2);
}
#[test]
fn test_pool_stats_utilisation() {
let mut pool = GpuMemoryPool::new(1000);
pool.alloc(500, Alignment::Bytes4);
let util = pool.stats().utilisation();
assert!(util > 0.0 && util <= 1.0);
}
#[test]
fn test_free_bytes_decreases_after_alloc() {
let mut pool = GpuMemoryPool::new(1024);
pool.alloc(256, Alignment::Bytes4);
assert!(pool.free_bytes() < 1024);
}
#[test]
fn test_reset_clears_all() {
let mut pool = GpuMemoryPool::new(512);
pool.alloc(100, Alignment::Bytes4);
pool.reset();
assert_eq!(pool.block_count(), 0);
assert_eq!(pool.stats().alloc_count, 0);
}
#[test]
fn test_alloc_id_increments() {
let mut pool = GpuMemoryPool::new(1024);
let h1 = pool
.alloc(10, Alignment::Bytes4)
.expect("allocation should succeed");
let h2 = pool
.alloc(10, Alignment::Bytes4)
.expect("allocation should succeed");
assert!(h2.id > h1.id);
}
#[test]
fn test_block_coalescing_after_free() {
let mut pool = GpuMemoryPool::new(256);
let h1 = pool
.alloc(64, Alignment::Bytes4)
.expect("allocation should succeed");
let h2 = pool
.alloc(64, Alignment::Bytes4)
.expect("allocation should succeed");
pool.free(&h1);
pool.free(&h2);
let h3 = pool.alloc(100, Alignment::Bytes4);
assert!(h3.is_some());
}
#[test]
fn test_stats_free_bytes() {
let mut stats = PoolStats {
total_reserved: 1000,
total_allocated: 400,
..Default::default()
};
assert_eq!(stats.free_bytes(), 600);
stats.total_allocated = 1000;
assert_eq!(stats.free_bytes(), 0);
}
#[test]
fn test_defragment_empty_pool() {
let mut pool = GpuMemoryPool::new(1024);
let result = pool.defragment();
assert_eq!(result.ranges_coalesced, 0);
assert_eq!(result.blocks_removed, 0);
}
#[test]
fn test_defragment_removes_empty_blocks() {
let mut pool = GpuMemoryPool::new(256);
let h1 = pool.alloc(256, Alignment::Bytes4).expect("alloc 1");
let _h2 = pool.alloc(256, Alignment::Bytes4).expect("alloc 2");
assert_eq!(pool.block_count(), 2);
pool.free(&h1);
let result = pool.defragment();
assert_eq!(result.blocks_removed, 1);
assert_eq!(pool.block_count(), 1);
}
#[test]
fn test_defragment_coalesces_ranges() {
let mut pool = GpuMemoryPool::new(1024);
let h1 = pool.alloc(64, Alignment::Bytes4).expect("alloc 1");
let h2 = pool.alloc(64, Alignment::Bytes4).expect("alloc 2");
let h3 = pool.alloc(64, Alignment::Bytes4).expect("alloc 3");
pool.free(&h1);
pool.free(&h3);
let frag_before = pool.fragmentation_ratio();
pool.free(&h2);
let result = pool.defragment();
assert!(result.ranges_coalesced > 0 || result.blocks_removed > 0);
let frag_after = pool.fragmentation_ratio();
assert!(
frag_after <= frag_before || frag_after == 0.0,
"fragmentation should not increase: before={frag_before}, after={frag_after}"
);
}
#[test]
fn test_fragmentation_ratio_no_fragmentation() {
let mut pool = GpuMemoryPool::new(1024);
pool.alloc(100, Alignment::Bytes4);
let ratio = pool.fragmentation_ratio();
assert!(ratio == 0.0, "expected 0.0 fragmentation, got {ratio}");
}
#[test]
fn test_fragmentation_ratio_with_holes() {
let mut pool = GpuMemoryPool::new(1024);
let h1 = pool.alloc(100, Alignment::Bytes4).expect("alloc 1");
let _h2 = pool.alloc(100, Alignment::Bytes4).expect("alloc 2");
let h3 = pool.alloc(100, Alignment::Bytes4).expect("alloc 3");
pool.free(&h1);
pool.free(&h3);
let ratio = pool.fragmentation_ratio();
assert!(ratio > 0.0, "should have fragmentation, got {ratio}");
assert!(ratio <= 1.0);
}
#[test]
fn test_shrink_removes_empty_blocks() {
let mut pool = GpuMemoryPool::new(128);
let h1 = pool.alloc(128, Alignment::Bytes4).expect("alloc 1");
let _h2 = pool.alloc(128, Alignment::Bytes4).expect("alloc 2");
pool.free(&h1);
let removed = pool.shrink();
assert_eq!(removed, 1);
assert_eq!(pool.block_count(), 1);
}
#[test]
fn test_compact_returns_plan() {
let mut pool = GpuMemoryPool::new(1024);
let h1 = pool.alloc(100, Alignment::Bytes4).expect("alloc 1");
let _ = h1;
let plan = pool.compact();
assert!(plan.is_some());
}
#[test]
fn test_compact_identifies_sparse_blocks() {
let mut pool = GpuMemoryPool::new(1024);
let h1 = pool.alloc(50, Alignment::Bytes4).expect("alloc");
let _h2 = pool.alloc(1024, Alignment::Bytes4).expect("alloc 2");
let _ = h1;
let plan = pool.compact().expect("compact should succeed");
let has_sparse = plan.migrations.iter().any(|m| m.source_block == 0);
assert!(has_sparse, "block 0 should be identified as sparse");
}
#[test]
fn test_defragment_updates_stats() {
let mut pool = GpuMemoryPool::new(256);
let h1 = pool.alloc(256, Alignment::Bytes4).expect("alloc 1");
let _h2 = pool.alloc(256, Alignment::Bytes4).expect("alloc 2");
pool.free(&h1);
let result = pool.defragment();
assert_eq!(result.blocks_removed, 1);
assert_eq!(pool.stats().block_count, 1);
}
}