Skip to content

Commit 641624a

Browse files
committed
feat: try_release_virtual_memory_buckets
1 parent 652d11b commit 641624a

File tree

2 files changed

+637
-10
lines changed

2 files changed

+637
-10
lines changed

src/memory_manager.rs

Lines changed: 201 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,17 @@ const HEADER_RESERVED_BYTES: usize = 32;
127127
/// Bucket MAX_NUM_BUCKETS ↕ N pages
128128
/// ```
129129
///
130+
/// # Memory Reclamation
131+
///
132+
/// Buckets can be manually released for reuse via `try_release_virtual_memory_buckets()`, which
133+
/// safely checks that virtual memory size is 0 before releasing. Released buckets are automatically
134+
/// reused when new virtual memories require storage.
135+
///
130136
/// # Current Limitations and Future Improvements
131137
///
132-
/// - Buckets are never deallocated once assigned to a memory ID, even when the memory becomes empty
133-
/// - Clearing data structures (BTreeMap, Vec) does not reclaim underlying bucket allocations
134-
/// - Long-running canisters may accumulate unused buckets, increasing storage costs
138+
/// - Manual bucket release is required - clearing data structures does not automatically reclaim buckets
139+
/// - Virtual memory size must be 0 for safe bucket release (data structures don't auto-shrink on clear)
140+
/// - Long-running canisters should periodically release unused buckets to optimize storage costs
135141
/// - Future versions may consider automatic bucket reclamation and memory compaction features
136142
pub struct MemoryManager<M: Memory> {
137143
inner: Rc<RefCell<MemoryManagerInner<M>>>,
@@ -162,6 +168,38 @@ impl<M: Memory> MemoryManager<M> {
162168
}
163169
}
164170

171+
/// Safely releases buckets only if data structures using this memory appear to be empty.
172+
///
173+
/// This method examines the actual state of data structures to determine if bucket release
174+
/// is safe, rather than relying solely on virtual memory size (which rarely shrinks to 0).
175+
///
176+
/// Returns:
177+
/// - `Ok(bucket_count)` if buckets were successfully released
178+
/// - `Err(reason)` with a descriptive message explaining why release was denied
179+
///
180+
/// Supported data structures:
181+
/// - BTreeMap/BTreeSet: Checks if allocator reports 0 allocated chunks and length is 0
182+
/// - Vec: Checks if length is 0
183+
/// - Empty memory: Always safe to release
184+
///
185+
/// # Examples
186+
/// ```rust,ignore
187+
/// let memory_manager = MemoryManager::init(DefaultMemoryImpl::default());
188+
/// let memory_id = MemoryId::new(0);
189+
///
190+
/// // After clearing a data structure
191+
/// map.clear_new();
192+
/// match memory_manager.try_release_virtual_memory_buckets(memory_id) {
193+
/// Ok(count) => println!("Released {} buckets", count),
194+
/// Err(reason) => println!("Cannot release: {}", reason),
195+
/// }
196+
/// ```
197+
pub fn try_release_virtual_memory_buckets(&self, id: MemoryId) -> Result<usize, String> {
198+
self.inner
199+
.borrow_mut()
200+
.try_release_virtual_memory_buckets(id)
201+
}
202+
165203
/// Returns the underlying memory.
166204
///
167205
/// # Returns
@@ -246,6 +284,9 @@ struct MemoryManagerInner<M: Memory> {
246284

247285
/// A map mapping each managed memory to the bucket ids that are allocated to it.
248286
memory_buckets: Vec<Vec<BucketId>>,
287+
288+
/// A pool of free buckets that can be reused by any memory.
289+
free_buckets: Vec<BucketId>,
249290
}
250291

