offheap 0.1.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 +115 -0
- package/Cargo.toml +35 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +161 -0
- package/artifacts/bindings-aarch64-apple-darwin/index.d.ts +30 -0
- package/artifacts/bindings-aarch64-apple-darwin/index.js +316 -0
- package/artifacts/bindings-x86_64-apple-darwin/index.d.ts +30 -0
- package/artifacts/bindings-x86_64-apple-darwin/index.js +316 -0
- package/artifacts/bindings-x86_64-pc-windows-msvc/index.d.ts +30 -0
- package/artifacts/bindings-x86_64-pc-windows-msvc/index.js +316 -0
- package/artifacts/bindings-x86_64-unknown-linux-gnu/index.d.ts +30 -0
- package/artifacts/bindings-x86_64-unknown-linux-gnu/index.js +316 -0
- package/benches/rust_bench.rs +96 -0
- package/benchmarks/js_bench.js +67 -0
- package/build.rs +3 -0
- package/index.d.ts +30 -0
- package/index.js +316 -0
- package/package.json +35 -0
- package/src/algorithms/arc.rs +355 -0
- package/src/algorithms/lru.rs +212 -0
- package/src/algorithms/mod.rs +42 -0
- package/src/algorithms/tinylfu.rs +499 -0
- package/src/cache.rs +172 -0
- package/src/lib.rs +6 -0
- package/src/manager.rs +50 -0
- package/tests/index.test.js +135 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use super::{CacheImpl, CacheEntry, CacheStats};
|
|
3
|
+
|
|
4
|
+
// Count-Min Sketch with 4-bit counters and aging.
|
|
5
|
+
// Sized dynamically relative to cache capacity.
|
|
6
|
+
struct FrequencySketch {
|
|
7
|
+
table: Vec<u8>,
|
|
8
|
+
mask: usize,
|
|
9
|
+
sample_size: usize,
|
|
10
|
+
additions: usize,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
impl FrequencySketch {
|
|
14
|
+
fn new(capacity: usize) -> Self {
|
|
15
|
+
// Number of counters: next power of 2 of capacity * 4, min 128
|
|
16
|
+
let counters = (capacity * 4).next_power_of_two().max(128);
|
|
17
|
+
let bytes = counters / 2;
|
|
18
|
+
Self {
|
|
19
|
+
table: vec![0; bytes],
|
|
20
|
+
mask: counters - 1,
|
|
21
|
+
sample_size: (capacity * 10).max(1024),
|
|
22
|
+
additions: 0,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn increment(&mut self, key: &str) {
|
|
27
|
+
let hash = seahash::hash(key.as_bytes());
|
|
28
|
+
let h1 = (hash & 0xFFFFFFFF) as usize;
|
|
29
|
+
let h2 = (hash >> 32) as usize;
|
|
30
|
+
|
|
31
|
+
let mut added = false;
|
|
32
|
+
for i in 0..4 {
|
|
33
|
+
let idx = (h1.wrapping_add(i * h2)) & self.mask;
|
|
34
|
+
if self.increment_at(idx) {
|
|
35
|
+
added = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if added {
|
|
40
|
+
self.additions += 1;
|
|
41
|
+
if self.additions >= self.sample_size {
|
|
42
|
+
self.decay();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn increment_at(&mut self, idx: usize) -> bool {
|
|
48
|
+
let byte_idx = idx / 2;
|
|
49
|
+
let is_high = idx % 2 == 1;
|
|
50
|
+
let val = self.table[byte_idx];
|
|
51
|
+
|
|
52
|
+
if is_high {
|
|
53
|
+
let count = val >> 4;
|
|
54
|
+
if count < 15 {
|
|
55
|
+
self.table[byte_idx] = (val & 0x0F) | ((count + 1) << 4);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
let count = val & 0x0F;
|
|
60
|
+
if count < 15 {
|
|
61
|
+
self.table[byte_idx] = (val & 0xF0) | (count + 1);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn estimate(&self, key: &str) -> u8 {
|
|
69
|
+
let hash = seahash::hash(key.as_bytes());
|
|
70
|
+
let h1 = (hash & 0xFFFFFFFF) as usize;
|
|
71
|
+
let h2 = (hash >> 32) as usize;
|
|
72
|
+
|
|
73
|
+
let mut min_val = 15;
|
|
74
|
+
for i in 0..4 {
|
|
75
|
+
let idx = (h1.wrapping_add(i * h2)) & self.mask;
|
|
76
|
+
let val = self.counter_at(idx);
|
|
77
|
+
if val < min_val {
|
|
78
|
+
min_val = val;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
min_val
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn counter_at(&self, idx: usize) -> u8 {
|
|
85
|
+
let byte_idx = idx / 2;
|
|
86
|
+
let is_high = idx % 2 == 1;
|
|
87
|
+
let val = self.table[byte_idx];
|
|
88
|
+
if is_high {
|
|
89
|
+
val >> 4
|
|
90
|
+
} else {
|
|
91
|
+
val & 0x0F
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn decay(&mut self) {
|
|
96
|
+
for val in self.table.iter_mut() {
|
|
97
|
+
let high = (*val >> 4) >> 1;
|
|
98
|
+
let low = (*val & 0x0F) >> 1;
|
|
99
|
+
*val = (high << 4) | low;
|
|
100
|
+
}
|
|
101
|
+
self.additions = 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[derive(Copy, Clone, PartialEq, Debug)]
|
|
106
|
+
enum TinyLfuList {
|
|
107
|
+
Window,
|
|
108
|
+
Probation,
|
|
109
|
+
Protected,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
struct TinyLfuNode {
|
|
113
|
+
key: String,
|
|
114
|
+
entry: CacheEntry,
|
|
115
|
+
list: TinyLfuList,
|
|
116
|
+
prev: Option<usize>,
|
|
117
|
+
next: Option<usize>,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
struct TinyLfuListMetadata {
|
|
121
|
+
head: Option<usize>,
|
|
122
|
+
tail: Option<usize>,
|
|
123
|
+
len: usize,
|
|
124
|
+
capacity: usize,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
impl TinyLfuListMetadata {
|
|
128
|
+
fn new(capacity: usize) -> Self {
|
|
129
|
+
Self {
|
|
130
|
+
head: None,
|
|
131
|
+
tail: None,
|
|
132
|
+
len: 0,
|
|
133
|
+
capacity,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub struct TinyLfuCache {
|
|
139
|
+
capacity: usize,
|
|
140
|
+
map: HashMap<String, usize>,
|
|
141
|
+
nodes: Vec<TinyLfuNode>,
|
|
142
|
+
free_nodes: Vec<usize>,
|
|
143
|
+
|
|
144
|
+
window: TinyLfuListMetadata,
|
|
145
|
+
probation: TinyLfuListMetadata,
|
|
146
|
+
protected: TinyLfuListMetadata,
|
|
147
|
+
|
|
148
|
+
sketch: FrequencySketch,
|
|
149
|
+
hits: u64,
|
|
150
|
+
misses: u64,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
impl TinyLfuCache {
|
|
154
|
+
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
|
|
160
|
+
let window_cap = (capacity / 100).max(1);
|
|
161
|
+
let main_cap = capacity.saturating_sub(window_cap);
|
|
162
|
+
|
|
163
|
+
let (probation_cap, protected_cap) = if main_cap > 0 {
|
|
164
|
+
let prob = (main_cap * 20 / 100).max(1);
|
|
165
|
+
let prot = main_cap.saturating_sub(prob);
|
|
166
|
+
(prob, prot)
|
|
167
|
+
} else {
|
|
168
|
+
(0, 0)
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
Self {
|
|
172
|
+
capacity,
|
|
173
|
+
map: HashMap::with_capacity(capacity),
|
|
174
|
+
nodes: Vec::with_capacity(capacity),
|
|
175
|
+
free_nodes: Vec::new(),
|
|
176
|
+
window: TinyLfuListMetadata::new(window_cap),
|
|
177
|
+
probation: TinyLfuListMetadata::new(probation_cap),
|
|
178
|
+
protected: TinyLfuListMetadata::new(protected_cap),
|
|
179
|
+
sketch: FrequencySketch::new(capacity),
|
|
180
|
+
hits: 0,
|
|
181
|
+
misses: 0,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn detach(&mut self, idx: usize) {
|
|
186
|
+
let prev = self.nodes[idx].prev;
|
|
187
|
+
let next = self.nodes[idx].next;
|
|
188
|
+
let list = self.nodes[idx].list;
|
|
189
|
+
|
|
190
|
+
let meta = match list {
|
|
191
|
+
TinyLfuList::Window => &mut self.window,
|
|
192
|
+
TinyLfuList::Probation => &mut self.probation,
|
|
193
|
+
TinyLfuList::Protected => &mut self.protected,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if let Some(p) = prev {
|
|
197
|
+
self.nodes[p].next = next;
|
|
198
|
+
} else {
|
|
199
|
+
meta.head = next;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if let Some(n) = next {
|
|
203
|
+
self.nodes[n].prev = prev;
|
|
204
|
+
} else {
|
|
205
|
+
meta.tail = prev;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
meta.len -= 1;
|
|
209
|
+
self.nodes[idx].prev = None;
|
|
210
|
+
self.nodes[idx].next = None;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn attach_to_head(&mut self, idx: usize, list: TinyLfuList) {
|
|
214
|
+
self.nodes[idx].list = list;
|
|
215
|
+
let meta = match list {
|
|
216
|
+
TinyLfuList::Window => &mut self.window,
|
|
217
|
+
TinyLfuList::Probation => &mut self.probation,
|
|
218
|
+
TinyLfuList::Protected => &mut self.protected,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
self.nodes[idx].prev = None;
|
|
222
|
+
self.nodes[idx].next = meta.head;
|
|
223
|
+
|
|
224
|
+
if let Some(h) = meta.head {
|
|
225
|
+
self.nodes[h].prev = Some(idx);
|
|
226
|
+
} else {
|
|
227
|
+
meta.tail = Some(idx);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
meta.head = Some(idx);
|
|
231
|
+
meta.len += 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn remove_completely(&mut self, idx: usize) -> String {
|
|
235
|
+
self.detach(idx);
|
|
236
|
+
self.free_nodes.push(idx);
|
|
237
|
+
let key = std::mem::take(&mut self.nodes[idx].key);
|
|
238
|
+
self.nodes[idx].entry = CacheEntry::new(Vec::new(), None);
|
|
239
|
+
key
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
impl CacheImpl for TinyLfuCache {
|
|
244
|
+
fn get(&mut self, key: &str) -> Option<Vec<u8>> {
|
|
245
|
+
self.sketch.increment(key);
|
|
246
|
+
|
|
247
|
+
if let Some(&idx) = self.map.get(key) {
|
|
248
|
+
if self.nodes[idx].entry.is_expired() {
|
|
249
|
+
self.remove_completely(idx);
|
|
250
|
+
self.map.remove(key);
|
|
251
|
+
self.misses += 1;
|
|
252
|
+
None
|
|
253
|
+
} else {
|
|
254
|
+
let list = self.nodes[idx].list;
|
|
255
|
+
match list {
|
|
256
|
+
TinyLfuList::Window => {
|
|
257
|
+
self.detach(idx);
|
|
258
|
+
self.attach_to_head(idx, TinyLfuList::Window);
|
|
259
|
+
}
|
|
260
|
+
TinyLfuList::Probation => {
|
|
261
|
+
// Promote to Protected segment
|
|
262
|
+
self.detach(idx);
|
|
263
|
+
self.attach_to_head(idx, TinyLfuList::Protected);
|
|
264
|
+
|
|
265
|
+
// Handle potential overflow of Protected segment
|
|
266
|
+
if self.protected.len > self.protected.capacity {
|
|
267
|
+
if let Some(prot_tail) = self.protected.tail {
|
|
268
|
+
self.detach(prot_tail);
|
|
269
|
+
self.attach_to_head(prot_tail, TinyLfuList::Probation);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
TinyLfuList::Protected => {
|
|
274
|
+
self.detach(idx);
|
|
275
|
+
self.attach_to_head(idx, TinyLfuList::Protected);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
self.hits += 1;
|
|
279
|
+
Some(self.nodes[idx].entry.value.clone())
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
self.misses += 1;
|
|
283
|
+
None
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn set(&mut self, key: &str, value: Vec<u8>, ttl_ms: Option<u64>) -> Option<Vec<u8>> {
|
|
288
|
+
self.sketch.increment(key);
|
|
289
|
+
|
|
290
|
+
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;
|
|
296
|
+
|
|
297
|
+
// Perform promotion/MRU updates identical to `get`
|
|
298
|
+
let list = self.nodes[idx].list;
|
|
299
|
+
match list {
|
|
300
|
+
TinyLfuList::Window => {
|
|
301
|
+
self.detach(idx);
|
|
302
|
+
self.attach_to_head(idx, TinyLfuList::Window);
|
|
303
|
+
}
|
|
304
|
+
TinyLfuList::Probation => {
|
|
305
|
+
self.detach(idx);
|
|
306
|
+
self.attach_to_head(idx, TinyLfuList::Protected);
|
|
307
|
+
|
|
308
|
+
if self.protected.len > self.protected.capacity {
|
|
309
|
+
if let Some(prot_tail) = self.protected.tail {
|
|
310
|
+
self.detach(prot_tail);
|
|
311
|
+
self.attach_to_head(prot_tail, TinyLfuList::Probation);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
TinyLfuList::Protected => {
|
|
316
|
+
self.detach(idx);
|
|
317
|
+
self.attach_to_head(idx, TinyLfuList::Protected);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
Some(old_value)
|
|
321
|
+
} else {
|
|
322
|
+
// Check if we have capacity at all
|
|
323
|
+
if self.capacity == 0 {
|
|
324
|
+
return None;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Create new node inside Window segment
|
|
328
|
+
let new_idx = if let Some(free_idx) = self.free_nodes.pop() {
|
|
329
|
+
self.nodes[free_idx].key = key.to_string();
|
|
330
|
+
self.nodes[free_idx].entry = CacheEntry::new(value, ttl_ms);
|
|
331
|
+
free_idx
|
|
332
|
+
} else {
|
|
333
|
+
let free_idx = self.nodes.len();
|
|
334
|
+
self.nodes.push(TinyLfuNode {
|
|
335
|
+
key: key.to_string(),
|
|
336
|
+
entry: CacheEntry::new(value, ttl_ms),
|
|
337
|
+
list: TinyLfuList::Window,
|
|
338
|
+
prev: None,
|
|
339
|
+
next: None,
|
|
340
|
+
});
|
|
341
|
+
free_idx
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
self.attach_to_head(new_idx, TinyLfuList::Window);
|
|
345
|
+
self.map.insert(key.to_string(), new_idx);
|
|
346
|
+
|
|
347
|
+
// If Window segment overflows
|
|
348
|
+
if self.window.len > self.window.capacity {
|
|
349
|
+
if let Some(win_tail) = self.window.tail {
|
|
350
|
+
// Candidate to move to Main cache (Probation segment)
|
|
351
|
+
let candidate_idx = win_tail;
|
|
352
|
+
self.detach(candidate_idx);
|
|
353
|
+
|
|
354
|
+
let main_has_capacity = self.probation.capacity > 0;
|
|
355
|
+
if main_has_capacity {
|
|
356
|
+
let total_size = self.window.len + self.probation.len + self.protected.len;
|
|
357
|
+
if total_size + 1 <= self.capacity {
|
|
358
|
+
// Cache is not full yet. Admit candidate directly.
|
|
359
|
+
self.attach_to_head(candidate_idx, TinyLfuList::Probation);
|
|
360
|
+
} else {
|
|
361
|
+
// Cache is full. Compare candidate and victim.
|
|
362
|
+
if let Some(prob_tail) = self.probation.tail {
|
|
363
|
+
let victim_idx = prob_tail;
|
|
364
|
+
|
|
365
|
+
let candidate_key = &self.nodes[candidate_idx].key;
|
|
366
|
+
let victim_key = &self.nodes[victim_idx].key;
|
|
367
|
+
|
|
368
|
+
let candidate_freq = self.sketch.estimate(candidate_key);
|
|
369
|
+
let victim_freq = self.sketch.estimate(victim_key);
|
|
370
|
+
|
|
371
|
+
if candidate_freq > victim_freq {
|
|
372
|
+
// Candidate wins! Evict victim, admit candidate.
|
|
373
|
+
let evicted_key = self.remove_completely(victim_idx);
|
|
374
|
+
self.map.remove(&evicted_key);
|
|
375
|
+
self.attach_to_head(candidate_idx, TinyLfuList::Probation);
|
|
376
|
+
} else {
|
|
377
|
+
// Candidate loses! Evict candidate, keep victim.
|
|
378
|
+
let evicted_key = self.remove_completely(candidate_idx);
|
|
379
|
+
self.map.remove(&evicted_key);
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// Fallback if no victim (should not happen if len == capacity > 0)
|
|
383
|
+
self.attach_to_head(candidate_idx, TinyLfuList::Probation);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// Main Cache capacity is 0 (capacity == 1 edge case).
|
|
388
|
+
// Candidate is evicted directly.
|
|
389
|
+
let evicted_key = self.remove_completely(candidate_idx);
|
|
390
|
+
self.map.remove(&evicted_key);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
None
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fn delete(&mut self, key: &str) -> bool {
|
|
399
|
+
if let Some(&idx) = self.map.get(key) {
|
|
400
|
+
self.remove_completely(idx);
|
|
401
|
+
self.map.remove(key);
|
|
402
|
+
true
|
|
403
|
+
} else {
|
|
404
|
+
false
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fn clear(&mut self) {
|
|
409
|
+
self.map.clear();
|
|
410
|
+
self.nodes.clear();
|
|
411
|
+
self.free_nodes.clear();
|
|
412
|
+
self.window.head = None;
|
|
413
|
+
self.window.tail = None;
|
|
414
|
+
self.window.len = 0;
|
|
415
|
+
self.probation.head = None;
|
|
416
|
+
self.probation.tail = None;
|
|
417
|
+
self.probation.len = 0;
|
|
418
|
+
self.protected.head = None;
|
|
419
|
+
self.protected.tail = None;
|
|
420
|
+
self.protected.len = 0;
|
|
421
|
+
self.hits = 0;
|
|
422
|
+
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
|
+
self.sketch.table.fill(0);
|
|
426
|
+
self.sketch.additions = 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
fn stats(&self) -> CacheStats {
|
|
430
|
+
CacheStats {
|
|
431
|
+
hits: self.hits,
|
|
432
|
+
misses: self.misses,
|
|
433
|
+
capacity: self.capacity,
|
|
434
|
+
size: self.window.len + self.probation.len + self.protected.len,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
fn keys(&self) -> Vec<String> {
|
|
439
|
+
let mut result = Vec::new();
|
|
440
|
+
// Return window keys
|
|
441
|
+
let mut curr = self.window.head;
|
|
442
|
+
while let Some(idx) = curr {
|
|
443
|
+
let node = &self.nodes[idx];
|
|
444
|
+
if !node.entry.is_expired() {
|
|
445
|
+
result.push(node.key.clone());
|
|
446
|
+
}
|
|
447
|
+
curr = node.next;
|
|
448
|
+
}
|
|
449
|
+
// Return probation keys
|
|
450
|
+
let mut curr = self.probation.head;
|
|
451
|
+
while let Some(idx) = curr {
|
|
452
|
+
let node = &self.nodes[idx];
|
|
453
|
+
if !node.entry.is_expired() {
|
|
454
|
+
result.push(node.key.clone());
|
|
455
|
+
}
|
|
456
|
+
curr = node.next;
|
|
457
|
+
}
|
|
458
|
+
// Return protected keys
|
|
459
|
+
let mut curr = self.protected.head;
|
|
460
|
+
while let Some(idx) = curr {
|
|
461
|
+
let node = &self.nodes[idx];
|
|
462
|
+
if !node.entry.is_expired() {
|
|
463
|
+
result.push(node.key.clone());
|
|
464
|
+
}
|
|
465
|
+
curr = node.next;
|
|
466
|
+
}
|
|
467
|
+
result
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[cfg(test)]
|
|
472
|
+
mod tests {
|
|
473
|
+
use super::*;
|
|
474
|
+
|
|
475
|
+
#[test]
|
|
476
|
+
fn test_tinylfu_basic() {
|
|
477
|
+
let mut cache = TinyLfuCache::new(5);
|
|
478
|
+
|
|
479
|
+
// Fill cache
|
|
480
|
+
for i in 0..5 {
|
|
481
|
+
cache.set(&format!("key-{}", i), vec![i], None);
|
|
482
|
+
}
|
|
483
|
+
assert_eq!(cache.stats().size, 5);
|
|
484
|
+
|
|
485
|
+
// Increase frequency of key-0 and key-1
|
|
486
|
+
for _ in 0..3 {
|
|
487
|
+
cache.get("key-0");
|
|
488
|
+
cache.get("key-1");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Add a new key. TinyLFU will compare frequency of candidate vs probation victim.
|
|
492
|
+
// It will evict a low-frequency key.
|
|
493
|
+
cache.set("key-5", vec![5], None);
|
|
494
|
+
|
|
495
|
+
// key-0 and key-1 must remain because of high frequency.
|
|
496
|
+
assert!(cache.get("key-0").is_some());
|
|
497
|
+
assert!(cache.get("key-1").is_some());
|
|
498
|
+
}
|
|
499
|
+
}
|
package/src/cache.rs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
use napi::{Env, JsUnknown, JsBuffer, JsString, Result, ValueType};
|
|
3
|
+
use super::algorithms::{CacheImpl, CacheStats, lru::LruCache, arc::ArcCache, tinylfu::TinyLfuCache};
|
|
4
|
+
|
|
5
|
+
#[napi(object)]
|
|
6
|
+
pub struct CacheConfig {
|
|
7
|
+
pub policy: String,
|
|
8
|
+
pub capacity: u32,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[napi(object)]
|
|
12
|
+
pub struct CacheStatsJs {
|
|
13
|
+
pub hits: f64,
|
|
14
|
+
pub misses: f64,
|
|
15
|
+
pub capacity: f64,
|
|
16
|
+
pub size: f64,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[napi]
|
|
20
|
+
pub struct Cache {
|
|
21
|
+
inner: Arc<parking_lot::Mutex<Box<dyn CacheImpl>>>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Clone for Cache {
|
|
25
|
+
fn clone(&self) -> Self {
|
|
26
|
+
Self {
|
|
27
|
+
inner: Arc::clone(&self.inner),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Cache {
|
|
33
|
+
pub fn from_config(config: CacheConfig) -> Self {
|
|
34
|
+
let capacity = config.capacity as usize;
|
|
35
|
+
let policy_lower = config.policy.to_lowercase();
|
|
36
|
+
let inner: Box<dyn CacheImpl> = match policy_lower.as_str() {
|
|
37
|
+
"arc" => Box::new(ArcCache::new(capacity)),
|
|
38
|
+
"tinylfu" | "w-tinylfu" => Box::new(TinyLfuCache::new(capacity)),
|
|
39
|
+
_ => Box::new(LruCache::new(capacity)),
|
|
40
|
+
};
|
|
41
|
+
Self {
|
|
42
|
+
inner: Arc::new(parking_lot::Mutex::new(inner)),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[napi]
|
|
48
|
+
impl Cache {
|
|
49
|
+
#[napi]
|
|
50
|
+
pub fn get(&self, env: Env, key: String) -> Result<JsUnknown> {
|
|
51
|
+
let mut lock = self.inner.lock();
|
|
52
|
+
if let Some(bytes) = lock.get(&key) {
|
|
53
|
+
if bytes.is_empty() {
|
|
54
|
+
return Ok(env.get_undefined()?.into_unknown());
|
|
55
|
+
}
|
|
56
|
+
let tag = bytes[0];
|
|
57
|
+
let payload = &bytes[1..];
|
|
58
|
+
match tag {
|
|
59
|
+
1 => {
|
|
60
|
+
// Buffer
|
|
61
|
+
let js_buf = env.create_buffer_copy(payload)?;
|
|
62
|
+
Ok(js_buf.into_unknown())
|
|
63
|
+
}
|
|
64
|
+
2 => {
|
|
65
|
+
// String
|
|
66
|
+
let s = std::str::from_utf8(payload)
|
|
67
|
+
.map_err(|e| napi::Error::new(napi::Status::StringExpected, e.to_string()))?;
|
|
68
|
+
let js_str = env.create_string(s)?;
|
|
69
|
+
Ok(js_str.into_unknown())
|
|
70
|
+
}
|
|
71
|
+
3 => {
|
|
72
|
+
// JSON
|
|
73
|
+
let json_val: serde_json::Value = serde_json::from_slice(payload)
|
|
74
|
+
.map_err(|e| napi::Error::new(napi::Status::InvalidArg, e.to_string()))?;
|
|
75
|
+
let parsed = env.to_js_value(&json_val)?;
|
|
76
|
+
Ok(parsed)
|
|
77
|
+
}
|
|
78
|
+
_ => Err(napi::Error::new(napi::Status::InvalidArg, "Invalid data type tag in cache storage")),
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
Ok(env.get_undefined()?.into_unknown())
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[napi]
|
|
86
|
+
pub fn set(&self, env: Env, key: String, value: JsUnknown, ttl_ms: Option<f64>) -> Result<JsUnknown> {
|
|
87
|
+
let mut bytes = Vec::new();
|
|
88
|
+
let value_type = value.get_type()?;
|
|
89
|
+
|
|
90
|
+
if value.is_buffer()? {
|
|
91
|
+
let buf = JsBuffer::try_from(value)?;
|
|
92
|
+
let raw_bytes: Vec<u8> = buf.into_value()?.to_vec();
|
|
93
|
+
bytes.push(1); // Tag: Buffer
|
|
94
|
+
bytes.extend_from_slice(&raw_bytes);
|
|
95
|
+
} else if value_type == ValueType::String {
|
|
96
|
+
let js_str = JsString::try_from(value)?;
|
|
97
|
+
let utf8 = js_str.into_utf8()?;
|
|
98
|
+
bytes.push(2); // Tag: String
|
|
99
|
+
bytes.extend_from_slice(utf8.as_slice());
|
|
100
|
+
} else {
|
|
101
|
+
// Treat as JSON
|
|
102
|
+
let json_val: serde_json::Value = env.from_js_value(value)?;
|
|
103
|
+
let serialized = serde_json::to_vec(&json_val)
|
|
104
|
+
.map_err(|e| napi::Error::new(napi::Status::InvalidArg, e.to_string()))?;
|
|
105
|
+
bytes.push(3); // Tag: JSON
|
|
106
|
+
bytes.extend_from_slice(&serialized);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let ttl = ttl_ms.map(|ms| ms as u64);
|
|
110
|
+
let mut lock = self.inner.lock();
|
|
111
|
+
let old_bytes = lock.set(&key, bytes, ttl);
|
|
112
|
+
|
|
113
|
+
if let Some(bytes) = old_bytes {
|
|
114
|
+
if bytes.is_empty() {
|
|
115
|
+
return Ok(env.get_undefined()?.into_unknown());
|
|
116
|
+
}
|
|
117
|
+
let tag = bytes[0];
|
|
118
|
+
let payload = &bytes[1..];
|
|
119
|
+
match tag {
|
|
120
|
+
1 => {
|
|
121
|
+
let js_buf = env.create_buffer_copy(payload)?;
|
|
122
|
+
Ok(js_buf.into_unknown())
|
|
123
|
+
}
|
|
124
|
+
2 => {
|
|
125
|
+
let s = std::str::from_utf8(payload)
|
|
126
|
+
.map_err(|e| napi::Error::new(napi::Status::StringExpected, e.to_string()))?;
|
|
127
|
+
let js_str = env.create_string(s)?;
|
|
128
|
+
Ok(js_str.into_unknown())
|
|
129
|
+
}
|
|
130
|
+
3 => {
|
|
131
|
+
let json_val: serde_json::Value = serde_json::from_slice(payload)
|
|
132
|
+
.map_err(|e| napi::Error::new(napi::Status::InvalidArg, e.to_string()))?;
|
|
133
|
+
let parsed = env.to_js_value(&json_val)?;
|
|
134
|
+
Ok(parsed)
|
|
135
|
+
}
|
|
136
|
+
_ => Err(napi::Error::new(napi::Status::InvalidArg, "Invalid data type tag in cache storage")),
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
Ok(env.get_undefined()?.into_unknown())
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[napi]
|
|
144
|
+
pub fn delete(&self, key: String) -> bool {
|
|
145
|
+
let mut lock = self.inner.lock();
|
|
146
|
+
lock.delete(&key)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[napi]
|
|
150
|
+
pub fn clear(&self) {
|
|
151
|
+
let mut lock = self.inner.lock();
|
|
152
|
+
lock.clear();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[napi]
|
|
156
|
+
pub fn stats(&self) -> CacheStatsJs {
|
|
157
|
+
let lock = self.inner.lock();
|
|
158
|
+
let stats = lock.stats();
|
|
159
|
+
CacheStatsJs {
|
|
160
|
+
hits: stats.hits as f64,
|
|
161
|
+
misses: stats.misses as f64,
|
|
162
|
+
capacity: stats.capacity as f64,
|
|
163
|
+
size: stats.size as f64,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[napi]
|
|
168
|
+
pub fn keys(&self) -> Vec<String> {
|
|
169
|
+
let lock = self.inner.lock();
|
|
170
|
+
lock.keys()
|
|
171
|
+
}
|
|
172
|
+
}
|