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.
- package/.github/workflows/CI.yml +22 -1
- package/Cargo.toml +2 -2
- package/artifacts/bindings-aarch64-apple-darwin/index.d.ts +37 -25
- package/artifacts/bindings-aarch64-apple-darwin/index.js +165 -295
- package/artifacts/bindings-aarch64-unknown-linux-musl/index.d.ts +42 -0
- package/artifacts/bindings-aarch64-unknown-linux-musl/index.js +186 -0
- package/artifacts/bindings-x86_64-apple-darwin/index.d.ts +37 -25
- package/artifacts/bindings-x86_64-apple-darwin/index.js +165 -295
- package/artifacts/bindings-x86_64-pc-windows-msvc/index.d.ts +42 -30
- package/artifacts/bindings-x86_64-pc-windows-msvc/index.js +186 -316
- package/artifacts/bindings-x86_64-unknown-linux-gnu/index.d.ts +37 -25
- package/artifacts/bindings-x86_64-unknown-linux-gnu/index.js +165 -295
- package/artifacts/bindings-x86_64-unknown-linux-musl/index.d.ts +42 -0
- package/artifacts/bindings-x86_64-unknown-linux-musl/index.js +186 -0
- package/benchmarks/crossover_bench.js +139 -0
- package/docs/.vitepress/config.js +35 -0
- package/docs/guide/api.md +216 -0
- package/docs/guide/architecture.md +58 -0
- package/docs/guide/benchmarks.md +48 -0
- package/docs/guide/getting-started.md +74 -0
- package/docs/index.md +23 -0
- package/index.d.ts +37 -25
- package/index.js +165 -295
- package/package.json +21 -12
- package/src/algorithms/arc.rs +123 -25
- package/src/algorithms/lru.rs +94 -12
- package/src/algorithms/mod.rs +4 -0
- package/src/algorithms/tinylfu.rs +98 -35
- package/src/cache.rs +289 -94
- 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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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::*;
|