251292
impl<M: Memory> MemoryManagerInner<M> {
@@ -274,6 +315,7 @@ impl<M: Memory> MemoryManagerInner<M> {
274315
memory_sizes_in_pages: [0; MAX_NUM_MEMORIES as usize],
275316
memory_buckets: vec![vec![]; MAX_NUM_MEMORIES as usize],
276317
bucket_size_in_pages,
318+
free_buckets: Vec::new(),
277319
};
278320

279321
mem_mgr.save_header();
@@ -303,9 +345,13 @@ impl<M: Memory> MemoryManagerInner<M> {
303345
);
304346

305347
let mut memory_buckets = vec![vec![]; MAX_NUM_MEMORIES as usize];
306-
for (bucket_idx, memory) in buckets.into_iter().enumerate() {
307-
if memory != UNALLOCATED_BUCKET_MARKER {
308-
memory_buckets[memory as usize].push(BucketId(bucket_idx as u16));
348+
let mut free_buckets = Vec::new();
349+
for (bucket_idx, memory_id) in buckets.into_iter().enumerate() {
350+
if memory_id != UNALLOCATED_BUCKET_MARKER {
351+
memory_buckets[memory_id as usize].push(BucketId(bucket_idx as u16));
352+
} else if (bucket_idx as u16) < header.num_allocated_buckets {
353+
// This bucket was allocated but is now marked as unallocated, so it's free to reuse
354+
free_buckets.push(BucketId(bucket_idx as u16));
309355
}
310356
}
311357

@@ -315,6 +361,7 @@ impl<M: Memory> MemoryManagerInner<M> {
315361
bucket_size_in_pages: header.bucket_size_in_pages,
316362
memory_sizes_in_pages: header.memory_sizes_in_pages,
317363
memory_buckets,
364+
free_buckets,
318365
}
319366
}
320367

@@ -345,7 +392,11 @@ impl<M: Memory> MemoryManagerInner<M> {
345392
let required_buckets = self.num_buckets_needed(new_size);
346393
let new_buckets_needed = required_buckets - current_buckets;
347394

348-
if new_buckets_needed + self.allocated_buckets as u64 > MAX_NUM_BUCKETS {
395+
// Check if we have enough buckets available (either already allocated or can allocate new ones)
396+
let available_free_buckets = self.free_buckets.len() as u64;
397+
let new_buckets_to_allocate = new_buckets_needed.saturating_sub(available_free_buckets);
398+
399+
if self.allocated_buckets as u64 + new_buckets_to_allocate > MAX_NUM_BUCKETS {
349400
// Exceeded the memory that can be managed.
350401
return -1;
351402
}
@@ -354,7 +405,16 @@ impl<M: Memory> MemoryManagerInner<M> {
354405
// Allocate new buckets as needed.
355406
memory_bucket.reserve(new_buckets_needed as usize);
356407
for _ in 0..new_buckets_needed {
357-
let new_bucket_id = BucketId(self.allocated_buckets);
408+
let new_bucket_id = if let Some(free_bucket) = self.free_buckets.pop() {
409+
// Reuse a bucket from the free pool
410+
free_bucket
411+
} else {
412+
// Allocate a new bucket
413+
let bucket = BucketId(self.allocated_buckets);
414+
self.allocated_buckets += 1;
415+
bucket
416+
};
417+
358418
memory_bucket.push(new_bucket_id);
359419

360420
// Write in stable store that this bucket belongs to the memory with the provided `id`.
@@ -363,8 +423,6 @@ impl<M: Memory> MemoryManagerInner<M> {
363423
bucket_allocations_address(new_bucket_id).get(),
364424
&[id.0],
365425
);
366-
367-
self.allocated_buckets += 1;
368426
}
369427

370428
// Grow the underlying memory if necessary.
@@ -386,6 +444,136 @@ impl<M: Memory> MemoryManagerInner<M> {
386444
old_size as i64
387445
}
388446

447+
/// Safely releases buckets only if data structures using this memory appear to be empty.
448+
///
449+
/// This method examines the actual state of data structures to determine if bucket release
450+
/// is safe, rather than relying solely on virtual memory size (which rarely shrinks to 0).
451+
///
452+
/// Returns:
453+
/// - `Ok(bucket_count)` if buckets were successfully released
454+
/// - `Err(reason)` with a descriptive message explaining why release was denied
455+
fn try_release_virtual_memory_buckets(&mut self, id: MemoryId) -> Result<usize, String> {
456+
let current_size = self.memory_sizes_in_pages[id.0 as usize];
457+
let virtual_memory_buckets = &self.memory_buckets[id.0 as usize];
458+
459+
if virtual_memory_buckets.is_empty() {
460+
// No buckets allocated to this memory, nothing to release
461+
return Ok(0);
462+
}
463+
464+
if current_size == 0 {
465+
// Memory size is 0, definitely safe to release buckets
466+
return self.release_virtual_memory_buckets_unchecked(id);
467+
}
468+
469+
// Read the magic bytes from the first bucket to identify the data structure
470+
let first_bucket_addr = self.bucket_address(&virtual_memory_buckets[0]);
471+
let mut magic = [0u8; 3];
472+
self.memory.read(first_bucket_addr.get(), &mut magic);
473+
474+
match &magic {
475+
b"BTR" => {
476+
// BTreeMap detected - check if it's empty
477+
self.check_btreemap_empty(id, first_bucket_addr)
478+
}
479+
b"SVC" => {
480+
// Vec detected - check if it's empty
481+
self.check_vec_empty(id, first_bucket_addr)
482+
}
483+
_ => {
484+
// Check if it's uninitialized memory (all zeros)
485+
let mut sample = [0u8; 32];
486+
self.memory.read(first_bucket_addr.get(), &mut sample);
487+
488+
if sample.iter().all(|&b| b == 0) {
489+
// Appears to be uninitialized memory, probably safe
490+
self.release_virtual_memory_buckets_unchecked(id)
491+
} else {
492+
Err(format!(
493+
"Unknown data structure with magic {:?} - cannot verify if empty. \
494+
Use explicit release method if you're certain it's safe.",
495+
magic
496+
))
497+
}
498+
}
499+
}
500+
}
501+
502+
/// Check if a BTreeMap appears to be empty by examining its header
503+
fn check_btreemap_empty(&mut self, id: MemoryId, base_addr: Address) -> Result<usize, String> {
504+
// Read BTreeMap length (at offset 20 in the header)
505+
let mut len_bytes = [0u8; 8];
506+
self.memory.read(base_addr.get() + 20, &mut len_bytes);
507+
let btree_len = u64::from_le_bytes(len_bytes);
508+
509+
if btree_len > 0 {
510+
return Err(format!("BTreeMap contains {} items", btree_len));
511+
}
512+
513+
// Check allocator state (at ALLOCATOR_OFFSET = 52)
514+
// The allocator header starts with magic [3 bytes] + version [1 byte] + alignment [4 bytes]
515+
// Then allocation_size [8 bytes] + num_allocated_chunks [8 bytes] at offset 16
516+
let mut chunks_bytes = [0u8; 8];
517+
self.memory
518+
.read(base_addr.get() + 52 + 16, &mut chunks_bytes);
519+
let allocated_chunks = u64::from_le_bytes(chunks_bytes);
520+
521+
if allocated_chunks > 0 {
522+
return Err(format!(
523+
"BTreeMap allocator has {} allocated chunks (not empty)",
524+
allocated_chunks
525+
));
526+
}
527+
528+
// BTreeMap appears empty, safe to release
529+
self.release_virtual_memory_buckets_unchecked(id)
530+
}
531+
532+
/// Check if a Vec appears to be empty by examining its header
533+
fn check_vec_empty(&mut self, id: MemoryId, base_addr: Address) -> Result<usize, String> {
534+
// Read Vec length (at offset 4 in the header)
535+
let mut len_bytes = [0u8; 8];
536+
self.memory.read(base_addr.get() + 4, &mut len_bytes);
537+
let vec_len = u64::from_le_bytes(len_bytes);
538+
539+
if vec_len > 0 {
540+
return Err(format!("Vec contains {} items", vec_len));
541+
}
542+
543+
// Vec appears empty, safe to release
544+
self.release_virtual_memory_buckets_unchecked(id)
545+
}
546+
547+
/// Release buckets without safety checks - caller must ensure it's safe
548+
fn release_virtual_memory_buckets_unchecked(&mut self, id: MemoryId) -> Result<usize, String> {
549+
let memory_buckets = &mut self.memory_buckets[id.0 as usize];
550+
let bucket_count = memory_buckets.len();
551+
552+
if bucket_count == 0 {
553+
return Ok(0);
554+
}
555+
556+
// Mark all buckets as unallocated in stable storage and move to free pool
557+
for &bucket_id in memory_buckets.iter() {
558+
write(
559+
&self.memory,
560+
bucket_allocations_address(bucket_id).get(),
561+
&[UNALLOCATED_BUCKET_MARKER],
562+
);
563+
self.free_buckets.push(bucket_id);
564+
}
565+
566+
// Clear the memory's bucket list
567+
memory_buckets.clear();
568+
569+
// Reset the memory size to 0
570+
self.memory_sizes_in_pages[id.0 as usize] = 0;
571+
572+
self.save_header();
573+
574+
Ok(bucket_count)
575+
}
576+
389577
fn write(&self, id: MemoryId, offset: u64, src: &[u8], bucket_cache: &BucketCache) {
390578
if let Some(real_address) = bucket_cache.get(VirtualSegment {
391579
address: offset.into(),
@@ -1110,3 +1298,6 @@ mod test {
11101298
);
11111299
}
11121300
}
1301+
1302+
#[cfg(test)]
1303+
mod bucket_release_tests;

0 commit comments

Comments
 (0)