offheap 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.github/workflows/CI.yml +22 -1
  2. package/Cargo.toml +2 -2
  3. package/artifacts/bindings-aarch64-apple-darwin/index.d.ts +37 -25
  4. package/artifacts/bindings-aarch64-apple-darwin/index.js +165 -295
  5. package/artifacts/bindings-aarch64-unknown-linux-musl/index.d.ts +42 -0
  6. package/artifacts/bindings-aarch64-unknown-linux-musl/index.js +186 -0
  7. package/artifacts/bindings-x86_64-apple-darwin/index.d.ts +37 -25
  8. package/artifacts/bindings-x86_64-apple-darwin/index.js +165 -295
  9. package/artifacts/bindings-x86_64-pc-windows-msvc/index.d.ts +42 -30
  10. package/artifacts/bindings-x86_64-pc-windows-msvc/index.js +186 -316
  11. package/artifacts/bindings-x86_64-unknown-linux-gnu/index.d.ts +37 -25
  12. package/artifacts/bindings-x86_64-unknown-linux-gnu/index.js +165 -295
  13. package/artifacts/bindings-x86_64-unknown-linux-musl/index.d.ts +42 -0
  14. package/artifacts/bindings-x86_64-unknown-linux-musl/index.js +186 -0
  15. package/benchmarks/crossover_bench.js +139 -0
  16. package/docs/.vitepress/config.js +35 -0
  17. package/docs/guide/api.md +216 -0
  18. package/docs/guide/architecture.md +58 -0
  19. package/docs/guide/benchmarks.md +48 -0
  20. package/docs/guide/getting-started.md +74 -0
  21. package/docs/index.md +23 -0
  22. package/index.d.ts +37 -25
  23. package/index.js +165 -295
  24. package/package.json +21 -12
  25. package/src/algorithms/arc.rs +123 -25
  26. package/src/algorithms/lru.rs +94 -12
  27. package/src/algorithms/mod.rs +4 -0
  28. package/src/algorithms/tinylfu.rs +98 -35
  29. package/src/cache.rs +289 -94
  30. package/tests/index.test.js +134 -7
