Skip to content

Commit 74613ac

Browse files
committed
feat: try_release_virtual_memory_buckets
1 parent 652d11b commit 74613ac

File tree

2 files changed

+462
-7
lines changed

2 files changed

+462
-7
lines changed

src/memory_manager.rs

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ const HEADER_RESERVED_BYTES: usize = 32;
127127
/// Bucket MAX_NUM_BUCKETS ↕ N pages
128128
/// ```
129129
///
130+
/// # Bucket Release and Memory Reclamation
131+
///
132+
/// The memory manager now supports manual bucket release to address long-term memory cost concerns.
133+
/// When data structures are cleared, their underlying buckets can be explicitly released for reuse:
134+
///
135+
/// - **Safe Release**: Use `try_release_virtual_memory_buckets()` which checks if the virtual memory
136+
/// size is 0 before releasing buckets. This prevents accidental bucket release when data might
137+
/// still exist.
138+
/// - **Bucket Reuse**: Released buckets are automatically reused when new virtual memories need
139+
/// storage, reducing overall memory allocation.
140+
///
141+
/// Note: Calling `clear_new()` on data structures does not automatically shrink virtual memory size,
142+
/// so the safe release method provides an important safety check.
143+
///
130144
/// # Current Limitations and Future Improvements
131145
///
132146
/// - Buckets are never deallocated once assigned to a memory ID, even when the memory becomes empty
@@ -162,6 +176,33 @@ impl<M: Memory> MemoryManager<M> {
162176
}
163177
}
164178

179+
/// Safely releases buckets only if the virtual memory appears to be empty.
180+
///
181+
/// This method checks if the memory size is 0 before releasing buckets, providing a safety check
182+
/// to avoid releasing buckets when data structures might still contain data.
183+
/// Returns Ok(bucket_count) if buckets were released, or Err(current_size) if the memory
184+
/// appears to still contain data.
185+
///
186+
/// This method should be called when a data structure is completely cleared
187+
/// to allow its memory buckets to be reused by other data structures.
188+
///
189+
/// # Example
190+
/// ```rust,ignore
191+
/// let memory_manager = MemoryManager::init(DefaultMemoryImpl::default());
192+
/// let memory_id = MemoryId::new(0);
193+
///
194+
/// // After clearing a data structure
195+
/// match memory_manager.try_release_virtual_memory_buckets(memory_id) {
196+
/// Ok(count) => println!("Released {} buckets", count),
197+
/// Err(size) => println!("Memory still has {} pages, cannot release", size),
198+
/// }
199+
/// ```
200+
pub fn try_release_virtual_memory_buckets(&self, id: MemoryId) -> Result<usize, u64> {
201+
self.inner
202+
.borrow_mut()
203+
.try_release_virtual_memory_buckets(id)
204+
}
205+
165206
/// Returns the underlying memory.
166207
///
167208
/// # Returns
@@ -246,6 +287,9 @@ struct MemoryManagerInner<M: Memory> {
246287

247288
/// A map mapping each managed memory to the bucket ids that are allocated to it.
248289
memory_buckets: Vec<Vec<BucketId>>,
290+
291+
/// A pool of free buckets that can be reused by any memory.
292+
free_buckets: Vec<BucketId>,
249293
}
250294

251295
impl<M: Memory> MemoryManagerInner<M> {
@@ -274,6 +318,7 @@ impl<M: Memory> MemoryManagerInner<M> {
274318
memory_sizes_in_pages: [0; MAX_NUM_MEMORIES as usize],
275319
memory_buckets: vec![vec![]; MAX_NUM_MEMORIES as usize],
276320
bucket_size_in_pages,
321+
free_buckets: Vec::new(),
277322
};
278323

279324
mem_mgr.save_header();
@@ -303,9 +348,13 @@ impl<M: Memory> MemoryManagerInner<M> {
303348
);
304349

305350
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));
351+
let mut free_buckets = Vec::new();
352+
for (bucket_idx, memory_id) in buckets.into_iter().enumerate() {
353+
if memory_id != UNALLOCATED_BUCKET_MARKER {
354+
memory_buckets[memory_id as usize].push(BucketId(bucket_idx as u16));
355+
} else if (bucket_idx as u16) < header.num_allocated_buckets {
356+
// This bucket was allocated but is now marked as unallocated, so it's free to reuse
357+
free_buckets.push(BucketId(bucket_idx as u16));
309358
}
310359
}
311360

