@@ -127,6 +127,20 @@ const HEADER_RESERVED_BYTES: usize = 32;
127
127
/// Bucket MAX_NUM_BUCKETS ↕ N pages
128
128
/// ```
129
129
///
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
+ ///
130
144
/// # Current Limitations and Future Improvements
131
145
///
132
146
/// - 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> {
162
176
}
163
177
}
164
178
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
+
165
206
/// Returns the underlying memory.
166
207
///
167
208
/// # Returns
@@ -246,6 +287,9 @@ struct MemoryManagerInner<M: Memory> {
246
287
247
288
/// A map mapping each managed memory to the bucket ids that are allocated to it.
248
289
memory_buckets : Vec < Vec < BucketId > > ,
290
+
291
+ /// A pool of free buckets that can be reused by any memory.
292
+ free_buckets : Vec < BucketId > ,
249
293
}
250
294
251
295
impl < M : Memory > MemoryManagerInner < M > {
@@ -274,6 +318,7 @@ impl<M: Memory> MemoryManagerInner<M> {
274
318
memory_sizes_in_pages : [ 0 ; MAX_NUM_MEMORIES as usize ] ,
275
319
memory_buckets : vec ! [ vec![ ] ; MAX_NUM_MEMORIES as usize ] ,
276
320
bucket_size_in_pages,
321
+ free_buckets : Vec :: new ( ) ,
277
322
} ;
278
323
279
324
mem_mgr. save_header ( ) ;
@@ -303,9 +348,13 @@ impl<M: Memory> MemoryManagerInner<M> {
303
348
) ;
304
349
305
350
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 ) ) ;
309
358
}
310
359
}
311
360
@@ -315,6 +364,7 @@ impl<M: Memory> MemoryManagerInner<M> {
315
364
bucket_size_in_pages : header. bucket_size_in_pages ,
316
365
memory_sizes_in_pages : header. memory_sizes_in_pages ,
317
366
memory_buckets,
367
+ free_buckets,
318
368
}
319
369
}
320
370
@@ -345,7 +395,11 @@ impl<M: Memory> MemoryManagerInner<M> {
345
395
let required_buckets = self . num_buckets_needed ( new_size) ;
346
396
let new_buckets_needed = required_buckets - current_buckets;
347
397
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 {
349
403
// Exceeded the memory that can be managed.
350
404
return -1 ;
351
405
}
@@ -354,7 +408,16 @@ impl<M: Memory> MemoryManagerInner<M> {
354
408
// Allocate new buckets as needed.
355
409
memory_bucket. reserve ( new_buckets_needed as usize ) ;
356
410
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
+
358
421
memory_bucket. push ( new_bucket_id) ;
359
422
360
423
// Write in stable store that this bucket belongs to the memory with the provided `id`.
@@ -363,8 +426,6 @@ impl<M: Memory> MemoryManagerInner<M> {
363
426
bucket_allocations_address ( new_bucket_id) . get ( ) ,
364
427
& [ id. 0 ] ,
365
428
) ;
366
-
367
- self . allocated_buckets += 1 ;
368
429
}
369
430
370
431
// Grow the underlying memory if necessary.
@@ -386,6 +447,41 @@ impl<M: Memory> MemoryManagerInner<M> {
386
447
old_size as i64
387
448
}
388
449
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
+
389
485
fn write ( & self , id : MemoryId , offset : u64 , src : & [ u8 ] , bucket_cache : & BucketCache ) {
390
486
if let Some ( real_address) = bucket_cache. get ( VirtualSegment {
391
487
address : offset. into ( ) ,
@@ -1110,3 +1206,6 @@ mod test {
1110
1206
) ;
1111
1207
}
1112
1208
}
1209
+
1210
+ #[ cfg( test) ]
1211
+ mod bucket_release_tests;
0 commit comments