@@ -35,6 +35,8 @@ impl ArcListMetadata {
35
35
 
36
36
  pub struct ArcCache {
37
37
  capacity: usize,
38
+ max_bytes: Option<usize>,
39
+ bytes_used: usize,
38
40
  map: HashMap<String, usize>,
39
41
  nodes: Vec<ArcNode>,
40
42
  free_nodes: Vec<usize>,
@@ -51,9 +53,15 @@ pub struct ArcCache {
51
53
 
52
54
  impl ArcCache {
53
55
  pub fn new(capacity: usize) -> Self {
56
+ Self::new_with_max_bytes(capacity, None)
57
+ }
58
+
59
+ pub fn new_with_max_bytes(capacity: usize, max_bytes: Option<usize>) -> Self {
54
60
  Self {
55
61
  capacity,
56
- map: HashMap::with_capacity(capacity * 2), // Map contains history as well
62
+ max_bytes,
63
+ bytes_used: 0,
64
+ map: HashMap::with_capacity(capacity * 2),
57
65
  nodes: Vec::with_capacity(capacity * 2),
58
66
  free_nodes: Vec::new(),
59
67
  t1: ArcListMetadata::new(),
@@ -66,6 +74,15 @@ impl ArcCache {
66
74
  }
67
75
  }
68
76
 
77
+ fn node_bytes(&self, idx: usize) -> usize {
78
+ let node = &self.nodes[idx];
79
+ if let Some(ref entry) = node.value {
80
+ node.key.len() + entry.value.len()
81
+ } else {
82
+ 0
83
+ }
84
+ }
85
+
69
86
  fn detach(&mut self, idx: usize) {
70
87
  let prev = self.nodes[idx].prev;
71
88
  let next = self.nodes[idx].next;
@@ -118,6 +135,8 @@ impl ArcCache {
118
135
  }
119
136
 
120
137
  fn remove_completely(&mut self, idx: usize) -> String {
138
+ let bytes = self.node_bytes(idx);
139
+ self.bytes_used -= bytes;
121
140
  self.detach(idx);
122
141
  self.free_nodes.push(idx);
123
142
  let key = std::mem::take(&mut self.nodes[idx].key);
@@ -128,12 +147,16 @@ impl ArcCache {
128
147
  fn replace(&mut self, key_in_b2: bool) {
129
148
  if self.t1.len > 0 && (self.t1.len > self.p || (key_in_b2 && self.t1.len == self.p)) {
130
149
  if let Some(t1_tail) = self.t1.tail {
150
+ let bytes = self.node_bytes(t1_tail);
151
+ self.bytes_used -= bytes;
131
152
  self.detach(t1_tail);
132
153
  self.nodes[t1_tail].value = None; // Evict value from memory
133
154
  self.attach_to_head(t1_tail, ArcList::B1);
134
155
  }
135
156
  } else {
136
157
  if let Some(t2_tail) = self.t2.tail {
158
+ let bytes = self.node_bytes(t2_tail);
159
+ self.bytes_used -= bytes;
137
160
  self.detach(t2_tail);
138
161
  self.nodes[t2_tail].value = None; // Evict value from memory
139
162
  self.attach_to_head(t2_tail, ArcList::B2);
@@ -148,12 +171,7 @@ impl CacheImpl for ArcCache {
148
171
  let list = self.nodes[idx].list;
149
172
  match list {
150
173
  ArcList::T1 | ArcList::T2 => {
151
- let expired = if let Some(ref entry) = self.nodes[idx].value {
152
- entry.is_expired()
153
- } else {
154
- true
155
- };
156
-
174
+ let expired = self.nodes[idx].value.as_ref().map_or(true, |e| e.is_expired());
157
175
  if expired {
158
176
  self.remove_completely(idx);
159
177
  self.map.remove(key);
@@ -167,8 +185,6 @@ impl CacheImpl for ArcCache {
167
185
  }
168
186
  }
169
187
  ArcList::B1 | ArcList::B2 => {
170
- // History hit. We can't return value (evicted), so counts as miss for user,
171
- // but we trigger adaptation.
172
188
  self.misses += 1;
173
189
  None
174
190
  }
@@ -179,23 +195,74 @@ impl CacheImpl for ArcCache {
179
195
  }
180
196
  }
181
197
 
198
+ fn peek(&mut self, key: &str) -> Option<Vec<u8>> {
199
+ if let Some(&idx) = self.map.get(key) {
200
+ let list = self.nodes[idx].list;
201
+ match list {
202
+ ArcList::T1 | ArcList::T2 => {
203
+ let expired = self.nodes[idx].value.as_ref().map_or(true, |e| e.is_expired());
204
+ if expired {
205
+ self.remove_completely(idx);
206
+ self.map.remove(key);
207
+ self.misses += 1;
208
+ None
209
+ } else {
210
+ self.hits += 1;
211
+ self.nodes[idx].value.as_ref().map(|e| e.value.clone())
212
+ }
213
+ }
214
+ _ => {
215
+ self.misses += 1;
216
+ None
217
+ }
218
+ }
219
+ } else {
220
+ self.misses += 1;
221
+ None
222
+ }
223
+ }
224
+
225
+ fn has(&mut self, key: &str) -> bool {
226
+ if let Some(&idx) = self.map.get(key) {
227
+ let list = self.nodes[idx].list;
228
+ match list {
229
+ ArcList::T1 | ArcList::T2 => {
230
+ let expired = self.nodes[idx].value.as_ref().map_or(true, |e| e.is_expired());
231
+ if expired {
232
+ self.remove_completely(idx);
233
+ self.map.remove(key);
234
+ false
235
+ } else {
236
+ true
237
+ }
238
+ }
239
+ _ => false
240
+ }
241
+ } else {
242
+ false
243
+ }
244
+ }
245
+
182
246
  fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>> {
183
247
  if self.capacity == 0 {
184
248
  return None;
185
249
  }
186
250
 
251
+ let new_entry = CacheEntry::new(value, ttl_ms);
252
+ let new_bytes = key.len() + new_entry.value.len();
253
+ let mut old_value = None;
254
+
187
255
  if let Some(&idx) = self.map.get(key) {
188
256
  let list = self.nodes[idx].list;
189
257
  match list {
190
258
  ArcList::T1 | ArcList::T2 => {
191
- // Cache hit. Update value.
192
- let old_value = self.nodes[idx].value.replace(CacheEntry::new(value, ttl_ms)).map(|e| e.value);
259
+ let old_bytes = self.node_bytes(idx);
260
+ old_value = self.nodes[idx].value.replace(new_entry).map(|e| e.value);
261
+ self.bytes_used = self.bytes_used + new_bytes - old_bytes;
193
262
  self.detach(idx);
194
263
  self.attach_to_head(idx, ArcList::T2);
195
- old_value
196
264
  }
197
265
  ArcList::B1 => {
198
- // Hit in B1: we need more recency
199
266
  let b1_len = if self.b1.len == 0 { 1 } else { self.b1.len };
200
267
  let delta = std::cmp::max(1, self.b2.len / b1_len);
201
268
  self.p = std::cmp::min(self.p + delta, self.capacity);
@@ -203,12 +270,11 @@ impl CacheImpl for ArcCache {
203
270
  self.replace(false);
204
271
 
205
272
  self.detach(idx);
206
- self.nodes[idx].value = Some(CacheEntry::new(value, ttl_ms));
273
+ self.nodes[idx].value = Some(new_entry);
274
+ self.bytes_used += new_bytes;
207
275
  self.attach_to_head(idx, ArcList::T2);
208
- None
209
276
  }
210
277
  ArcList::B2 => {
211
- // Hit in B2: we need more frequency
212
278
  let b2_len = if self.b2.len == 0 { 1 } else { self.b2.len };
213
279
  let delta = std::cmp::max(1, self.b1.len / b2_len);
214
280
  self.p = self.p.saturating_sub(delta);
@@ -216,13 +282,12 @@ impl CacheImpl for ArcCache {
216
282
  self.replace(true);
217
283
 
218
284
  self.detach(idx);
219
- self.nodes[idx].value = Some(CacheEntry::new(value, ttl_ms));
285
+ self.nodes[idx].value = Some(new_entry);
286
+ self.bytes_used += new_bytes;
220
287
  self.attach_to_head(idx, ArcList::T2);
221
- None
222
288
  }
223
289
  }
224
290
  } else {
225
- // Cache Miss (not in cache, nor in history)
226
291
  let l1_len = self.t1.len + self.b1.len;
227
292
  let l2_len = self.t2.len + self.b2.len;
228
293
 
@@ -249,16 +314,15 @@ impl CacheImpl for ArcCache {
249
314
  self.replace(false);
250
315
  }
251
316
 
252
- // Create new node
253
317
  let idx = if let Some(free_idx) = self.free_nodes.pop() {
254
318
  self.nodes[free_idx].key = key.to_string();
255
- self.nodes[free_idx].value = Some(CacheEntry::new(value, ttl_ms));
319
+ self.nodes[free_idx].value = Some(new_entry);
256
320
  free_idx
257
321
  } else {
258
322
  let free_idx = self.nodes.len();
259
323
  self.nodes.push(ArcNode {
260
324
  key: key.to_string(),
261
- value: Some(CacheEntry::new(value, ttl_ms)),
325
+ value: Some(new_entry),
262
326
  list: ArcList::T1,
263
327
  prev: None,
264
328
  next: None,
@@ -266,9 +330,42 @@ impl CacheImpl for ArcCache {
266
330
  free_idx
267
331
  };
268
332
 
333
+ self.bytes_used += new_bytes;
269
334
  self.attach_to_head(idx, ArcList::T1);
270
335
  self.map.insert(key.to_string(), idx);
271
- None
336
+ }
337
+
338
+ if let Some(max) = self.max_bytes {
339
+ while self.bytes_used > max && (self.t1.len > 0 || self.t2.len > 0) {
340
+ self.replace(false);
341
+ }
342
+ }
343
+
344
+ old_value
345
+ }
346
+
347
+ fn touch(&mut self, key: &str, ttl_ms: Option<u64>) -> bool {
348
+ if let Some(&idx) = self.map.get(key) {
349
+ let list = self.nodes[idx].list;
350
+ match list {
351
+ ArcList::T1 | ArcList::T2 => {
352
+ let expired = self.nodes[idx].value.as_ref().map_or(true, |e| e.is_expired());
353
+ if expired {
354
+ self.remove_completely(idx);
355
+ self.map.remove(key);
356
+ false
357
+ } else {
358
+ let val = self.nodes[idx].value.as_ref().unwrap().value.clone();
359
+ self.nodes[idx].value = Some(CacheEntry::new(val, ttl_ms));
360
+ self.detach(idx);
361
+ self.attach_to_head(idx, ArcList::T2);
362
+ true
363
+ }
364
+ }
365
+ _ => false
366
+ }
367
+ } else {
368
+ false
272
369
  }
273
370
  }
274
371
 
@@ -293,6 +390,7 @@ impl CacheImpl for ArcCache {
293
390
  self.p = 0;
294
391
  self.hits = 0;
295
392
  self.misses = 0;
393
+ self.bytes_used = 0;
296
394
  }
297
395
 
298
396
  fn stats(&self) -> CacheStats {
@@ -301,12 +399,12 @@ impl CacheImpl for ArcCache {
301
399
  misses: self.misses,
302
400
  capacity: self.capacity,
303
401
  size: self.t1.len + self.t2.len,
402
+ bytes_used: self.bytes_used,
304
403
  }
305
404
  }
306
405
 
307
406
  fn keys(&self) -> Vec<String> {
308
407
  let mut result = Vec::new();
309
- // Return active keys in T1
310
408
  let mut curr = self.t1.head;
311
409
  while let Some(idx) = curr {
312
410
  let node = &self.nodes[idx];
@@ -316,7 +414,6 @@ impl CacheImpl for ArcCache {
316
414
  }
317
415
  curr = node.next;
318
416
  }
319
- // Return active keys in T2
320
417
  let mut curr = self.t2.head;
321
418
  while let Some(idx) = curr {
322
419
  let node = &self.nodes[idx];
@@ -330,6 +427,7 @@ impl CacheImpl for ArcCache {
330
427
  }
331
428
  }
332
429
 
430
+
333
431
  #[cfg(test)]
334
432
  mod tests {
335
433
  use super::*;
@@ -10,6 +10,8 @@ struct LruNode {
10
10
 
11
11
  pub struct LruCache {
12
12
  capacity: usize,
13
+ max_bytes: Option<usize>,
14
+ bytes_used: usize,
13
15
  map: HashMap<String, usize>,
14
16
  nodes: Vec<LruNode>,
15
17
  free_nodes: Vec<usize>,
@@ -21,10 +23,16 @@ pub struct LruCache {
21
23
 
22
24
  impl LruCache {
23
25
  pub fn new(capacity: usize) -> Self {
26
+ Self::new_with_max_bytes(capacity, None)
27
+ }
28
+
29
+ pub fn new_with_max_bytes(capacity: usize, max_bytes: Option<usize>) -> Self {
24
30
  Self {
25
31
  capacity,
26
- map: HashMap::with_capacity(capacity),
27
- nodes: Vec::with_capacity(capacity),
32
+ max_bytes,
33
+ bytes_used: 0,
34
+ map: HashMap::with_capacity(capacity.min(1024)),
35
+ nodes: Vec::with_capacity(capacity.min(1024)),
28
36
  free_nodes: Vec::new(),
29
37
  head: None,
30
38
  tail: None,
@@ -87,7 +95,8 @@ impl CacheImpl for LruCache {
87
95
  fn get(&mut self, key: &str) -> Option<Vec<u8>> {
88
96
  if let Some(&node_idx) = self.map.get(key) {
89
97
  if self.nodes[node_idx].entry.is_expired() {
90
- self.remove_node(node_idx);
98
+ let (evicted_key, evicted_entry) = self.remove_node(node_idx);
99
+ self.bytes_used -= evicted_key.len() + evicted_entry.value.len();
91
100
  self.map.remove(key);
92
101
  self.misses += 1;
93
102
  None
@@ -102,25 +111,61 @@ impl CacheImpl for LruCache {
102
111
  }
103
112
  }
104
113
 
114
+ fn peek(&mut self, key: &str) -> Option<Vec<u8>> {
115
+ if let Some(&node_idx) = self.map.get(key) {
116
+ if self.nodes[node_idx].entry.is_expired() {
117
+ let (evicted_key, evicted_entry) = self.remove_node(node_idx);
118
+ self.bytes_used -= evicted_key.len() + evicted_entry.value.len();
119
+ self.map.remove(key);
120
+ self.misses += 1;
121
+ None
122
+ } else {
123
+ self.hits += 1;
124
+ Some(self.nodes[node_idx].entry.value.clone())
125
+ }
126
+ } else {
127
+ self.misses += 1;
128
+ None
129
+ }
130
+ }
131
+
132
+ fn has(&mut self, key: &str) -> bool {
133
+ if let Some(&node_idx) = self.map.get(key) {
134
+ if self.nodes[node_idx].entry.is_expired() {
135
+ let (evicted_key, evicted_entry) = self.remove_node(node_idx);
136
+ self.bytes_used -= evicted_key.len() + evicted_entry.value.len();
137
+ self.map.remove(key);
138
+ false
139
+ } else {
140
+ true
141
+ }
142
+ } else {
143
+ false
144
+ }
145
+ }
146
+
105
147
  fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>> {
148
+ let new_bytes = key.len() + value.len();
149
+ let mut old_value = None;
150
+
106
151
  if let Some(&node_idx) = self.map.get(key) {
107
- // Key exists, update it
108
- let old_value = std::mem::replace(
152
+ let old_entry = std::mem::replace(
109
153
  &mut self.nodes[node_idx].entry,
110
154
  CacheEntry::new(value, ttl_ms),
111
- ).value;
155
+ );
156
+ let old_bytes = key.len() + old_entry.value.len();
157
+ self.bytes_used = self.bytes_used + new_bytes - old_bytes;
112
158
  self.move_to_head(node_idx);
113
- Some(old_value)
159
+ old_value = Some(old_entry.value);
114
160
  } else {
115
- // If full, evict the LRU (tail)
116
161
  if self.map.len() >= self.capacity && self.capacity > 0 {
117
162
  if let Some(t_idx) = self.tail {
118
- let (evicted_key, _) = self.remove_node(t_idx);
163
+ let (evicted_key, evicted_entry) = self.remove_node(t_idx);
164
+ self.bytes_used -= evicted_key.len() + evicted_entry.value.len();
119
165
  self.map.remove(&evicted_key);
120
166
  }
121
167
  }
122
168
 
123
- // Insert new entry
124
169
  let node_idx = if let Some(idx) = self.free_nodes.pop() {
125
170
  self.nodes[idx].key = key.to_string();
126
171
  self.nodes[idx].entry = CacheEntry::new(value, ttl_ms);
@@ -136,15 +181,50 @@ impl CacheImpl for LruCache {
136
181
  idx
137
182
  };
138
183
 
184
+ self.bytes_used += new_bytes;
139
185
  self.attach_to_head(node_idx);
140
186
  self.map.insert(key.to_string(), node_idx);
141
- None
187
+ }
188
+
189
+ if let Some(max) = self.max_bytes {
190
+ while self.bytes_used > max && !self.map.is_empty() {
191
+ if let Some(t_idx) = self.tail {
192
+ let (evicted_key, evicted_entry) = self.remove_node(t_idx);
193
+ self.bytes_used -= evicted_key.len() + evicted_entry.value.len();
194
+ self.map.remove(&evicted_key);
195
+ } else {
196
+ break;
197
+ }
198
+ }
199
+ }
200
+
201
+ old_value
202
+ }
203
+
204
+ fn touch(&mut self, key: &str, ttl_ms: Option<u64>) -> bool {
205
+ if let Some(&node_idx) = self.map.get(key) {
206
+ if self.nodes[node_idx].entry.is_expired() {
207
+ let (evicted_key, evicted_entry) = self.remove_node(node_idx);
208
+ self.bytes_used -= evicted_key.len() + evicted_entry.value.len();
209
+ self.map.remove(key);
210
+ false
211
+ } else {
212
+ self.nodes[node_idx].entry = CacheEntry::new(
213
+ self.nodes[node_idx].entry.value.clone(),
214
+ ttl_ms,
215
+ );
216
+ self.move_to_head(node_idx);
217
+ true
218
+ }
219
+ } else {
220
+ false
142
221
  }
143
222
  }
144
223
 
145
224
  fn delete(&mut self, key: &str) -> bool {
146
225
  if let Some(&node_idx) = self.map.get(key) {
147
- self.remove_node(node_idx);
226
+ let (evicted_key, evicted_entry) = self.remove_node(node_idx);
227
+ self.bytes_used -= evicted_key.len() + evicted_entry.value.len();
148
228
  self.map.remove(key);
149
229
  true
150
230
  } else {
@@ -160,6 +240,7 @@ impl CacheImpl for LruCache {
160
240
  self.tail = None;
161
241
  self.hits = 0;
162
242
  self.misses = 0;
243
+ self.bytes_used = 0;
163
244
  }
164
245
 
165
246
  fn stats(&self) -> CacheStats {
@@ -168,6 +249,7 @@ impl CacheImpl for LruCache {
168
249
  misses: self.misses,
169
250
  capacity: self.capacity,
170
251
  size: self.map.len(),
252
+ bytes_used: self.bytes_used,
171
253
  }
172
254
  }
173
255
 
@@ -6,6 +6,7 @@ pub struct CacheStats {
6
6
  pub misses: u64,
7
7
  pub capacity: usize,
8
8
  pub size: usize,
9
+ pub bytes_used: usize,
9
10
  }
10
11
 
11
12
  pub struct CacheEntry {
@@ -30,7 +31,10 @@ impl CacheEntry {
30
31
 
31
32
  pub trait CacheImpl: Send + Sync {
32
33
  fn get(&mut self, key: &str) -> Option<Vec<u8>>;
34
+ fn peek(&mut self, key: &str) -> Option<Vec<u8>>;
35
+ fn has(&mut self, key: &str) -> bool;
33
36
  fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>>;
37
+ fn touch(&mut self, key: &str, ttl_ms: Option<u64>) -> bool;
34
38
  fn delete(&mut self, key: &str) -> bool;
35
39
  fn clear(&mut self);
36
40
  fn stats(&self) -> CacheStats;