Skip to content

Commit 7a36ac2

Browse files
authored
fix: revert adding reclaim_memory to memory manager (#396)
This PR reverts adding `reclaim_memory` method. Reverts several recent commits in a single change: - `6e397bd` fix: use conservative bucket reuse that survives reload #394 - `00468e1` docs: update memory reclamation examples in the docs #392 - `b911479` docs: cleanup documentation #391 - `d1fde89` docs: use reclaim_memory() name and update docs accordingly #388 - `a18917b` docs: add safety documentation and tests for manual bucket release #387 - `73e96e8` feat: add manual bucket release to prevent memory waste #386 This PR restores the codebase to the state before these commits. Done with `git revert -n 6e397bd 00468e1 b911479 d1fde89 a18917b 73e96e8` The reason for reverting this approach was that it can reclaim unused memory in theory but provides little benefit in real-world migrations. All due to the requirement to keep buckets in ascending order in each VM. Example 1: Reuse works ``` A allocates: [0, 4, 5] B allocates: [1, 2, 3] A frees: [0, 4, 5] B grows: can reuse bucket 4 (since 4 > max(B) = 3) B after grow: [1, 2, 3, 4] ``` Example 2: Reuse fails ``` A allocates: [0, 1, 2] B allocates: [4, 5, 6] A frees: [0, 1, 2] B grows: cannot reuse any freed bucket (all < max(B) = 6), so allocates new bucket 7 B after grow: [4, 5, 6, 7] ``` In real life when migrating state A to state B, state B created after state A grown, so it's first bucket ID is already higher than any free bucket in state A virtual memory, therefore can not be reused.
1 parent 6e397bd commit 7a36ac2

File tree

12 files changed

+124
-1113
lines changed

12 files changed

+124
-1113
lines changed

README.md

Lines changed: 41 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ Stable structures are able to work directly in stable memory because each data s
4040
its own memory.
4141
When initializing a stable structure, a memory is provided that the data structure can use to store its data.
4242

43-
### Basic Usage
43+
Here are some basic examples:
4444

45-
Here's a basic example:
45+
### Example: BTreeMap
4646

4747
```rust
4848
use ic_stable_structures::{BTreeMap, DefaultMemoryImpl};
49-
let mut map: BTreeMap<u64, String, _> = BTreeMap::init(DefaultMemoryImpl::default());
49+
let mut map: BTreeMap<u64, u64, _> = BTreeMap::init(DefaultMemoryImpl::default());
5050

51-
map.insert(1, "hello".to_string());
52-
assert_eq!(map.get(&1), Some("hello".to_string()));
51+
map.insert(1, 2);
52+
assert_eq!(map.get(&1), Some(2));
5353
```
5454

5555
Memories are abstracted with the [Memory] trait, and stable structures can work with any storage
@@ -58,108 +58,52 @@ This includes stable memory, a vector ([VectorMemory]), or even a flat file ([Fi
5858

5959
The example above initializes a [BTreeMap] with a [DefaultMemoryImpl], which maps to stable memory when used in a canister and to a [VectorMemory] otherwise.
6060

61-
### Memory Isolation Requirement
61+
### Example: BTreeSet
6262

63-
> **⚠️ CRITICAL:** Stable structures **MUST NOT** share memories!
64-
> Each memory must belong to only one stable structure.
63+
The `BTreeSet` is a stable set implementation based on a B-Tree. It allows efficient insertion, deletion, and lookup of unique elements.
6564

65+
```rust
66+
use ic_stable_structures::{BTreeSet, DefaultMemoryImpl};
67+
let mut set: BTreeSet<u64, _> = BTreeSet::new(DefaultMemoryImpl::default());
68+
69+
set.insert(42);
70+
assert!(set.contains(&42));
71+
assert_eq!(set.pop_first(), Some(42));
72+
assert!(set.is_empty());
73+
```
74+
75+
76+
Note that **stable structures cannot share memories.**
77+
Each memory must belong to only one stable structure.
6678
For example, this fails when run in a canister:
6779

68-
```rust,ignore
80+
```no_run
6981
use ic_stable_structures::{BTreeMap, DefaultMemoryImpl};
70-
let mut map_a: BTreeMap<u64, u8, _> = BTreeMap::init(DefaultMemoryImpl::default());
71-
let mut map_b: BTreeMap<u64, u8, _> = BTreeMap::init(DefaultMemoryImpl::default());
82+
let mut map_1: BTreeMap<u64, u64, _> = BTreeMap::init(DefaultMemoryImpl::default());
83+
let mut map_2: BTreeMap<u64, u64, _> = BTreeMap::init(DefaultMemoryImpl::default());
7284
73-
map_a.insert(1, b'A');
74-
map_b.insert(1, b'B');
75-
assert_eq!(map_a.get(&1), Some(b'A')); // ❌ FAILS: Returns b'B' due to shared memory!
76-
assert_eq!(map_b.get(&1), Some(b'B')); // ✅ Succeeds, but corrupted map_a
85+
map_1.insert(1, 2);
86+
map_2.insert(1, 3);
87+
assert_eq!(map_1.get(&1), Some(2)); // This assertion fails.
7788
```
7889

79-
It fails because both `map_a` and `map_b` are using the same stable memory under the hood, and so changes in `map_b` end up changing or corrupting `map_a`.
90+
It fails because both `map_1` and `map_2` are using the same stable memory under the hood, and so changes in `map_1` end up changing or corrupting `map_2`.
8091

81-
### Using MemoryManager
82-
83-
To address this issue, we use the [MemoryManager](memory_manager::MemoryManager), which takes a single memory and creates up to 255 virtual memories for our use.
84-
Here's the above failing example, but fixed:
92+
To address this issue, we make use of the [MemoryManager](memory_manager::MemoryManager), which takes a single memory and creates up to 255 virtual memories for our disposal.
93+
Here's the above failing example, but fixed by using the [MemoryManager](memory_manager::MemoryManager):
8594

8695
```rust
8796
use ic_stable_structures::{
8897
memory_manager::{MemoryId, MemoryManager},
8998
BTreeMap, DefaultMemoryImpl,
9099
};
91100
let mem_mgr = MemoryManager::init(DefaultMemoryImpl::default());
92-
let (mem_id_a, mem_id_b) = (MemoryId::new(0), MemoryId::new(1));
93-
let mut map_a: BTreeMap<u64, u8, _> = BTreeMap::init(mem_mgr.get(mem_id_a));
94-
let mut map_b: BTreeMap<u64, u8, _> = BTreeMap::init(mem_mgr.get(mem_id_b));
95-
96-
map_a.insert(1, b'A');
97-
map_b.insert(1, b'B');
98-
assert_eq!(map_a.get(&1), Some(b'A')); // ✅ Succeeds: Each map has its own memory
99-
assert_eq!(map_b.get(&1), Some(b'B')); // ✅ Succeeds: No data corruption
100-
```
101-
102-
### Memory Reclamation
103-
104-
During migrations you often create a new structure (B) and copy data from an existing one (A). Without reclamation, this can double memory usage even after A is no longer needed.
105-
106-
Bucket IDs are an internal implementation detail — hidden and not user-controllable — and each virtual memory must receive bucket IDs in strictly ascending order. Because of this, reuse of freed buckets is guaranteed when allocating into a newly created (empty) structure. For existing structures, reuse may or may not work: it succeeds only if there is a free bucket with an ID greater than the structure’s current maximum; otherwise a new bucket is allocated.
107-
108-
Example: A = `[0, 4, 5]`, B = `[1, 2, 3]`. After releasing A, `free = [0, 4, 5]`. When B grows, it can’t take `0` (must be `> 3`) but can take `4``B = [1, 2, 3, 4]`, `free = [0, 5]`.
109-
110-
**Recommendation:** for predictable reuse migrate into a newly created structure rather than relying on reuse with a populated one.
111-
112-
> **⚠️ CRITICAL SAFETY REQUIREMENT:**
113-
> - **MUST** drop the original structure object before calling `reclaim_memory`.
114-
> - **NEVER** use the original structure after reclamation — doing so corrupts data.
115-
116-
The `MemoryManager` provides a `reclaim_memory` method to efficiently handle these scenarios:
117-
118-
```rust
119-
use ic_stable_structures::{
120-
memory_manager::{MemoryId, MemoryManager},
121-
BTreeMap, DefaultMemoryImpl, Memory,
122-
};
101+
let mut map_1: BTreeMap<u64, u64, _> = BTreeMap::init(mem_mgr.get(MemoryId::new(0)));
102+
let mut map_2: BTreeMap<u64, u64, _> = BTreeMap::init(mem_mgr.get(MemoryId::new(1)));
123103

124-
let mem = DefaultMemoryImpl::default();
125-
let mem_mgr = MemoryManager::init(mem.clone());
126-
let (mem_id_a, mem_id_b) = (MemoryId::new(0), MemoryId::new(1));
127-
128-
// ========================================
129-
// Scenario 1: WITHOUT reclamation
130-
// ========================================
131-
let mut map_a: BTreeMap<u64, u8, _> = BTreeMap::init(mem_mgr.get(mem_id_a));
132-
map_a.insert(1, b'A'); // Populate map A with data
133-
let data = map_a.get(&1); // Extract data for migration
134-
map_a.clear_new(); // A is now empty
135-
drop(map_a); // Memory stays allocated to mem_id_a
136-
let actual_size_before_migration = mem.size();
137-
138-
let mut map_b: BTreeMap<u64, u8, _> = BTreeMap::new(mem_mgr.get(mem_id_b));
139-
map_b.insert(1, data.unwrap()); // B allocates NEW memory
140-
let actual_size_after_migration = mem.size();
141-
// Result: ~2x memory usage
142-
// Memory allocation grew (waste)
143-
assert!(actual_size_before_migration < actual_size_after_migration);
144-
145-
// ========================================
146-
// Scenario 2: WITH reclamation
147-
// ========================================
148-
let mut map_a: BTreeMap<u64, u8, _> = BTreeMap::init(mem_mgr.get(mem_id_a));
149-
map_a.insert(1, b'A'); // Populate map A with data
150-
let data = map_a.get(&1); // Extract data for migration
151-
map_a.clear_new(); // A is now empty
152-
drop(map_a); // Drop A completely
153-
let actual_size_before_migration = mem.size();
154-
mem_mgr.reclaim_memory(mem_id_a); // Free A's memory buckets for reuse
155-
156-
// Reusing free memory buckets works best on newly created structures
157-
let mut map_b: BTreeMap<u64, u8, _> = BTreeMap::new(mem_mgr.get(mem_id_b));
158-
map_b.insert(1, data.unwrap()); // B reuses A's reclaimed memory buckets
159-
let actual_size_after_migration = mem.size();
160-
// Result: 1x memory usage
161-
// Memory allocation stayed the same (no waste)
162-
assert!(actual_size_before_migration == actual_size_after_migration);
104+
map_1.insert(1, 2);
105+
map_2.insert(1, 3);
106+
assert_eq!(map_1.get(&1), Some(2)); // Succeeds, as expected.
163107
```
164108

165109
## Example Canister
@@ -191,7 +135,7 @@ thread_local! {
191135
RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
192136

193137
// Initialize a `StableBTreeMap` with `MemoryId(0)`.
194-
static MAP: RefCell<StableBTreeMap<u64, String, Memory>> = RefCell::new(
138+
static MAP: RefCell<StableBTreeMap<u128, u128, Memory>> = RefCell::new(
195139
StableBTreeMap::init(
196140
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))),
197141
)
@@ -200,32 +144,32 @@ thread_local! {
200144

201145
// Retrieves the value associated with the given key if it exists.
202146
#[ic_cdk_macros::query]
203-
fn get(key: u64) -> Option<String> {
147+
fn get(key: u128) -> Option<u128> {
204148
MAP.with(|p| p.borrow().get(&key))
205149
}
206150

207151
// Inserts an entry into the map and returns the previous value of the key if it exists.
208152
#[ic_cdk_macros::update]
209-
fn insert(key: u64, value: String) -> Option<String> {
153+
fn insert(key: u128, value: u128) -> Option<u128> {
210154
MAP.with(|p| p.borrow_mut().insert(key, value))
211155
}
212156
```
213157

214158
### More Examples
215159

216-
- [Basic Example](https://github.com/dfinity/stable-structures/tree/main/examples/src/basic_example): Simple usage patterns
160+
- [Basic Example](https://github.com/dfinity/stable-structures/tree/main/examples/src/basic_example) (the one above)
217161
- [Quickstart Example](https://github.com/dfinity/stable-structures/tree/main/examples/src/quick_start): Ideal as a template when developing a new canister
218162
- [Custom Types Example](https://github.com/dfinity/stable-structures/tree/main/examples/src/custom_types_example): Showcases storing your own custom types
219163

220164
## Combined Persistence
221165

222-
If your project uses only stable structures, memory can expand in size without requiring `pre_upgrade`/`post_upgrade` hooks.
166+
If your project exclusively relies on stable structures, the memory can expand in size without the requirement of `pre_upgrade`/`post_upgrade` hooks.
223167

224-
However, if you also need to serialize/deserialize heap data, you must use the memory manager to avoid conflicts. To combine both approaches effectively, refer to the [Quickstart Example](https://github.com/dfinity/stable-structures/tree/main/examples/src/quick_start) for guidance.
168+
However, it's important to note that if you also intend to perform serialization/deserialization of the heap data, utilizing the memory manager becomes necessary. To effectively combine both approaches, refer to the [Quickstart Example](https://github.com/dfinity/stable-structures/tree/main/examples/src/quick_start) for guidance.
225169

226170
## Fuzzing
227171

228-
Stable structures require strong guarantees to work reliably and scale over millions of operations. To that extent, we use fuzzing to emulate such operations on the available data structures.
172+
Stable structures requires strong guarantees to work reliably and scale over millions of operations. To that extent, we use fuzzing to emulate such operations on the available data structures.
229173

230174
To run a fuzzer locally,
231175
```sh

benchmarks/btreemap/canbench_results.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -975,35 +975,35 @@ benches:
975975
btreemap_v2_mem_manager_insert_blob512_u64:
976976
total:
977977
calls: 1
978-
instructions: 3127680632
978+
instructions: 3127680452
979979
heap_increase: 0
980980
stable_memory_increase: 0
981981
scopes: {}
982982
btreemap_v2_mem_manager_insert_u64_blob512:
983983
total:
984984
calls: 1
985-
instructions: 607288554
985+
instructions: 607288370
986986
heap_increase: 0
987987
stable_memory_increase: 0
988988
scopes: {}
989989
btreemap_v2_mem_manager_insert_u64_u64:
990990
total:
991991
calls: 1
992-
instructions: 520545876
992+
instructions: 520545864
993993
heap_increase: 0
994994
stable_memory_increase: 0
995995
scopes: {}
996996
btreemap_v2_mem_manager_insert_u64_vec512:
997997
total:
998998
calls: 1
999-
instructions: 834100785
999+
instructions: 834100595
10001000
heap_increase: 0
10011001
stable_memory_increase: 0
10021002
scopes: {}
10031003
btreemap_v2_mem_manager_insert_vec512_u64:
10041004
total:
10051005
calls: 1
1006-
instructions: 1964405636
1006+
instructions: 1964405448
10071007
heap_increase: 0
10081008
stable_memory_increase: 0
10091009
scopes: {}

benchmarks/io_chunks/canbench_results.yml

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,126 +2,126 @@ benches:
22
read_chunks_btreemap_1:
33
total:
44
calls: 1
5-
instructions: 148723635
5+
instructions: 148723585
66
heap_increase: 1601
77
stable_memory_increase: 0
88
scopes: {}
99
read_chunks_btreemap_1k:
1010
total:
1111
calls: 1
12-
instructions: 498267990
12+
instructions: 498228206
1313
heap_increase: 0
1414
stable_memory_increase: 0
1515
scopes: {}
1616
read_chunks_btreemap_1m:
1717
total:
1818
calls: 1
19-
instructions: 40947358379
19+
instructions: 40940569325
2020
heap_increase: 0
2121
stable_memory_increase: 0
2222
scopes: {}
2323
read_chunks_stable_1:
2424
total:
2525
calls: 1
26-
instructions: 104859145
26+
instructions: 104859143
2727
heap_increase: 0
2828
stable_memory_increase: 0
2929
scopes: {}
3030
read_chunks_stable_1k:
3131
total:
3232
calls: 1
33-
instructions: 104985765
33+
instructions: 104985739
3434
heap_increase: 0
3535
stable_memory_increase: 0
3636
scopes: {}
3737
read_chunks_stable_1m:
3838
total:
3939
calls: 1
40-
instructions: 230002765
40+
instructions: 230002739
4141
heap_increase: 0
4242
stable_memory_increase: 0
4343
scopes: {}
4444
read_chunks_vec_1:
4545
total:
4646
calls: 1
47-
instructions: 104859762
47+
instructions: 104859760
4848
heap_increase: 0
4949
stable_memory_increase: 0
5050
scopes: {}
5151
read_chunks_vec_1k:
5252
total:
5353
calls: 1
54-
instructions: 105830202
54+
instructions: 105826498
5555
heap_increase: 0
5656
stable_memory_increase: 0
5757
scopes: {}
5858
read_chunks_vec_1m:
5959
total:
6060
calls: 1
61-
instructions: 1014586404
61+
instructions: 1010905944
6262
heap_increase: 0
6363
stable_memory_increase: 0
6464
scopes: {}
6565
write_chunks_btreemap_1:
6666
total:
6767
calls: 1
68-
instructions: 357208825
68+
instructions: 357205397
6969
heap_increase: 13
7070
stable_memory_increase: 1536
7171
scopes: {}
7272
write_chunks_btreemap_1k:
7373
total:
7474
calls: 1
75-
instructions: 4187350965
75+
instructions: 4187119879
7676
heap_increase: 2
7777
stable_memory_increase: 1536
7878
scopes: {}
7979
write_chunks_btreemap_1m:
8080
total:
8181
calls: 1
82-
instructions: 83670464159
82+
instructions: 83659829857
8383
heap_increase: 0
8484
stable_memory_increase: 3072
8585
scopes: {}
8686
write_chunks_stable_1:
8787
total:
8888
calls: 1
89-
instructions: 130472086
89+
instructions: 130471968
9090
heap_increase: 0
9191
stable_memory_increase: 1664
9292
scopes: {}
9393
write_chunks_stable_1k:
9494
total:
9595
calls: 1
96-
instructions: 130598863
96+
instructions: 130598745
9797
heap_increase: 0
9898
stable_memory_increase: 1664
9999
scopes: {}
100100
write_chunks_stable_1m:
101101
total:
102102
calls: 1
103-
instructions: 255406776
103+
instructions: 255406658
104104
heap_increase: 0
105105
stable_memory_increase: 1664
106106
scopes: {}
107107
write_chunks_vec_1:
108108
total:
109109
calls: 1
110-
instructions: 549903682
110+
instructions: 549903573
111111
heap_increase: 0
112112
stable_memory_increase: 1536
113113
scopes: {}
114114
write_chunks_vec_1k:
115115
total:
116116
calls: 1
117-
instructions: 562259611
117+
instructions: 562257515
118118
heap_increase: 0
119119
stable_memory_increase: 1536
120120
scopes: {}
121121
write_chunks_vec_1m:
122122
total:
123123
calls: 1
124-
instructions: 1896596401
124+
instructions: 1896593101
125125
heap_increase: 0
126126
stable_memory_increase: 1536
127127
scopes: {}

0 commit comments

Comments
 (0)