@@ -127,11 +127,17 @@ const HEADER_RESERVED_BYTES: usize = 32;
127
127
/// Bucket MAX_NUM_BUCKETS ↕ N pages
128
128
/// ```
129
129
///
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
+ ///
130
136
/// # Current Limitations and Future Improvements
131
137
///
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
135
141
/// - Future versions may consider automatic bucket reclamation and memory compaction features
136
142
pub struct MemoryManager < M : Memory > {
137
143
inner : Rc < RefCell < MemoryManagerInner < M > > > ,
@@ -162,6 +168,38 @@ impl<M: Memory> MemoryManager<M> {
162
168
}
163
169
}
164
170
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
+
165
203
/// Returns the underlying memory.
166
204
///
167
205
/// # Returns
@@ -246,6 +284,9 @@ struct MemoryManagerInner<M: Memory> {
246
284
247
285
/// A map mapping each managed memory to the bucket ids that are allocated to it.
248
286
memory_buckets : Vec < Vec < BucketId > > ,
287
+
288
+ /// A pool of free buckets that can be reused by any memory.
289
+ free_buckets : Vec < BucketId > ,
249
290
}
250
291
251
292
impl < M : Memory > MemoryManagerInner < M > {
@@ -274,6 +315,7 @@ impl<M: Memory> MemoryManagerInner<M> {
274
315
memory_sizes_in_pages : [ 0 ; MAX_NUM_MEMORIES as usize ] ,
275
316
memory_buckets : vec ! [ vec![ ] ; MAX_NUM_MEMORIES as usize ] ,
276
317
bucket_size_in_pages,
318
+ free_buckets : Vec :: new ( ) ,
277
319
} ;
278
320
279
321
mem_mgr. save_header ( ) ;
@@ -303,9 +345,13 @@ impl<M: Memory> MemoryManagerInner<M> {
303
345
) ;
304
346
305
347
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 ) ) ;
309
355
}
310
356
}
311
357
@@ -315,6 +361,7 @@ impl<M: Memory> MemoryManagerInner<M> {
315
361
bucket_size_in_pages : header. bucket_size_in_pages ,
316
362
memory_sizes_in_pages : header. memory_sizes_in_pages ,
317
363
memory_buckets,
364
+ free_buckets,
318
365
}
319
366
}
320
367
@@ -345,7 +392,11 @@ impl<M: Memory> MemoryManagerInner<M> {
345
392
let required_buckets = self . num_buckets_needed ( new_size) ;
346
393
let new_buckets_needed = required_buckets - current_buckets;
347
394
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 {
349
400
// Exceeded the memory that can be managed.
350
401
return -1 ;
351
402
}
@@ -354,7 +405,16 @@ impl<M: Memory> MemoryManagerInner<M> {
354
405
// Allocate new buckets as needed.
355
406
memory_bucket. reserve ( new_buckets_needed as usize ) ;
356
407
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
+
358
418
memory_bucket. push ( new_bucket_id) ;
359
419
360
420
// Write in stable store that this bucket belongs to the memory with the provided `id`.
@@ -363,8 +423,6 @@ impl<M: Memory> MemoryManagerInner<M> {
363
423
bucket_allocations_address ( new_bucket_id) . get ( ) ,
364
424
& [ id. 0 ] ,
365
425
) ;
366
-
367
- self . allocated_buckets += 1 ;
368
426
}
369
427
370
428
// Grow the underlying memory if necessary.
@@ -386,6 +444,136 @@ impl<M: Memory> MemoryManagerInner<M> {
386
444
old_size as i64
387
445
}
388
446
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
+
389
577
fn write ( & self , id : MemoryId , offset : u64 , src : & [ u8 ] , bucket_cache : & BucketCache ) {
390
578
if let Some ( real_address) = bucket_cache. get ( VirtualSegment {
391
579
address : offset. into ( ) ,
@@ -1110,3 +1298,6 @@ mod test {
1110
1298
) ;
1111
1299
}
1112
1300
}
1301
+
1302
+ #[ cfg( test) ]
1303
+ mod bucket_release_tests;
0 commit comments