@@ -315,6 +364,7 @@ impl<M: Memory> MemoryManagerInner<M> {
315364
bucket_size_in_pages: header.bucket_size_in_pages,
316365
memory_sizes_in_pages: header.memory_sizes_in_pages,
317366
memory_buckets,
367+
free_buckets,
318368
}
319369
}
320370

@@ -345,7 +395,11 @@ impl<M: Memory> MemoryManagerInner<M> {
345395
let required_buckets = self.num_buckets_needed(new_size);
346396
let new_buckets_needed = required_buckets - current_buckets;
347397

348-
if new_buckets_needed + self.allocated_buckets as u64 > MAX_NUM_BUCKETS {
398+
// Check if we have enough buckets available (either already allocated or can allocate new ones)
399+
let available_free_buckets = self.free_buckets.len() as u64;
400+
let new_buckets_to_allocate = new_buckets_needed.saturating_sub(available_free_buckets);
401+
402+
if self.allocated_buckets as u64 + new_buckets_to_allocate > MAX_NUM_BUCKETS {
349403
// Exceeded the memory that can be managed.
350404
return -1;
351405
}
@@ -354,7 +408,16 @@ impl<M: Memory> MemoryManagerInner<M> {
354408
// Allocate new buckets as needed.
355409
memory_bucket.reserve(new_buckets_needed as usize);
356410
for _ in 0..new_buckets_needed {
357-
let new_bucket_id = BucketId(self.allocated_buckets);
411+
let new_bucket_id = if let Some(free_bucket) = self.free_buckets.pop() {
412+
// Reuse a bucket from the free pool
413+
free_bucket
414+
} else {
415+
// Allocate a new bucket
416+
let bucket = BucketId(self.allocated_buckets);
417+
self.allocated_buckets += 1;
418+
bucket
419+
};
420+
358421
memory_bucket.push(new_bucket_id);
359422

360423
// Write in stable store that this bucket belongs to the memory with the provided `id`.
@@ -363,8 +426,6 @@ impl<M: Memory> MemoryManagerInner<M> {
363426
bucket_allocations_address(new_bucket_id).get(),
364427
&[id.0],
365428
);
366-
367-
self.allocated_buckets += 1;
368429
}
369430

370431
// Grow the underlying memory if necessary.
@@ -386,6 +447,41 @@ impl<M: Memory> MemoryManagerInner<M> {
386447
old_size as i64
387448
}
388449

450+
/// Safely releases buckets only if the virtual memory appears to be empty.
451+
///
452+
/// Checks if the memory size is 0 before releasing buckets. This provides a safety check
453+
/// to avoid releasing buckets when data structures might still contain data.
454+
///
455+
/// Returns Ok(bucket_count) if buckets were released, or Err(current_size) if the memory
456+
/// appears to still contain data.
457+
fn try_release_virtual_memory_buckets(&mut self, id: MemoryId) -> Result<usize, u64> {
458+
let current_size = self.memory_sizes_in_pages[id.0 as usize];
459+
460+
if current_size == 0 {
461+
// Memory appears empty, safe to release buckets
462+
let memory_buckets = &mut self.memory_buckets[id.0 as usize];
463+
let bucket_count = memory_buckets.len();
464+
465+
// Mark all buckets as unallocated in stable storage and move to free pool
466+
for &bucket_id in memory_buckets.iter() {
467+
write(
468+
&self.memory,
469+
bucket_allocations_address(bucket_id).get(),
470+
&[UNALLOCATED_BUCKET_MARKER],
471+
);
472+
self.free_buckets.push(bucket_id);
473+
}
474+
475+
// Clear the memory's bucket list (size is already 0)
476+
memory_buckets.clear();
477+
self.save_header();
478+
479+
Ok(bucket_count)
480+
} else {
481+
Err(current_size)
482+
}
483+
}
484+
389485
fn write(&self, id: MemoryId, offset: u64, src: &[u8], bucket_cache: &BucketCache) {
390486
if let Some(real_address) = bucket_cache.get(VirtualSegment {
391487
address: offset.into(),
@@ -1110,3 +1206,6 @@ mod test {
11101206
);
11111207
}
11121208
}
1209+
1210+
#[cfg(test)]
1211+
mod bucket_release_tests;

0 commit comments

Comments
 (0)