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
@@ -137,6 +137,8 @@ impl TinyLfuListMetadata {
137
137
 
138
138
  pub struct TinyLfuCache {
139
139
  capacity: usize,
140
+ max_bytes: Option<usize>,
141
+ bytes_used: usize,
140
142
  map: HashMap<String, usize>,
141
143
  nodes: Vec<TinyLfuNode>,
142
144
  free_nodes: Vec<usize>,
@@ -152,11 +154,10 @@ pub struct TinyLfuCache {
152
154
 
153
155
  impl TinyLfuCache {
154
156
  pub fn new(capacity: usize) -> Self {
155
- // Segment capacities:
156
- // Window: 1% (min 1)
157
- // Main: 99%
158
- // Probation: 20% of Main (min 1 if Main capacity > 0)
159
- // Protected: 80% of Main
157
+ Self::new_with_max_bytes(capacity, None)
158
+ }
159
+
160
+ pub fn new_with_max_bytes(capacity: usize, max_bytes: Option<usize>) -> Self {
160
161
  let window_cap = (capacity / 100).max(1);
161
162
  let main_cap = capacity.saturating_sub(window_cap);
162
163
 
@@ -170,6 +171,8 @@ impl TinyLfuCache {
170
171
 
171
172
  Self {
172
173
  capacity,
174
+ max_bytes,
175
+ bytes_used: 0,
173
176
  map: HashMap::with_capacity(capacity),
174
177
  nodes: Vec::with_capacity(capacity),
175
178
  free_nodes: Vec::new(),
@@ -182,6 +185,11 @@ impl TinyLfuCache {
182
185
  }
183
186
  }
184
187
 
188
+ fn node_bytes(&self, idx: usize) -> usize {
189
+ let node = &self.nodes[idx];
190
+ node.key.len() + node.entry.value.len()
191
+ }
192
+
185
193
  fn detach(&mut self, idx: usize) {
186
194
  let prev = self.nodes[idx].prev;
187
195
  let next = self.nodes[idx].next;
@@ -232,6 +240,8 @@ impl TinyLfuCache {
232
240
  }
233
241
 
234
242
  fn remove_completely(&mut self, idx: usize) -> String {
243
+ let bytes = self.node_bytes(idx);
244
+ self.bytes_used -= bytes;
235
245
  self.detach(idx);
236
246
  self.free_nodes.push(idx);
237
247
  let key = std::mem::take(&mut self.nodes[idx].key);
@@ -246,8 +256,8 @@ impl CacheImpl for TinyLfuCache {
246
256
 
247
257
  if let Some(&idx) = self.map.get(key) {
248
258
  if self.nodes[idx].entry.is_expired() {
249
- self.remove_completely(idx);
250
- self.map.remove(key);
259
+ let evicted_key = self.remove_completely(idx);
260
+ self.map.remove(&evicted_key);
251
261
  self.misses += 1;
252
262
  None
253
263
  } else {
@@ -258,11 +268,9 @@ impl CacheImpl for TinyLfuCache {
258
268
  self.attach_to_head(idx, TinyLfuList::Window);
259
269
  }
260
270
  TinyLfuList::Probation => {
261
- // Promote to Protected segment
262
271
  self.detach(idx);
263
272
  self.attach_to_head(idx, TinyLfuList::Protected);
264
273
 
265
- // Handle potential overflow of Protected segment
266
274
  if self.protected.len > self.protected.capacity {
267
275
  if let Some(prot_tail) = self.protected.tail {
268
276
  self.detach(prot_tail);
@@ -284,17 +292,49 @@ impl CacheImpl for TinyLfuCache {
284
292
  }
285
293
  }
286
294
 
295
+ fn peek(&mut self, key: &str) -> Option<Vec<u8>> {
296
+ if let Some(&idx) = self.map.get(key) {
297
+ if self.nodes[idx].entry.is_expired() {
298
+ let evicted_key = self.remove_completely(idx);
299
+ self.map.remove(&evicted_key);
300
+ self.misses += 1;
301
+ None
302
+ } else {
303
+ self.hits += 1;
304
+ Some(self.nodes[idx].entry.value.clone())
305
+ }
306
+ } else {
307
+ self.misses += 1;
308
+ None
309
+ }
310
+ }
311
+
312
+ fn has(&mut self, key: &str) -> bool {
313
+ if let Some(&idx) = self.map.get(key) {
314
+ if self.nodes[idx].entry.is_expired() {
315
+ let evicted_key = self.remove_completely(idx);
316
+ self.map.remove(&evicted_key);
317
+ false
318
+ } else {
319
+ true
320
+ }
321
+ } else {
322
+ false
323
+ }
324
+ }
325
+
287
326
  fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>> {
288
327
  self.sketch.increment(key);
289
328
 
329
+ let new_entry = CacheEntry::new(value, ttl_ms);
330
+ let new_bytes = key.len() + new_entry.value.len();
331
+ let mut old_value = None;
332
+
290
333
  if let Some(&idx) = self.map.get(key) {
291
- // Key exists. Update value.
292
- let old_value = std::mem::replace(
293
- &mut self.nodes[idx].entry,
294
- CacheEntry::new(value, ttl_ms),
295
- ).value;
334
+ let old_bytes = self.node_bytes(idx);
335
+ old_value = Some(std::mem::replace(&mut self.nodes[idx].entry, new_entry).value);
336
+ self.bytes_used = self.bytes_used + new_bytes - old_bytes;
296
337
 
297
- // Perform promotion/MRU updates identical to `get`
298
338
  let list = self.nodes[idx].list;
299
339
  match list {
300
340
  TinyLfuList::Window => {
@@ -317,23 +357,20 @@ impl CacheImpl for TinyLfuCache {
317
357
  self.attach_to_head(idx, TinyLfuList::Protected);
318
358
  }
319
359
  }
320
- Some(old_value)
321
360
  } else {
322
- // Check if we have capacity at all
323
361
  if self.capacity == 0 {
324
362
  return None;
325
363
  }
326
364
 
327
- // Create new node inside Window segment
328
365
  let new_idx = if let Some(free_idx) = self.free_nodes.pop() {
329
366
  self.nodes[free_idx].key = key.to_string();
330
- self.nodes[free_idx].entry = CacheEntry::new(value, ttl_ms);
367
+ self.nodes[free_idx].entry = new_entry;
331
368
  free_idx
332
369
  } else {
333
370
  let free_idx = self.nodes.len();
334
371
  self.nodes.push(TinyLfuNode {
335
372
  key: key.to_string(),
336
- entry: CacheEntry::new(value, ttl_ms),
373
+ entry: new_entry,
337
374
  list: TinyLfuList::Window,
338
375
  prev: None,
339
376
  next: None,
@@ -341,13 +378,12 @@ impl CacheImpl for TinyLfuCache {
341
378
  free_idx
342
379
  };
343
380
 
381
+ self.bytes_used += new_bytes;
344
382
  self.attach_to_head(new_idx, TinyLfuList::Window);
345
383
  self.map.insert(key.to_string(), new_idx);
346
384
 
347
- // If Window segment overflows
348
385
  if self.window.len > self.window.capacity {
349
386
  if let Some(win_tail) = self.window.tail {
350
- // Candidate to move to Main cache (Probation segment)
351
387
  let candidate_idx = win_tail;
352
388
  self.detach(candidate_idx);
353
389
 
@@ -355,10 +391,8 @@ impl CacheImpl for TinyLfuCache {
355
391
  if main_has_capacity {
356
392
  let total_size = self.window.len + self.probation.len + self.protected.len;
357
393
  if total_size + 1 <= self.capacity {
358
- // Cache is not full yet. Admit candidate directly.
359
394
  self.attach_to_head(candidate_idx, TinyLfuList::Probation);
360
395
  } else {
361
- // Cache is full. Compare candidate and victim.
362
396
  if let Some(prob_tail) = self.probation.tail {
363
397
  let victim_idx = prob_tail;
364
398
 
@@ -369,29 +403,60 @@ impl CacheImpl for TinyLfuCache {
369
403
  let victim_freq = self.sketch.estimate(victim_key);
370
404
 
371
405
  if candidate_freq > victim_freq {
372
- // Candidate wins! Evict victim, admit candidate.
373
406
  let evicted_key = self.remove_completely(victim_idx);
374
407
  self.map.remove(&evicted_key);
375
408
  self.attach_to_head(candidate_idx, TinyLfuList::Probation);
376
409
  } else {
377
- // Candidate loses! Evict candidate, keep victim.
378
410
  let evicted_key = self.remove_completely(candidate_idx);
379
411
  self.map.remove(&evicted_key);
380
412
  }
381
413
  } else {
382
- // Fallback if no victim (should not happen if len == capacity > 0)
383
414
  self.attach_to_head(candidate_idx, TinyLfuList::Probation);
384
415
  }
385
416
  }
386
417
  } else {
387
- // Main Cache capacity is 0 (capacity == 1 edge case).
388
- // Candidate is evicted directly.
389
418
  let evicted_key = self.remove_completely(candidate_idx);
390
419
  self.map.remove(&evicted_key);
391
420
  }
392
421
  }
393
422
  }
394
- None
423
+ }
424
+
425
+ if let Some(max) = self.max_bytes {
426
+ while self.bytes_used > max && (self.probation.len > 0 || self.window.len > 0) {
427
+ if let Some(prob_tail) = self.probation.tail {
428
+ let evicted_key = self.remove_completely(prob_tail);
429
+ self.map.remove(&evicted_key);
430
+ } else if let Some(win_tail) = self.window.tail {
431
+ let evicted_key = self.remove_completely(win_tail);
432
+ self.map.remove(&evicted_key);
433
+ } else {
434
+ break;
435
+ }
436
+ }
437
+ }
438
+
439
+ old_value
440
+ }
441
+
442
+ fn touch(&mut self, key: &str, ttl_ms: Option<u64>) -> bool {
443
+ if let Some(&idx) = self.map.get(key) {
444
+ if self.nodes[idx].entry.is_expired() {
445
+ let evicted_key = self.remove_completely(idx);
446
+ self.map.remove(&evicted_key);
447
+ false
448
+ } else {
449
+ self.nodes[idx].entry = CacheEntry::new(
450
+ self.nodes[idx].entry.value.clone(),
451
+ ttl_ms,
452
+ );
453
+ let list = self.nodes[idx].list;
454
+ self.detach(idx);
455
+ self.attach_to_head(idx, list);
456
+ true
457
+ }
458
+ } else {
459
+ false
395
460
  }
396
461
  }
397
462
 
@@ -420,10 +485,9 @@ impl CacheImpl for TinyLfuCache {
420
485
  self.protected.len = 0;
421
486
  self.hits = 0;
422
487
  self.misses = 0;
423
- // FrequencySketch is not cleared completely, but can be decay'd or just left as is.
424
- // Actually, clearing table is cleaner:
425
488
  self.sketch.table.fill(0);
426
489
  self.sketch.additions = 0;
490
+ self.bytes_used = 0;
427
491
  }
428
492
 
429
493
  fn stats(&self) -> CacheStats {
@@ -432,12 +496,12 @@ impl CacheImpl for TinyLfuCache {
432
496
  misses: self.misses,
433
497
  capacity: self.capacity,
434
498
  size: self.window.len + self.probation.len + self.protected.len,
499
+ bytes_used: self.bytes_used,
435
500
  }
436
501
  }
437
502
 
438
503
  fn keys(&self) -> Vec<String> {
439
504
  let mut result = Vec::new();
440
- // Return window keys
441
505
  let mut curr = self.window.head;
442
506
  while let Some(idx) = curr {
443
507
  let node = &self.nodes[idx];
@@ -446,7 +510,6 @@ impl CacheImpl for TinyLfuCache {
446
510
  }
447
511
  curr = node.next;
448
512
  }
449
- // Return probation keys
450
513
  let mut curr = self.probation.head;
451
514
  while let Some(idx) = curr {
452
515
  let node = &self.nodes[idx];
@@ -455,7 +518,6 @@ impl CacheImpl for TinyLfuCache {
455
518
  }
456
519
  curr = node.next;
457
520
  }
458
- // Return protected keys
459
521
  let mut curr = self.protected.head;
460
522
  while let Some(idx) = curr {
461
523
  let node = &self.nodes[idx];
@@ -468,6 +530,7 @@ impl CacheImpl for TinyLfuCache {
468
530
  }
469
531
  }
470
532
 
533
+
471
534
  #[cfg(test)]
472
535
  mod tests {
473
536
  use super::*;