Skip to main content

dualcache_ff/
daemon.rs

1#[cfg(not(feature = "std"))]
2use alloc::{boxed::Box, vec::Vec};
3
4use crate::sync::{Arc, ArcSlice, new_arc_slice};
5use crate::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, Ordering};
6use crate::sync::index_types::AtomicTick;
7use core::hash::{Hash, BuildHasher};
8
9use crate::arena::Arena;
10use crate::storage::{Cache, Node};
11use crate::filters::{T1, T2};
12use crate::lossy_queue::{LossyQueue, OneshotAck};
13use crate::cache::{WorkerState, GLOBAL_EPOCH};
14
15/// Maximum rank (Revolution Shield value).
16/// A newly inserted or hit item gets rank = MAX_RANK, granting it
17/// MAX_RANK Pendulum sweeps of guaranteed survival.
18const MAX_RANK: u8 = 3;
19
20// ── Command ───────────────────────────────────────────────────────────────
21
22pub enum Command<K, V> {
23    /// Single insert from Worker (goes through probation gate).
24    Insert(K, V, u64, bool),
25    /// Batch of (K, V, hash) from sharded worker buffers.
26    BatchInsert(Vec<(K, V, u64, bool)>),
27    /// Insert directly as a top priority item bypassing probation, giving it max survival rank.
28    InsertT1(K, V, u64),
29    /// Remove by key+hash.
30    Remove(K, u64),
31    /// Blocking clear — caller spins on `OneshotAck::wait()`.
32    Clear(Arc<OneshotAck>),
33    /// Blocking maintenance flush — caller spins on `OneshotAck::wait()`.
34    Sync(Arc<OneshotAck>),
35    /// Signal Daemon to exit its run loop.
36    Shutdown,
37}
38
39// ── Daemon ────────────────────────────────────────────────────────────────
40
41pub struct Daemon<K, V, S> {
42    pub hasher: S,
43    pub arena: Arena,
44    pub t1: Arc<T1<K, V>>,
45    pub t2: Arc<T2<K, V>>,
46    pub cache: Arc<Cache<K, V>>,
47    pub cmd_rx: Arc<LossyQueue<Command<K, V>>>,
48    pub hit_rx: Arc<LossyQueue<[usize; 64]>>,
49    pub epoch: Arc<AtomicU32>,
50    /// Configurable poll interval in microseconds (1 000–10 000 µs).
51    /// Controls the trade-off between CPU idle cost and hit-signal latency.
52    pub poll_us: u64,
53    pub admission: Arc<AdmissionFilter>,
54    /// Pre-allocated accumulator for deferred-sort hit processing.
55    pub hit_accumulator: Vec<usize>,
56    pub last_decay_epoch: u32,
57    pub garbage_queue: Vec<(*mut Node<K, V>, usize)>,
58    pub worker_states: ArcSlice<WorkerState>,
59    /// Monotonically increasing tick counter — incremented on every poll loop.
60    /// Workers read this (Relaxed) to decide whether to time-flush their TLS
61    /// buffers without needing a hardware clock in no_std mode.
62    pub daemon_tick: Arc<AtomicTick>,
63    /// Cold-start flag shared with DualCacheFF
64    pub is_cold_start: Arc<AtomicBool>,
65    /// Configured TTL duration
66    pub duration: u32,
67}
68
69unsafe impl<K: Send, V: Send, S: Send> Send for Daemon<K, V, S> {}
70
71impl<K, V, S> Daemon<K, V, S>
72where
73    K: Hash + Eq + Send + Sync + Clone + 'static,
74    V: Send + Sync + Clone + 'static,
75    S: BuildHasher + Clone + Send + 'static,
76{
77    #[allow(clippy::too_many_arguments)]
78    pub fn new(
79        hasher: S,
80        capacity: usize,
81        t1: Arc<T1<K, V>>,
82        t2: Arc<T2<K, V>>,
83        cache: Arc<Cache<K, V>>,
84        cmd_rx: Arc<LossyQueue<Command<K, V>>>,
85        hit_rx: Arc<LossyQueue<[usize; 64]>>,
86        epoch: Arc<AtomicU32>,
87        duration: u32,
88        poll_us: u64,
89        worker_states: ArcSlice<WorkerState>,
90        daemon_tick: Arc<AtomicTick>,
91        is_cold_start: Arc<AtomicBool>,
92    ) -> Self {
93        Self {
94            hasher,
95            arena: Arena::new(capacity),
96            t1,
97            t2,
98            cache,
99            cmd_rx,
100            hit_rx,
101            epoch,
102            poll_us,
103            admission: Arc::new(AdmissionFilter::new(capacity)),
104            hit_accumulator: Vec::with_capacity(8192),
105            last_decay_epoch: 0,
106            garbage_queue: Vec::new(),
107            worker_states,
108            daemon_tick,
109            is_cold_start,
110            duration,
111        }
112    }
113
114    /// Main Daemon event loop.
115    ///
116    /// # std mode
117    /// Called from a dedicated `std::thread::spawn` inside `DualCacheFF::new`.
118    /// Sleeps `poll_us` microseconds when the command queue is empty.
119    ///
120    /// # no_std mode
121    /// The caller (e.g. RTOS task) must invoke `daemon.run()` on a dedicated
122    /// task. The loop uses `core::hint::spin_loop()` between iterations;
123    /// the RTOS scheduler handles preemption and CPU sharing.
124    pub fn run(mut self) {
125        #[cfg(feature = "std")]
126        let mut last_epoch_tick = std::time::Instant::now();
127
128        loop {
129            // ── Drain command queue (up to 8192 commands per poll) ────────
130            let mut processed = 0u32;
131            loop {
132                match self.cmd_rx.try_recv() {
133                    Some(Command::Shutdown) => return,
134                    Some(cmd) => {
135                        self.process_cmd(cmd);
136                        processed += 1;
137                        if processed >= 8192 {
138                            break;
139                        }
140                    }
141                    None => break,
142                }
143            }
144
145            // ── Epoch tick ────────────────────────────────────────────────
146            // In std mode: wall-clock driven, every ~100 ms.
147            // In no_std mode: daemon_tick driven, every 100 poll iterations.
148            #[cfg(feature = "std")]
149            {
150                let now = std::time::Instant::now();
151                if now.duration_since(last_epoch_tick)
152                    >= std::time::Duration::from_millis(100)
153                {
154                    self.epoch.fetch_add(1, Ordering::Relaxed);
155                    last_epoch_tick = now;
156                }
157            }
158            #[cfg(not(feature = "std"))]
159            {
160                let tick = self.daemon_tick.load(Ordering::Relaxed);
161                if tick % 100 == 0 {
162                    self.epoch.fetch_add(1, Ordering::Relaxed);
163                }
164            }
165
166            // ── Maintenance (GC + hit processing + eviction) ──────────────
167            self.maintenance();
168
169            // ── Advance daemon_tick ───────────────────────────────────────
170            #[cfg(any(feature = "loom", loom))]
171            {
172                if processed > 0 {
173                    self.daemon_tick.fetch_add(1, Ordering::Relaxed);
174                }
175            }
176            #[cfg(not(any(feature = "loom", loom)))]
177            {
178                self.daemon_tick.fetch_add(1, Ordering::Relaxed);
179            }
180
181            // ── Idle sleep / spin ─────────────────────────────────────────
182            if processed == 0 {
183                #[cfg(any(feature = "loom", loom))]
184                loom::thread::yield_now();
185                #[cfg(all(feature = "std", not(any(feature = "loom", loom))))]
186                std::thread::sleep(std::time::Duration::from_micros(self.poll_us));
187                #[cfg(not(feature = "std"))]
188                core::hint::spin_loop();
189            }
190        }
191    }
192
193    #[inline(always)]
194    fn process_cmd(&mut self, cmd: Command<K, V>) -> bool {
195        match cmd {
196            Command::Insert(k, v, hash, is_t1) => {
197                self.handle_admission_insert(k, v, hash, is_t1);
198                true
199            }
200            Command::BatchInsert(batch) => {
201                for (k, v, hash, is_t1) in batch {
202                    self.handle_admission_insert(k, v, hash, is_t1);
203                }
204                true
205            }
206            Command::InsertT1(k, v, hash) => {
207                self.handle_insert_t1(k, v, hash);
208                true
209            }
210            Command::Remove(k, hash) => {
211                self.handle_remove(k, hash);
212                true
213            }
214            Command::Clear(ack) => {
215                self.handle_clear();
216                ack.signal();
217                true
218            }
219            Command::Sync(ack) => {
220                self.maintenance();
221                ack.signal();
222                true
223            }
224            Command::Shutdown => unreachable!("handled in run()"),
225        }
226    }
227
228    /// Binary Valve Admission:
229    /// 1. Cold Start Mode (free slots > 5%): accept all.
230    /// 2. Steady State Mode: only accept if Ghost Set recognises the item.
231    fn handle_admission_insert(&mut self, k: K, v: V, hash: u64, is_t1: bool) {
232        if is_t1 {
233            self.handle_insert_t1(k, v, hash);
234            return;
235        }
236        let cold_start = self.arena.free_list_len() > self.arena.capacity / 20;
237        self.is_cold_start.store(cold_start, Ordering::Relaxed);
238        if cold_start || self.admission.check_ghost(hash) {
239            self.handle_insert_with_hash(k, v, hash);
240        }
241    }
242
243    fn handle_insert_with_hash(&mut self, k: K, v: V, hash: u64) {
244        let tag = (hash >> 48) as u16;
245
246        // 1. Check if it's an update of an existing entry
247        let global_idx = if let Some(existing_idx) = self.cache.index_probe(hash, tag) {
248            existing_idx
249        } else {
250            // 2. New insert: need a free slot
251            if self.arena.free_list_empty() {
252                self.evict_batch();
253            }
254            if let Some(new_idx) = self.arena.pop_free_slot() {
255                new_idx
256            } else {
257                return; // Still no slots after eviction — drop
258            }
259        };
260
261        let entry = (tag as u64) << 48 | (global_idx as u64 & 0x0000_FFFF_FFFF_FFFF);
262
263        let node_ptr = Box::into_raw(Box::new(Node {
264            key: k,
265            value: v,
266            expire_at: self.epoch.load(Ordering::Relaxed) + self.get_duration(),
267            g_idx: global_idx as u32,
268        }));
269
270        let old_ptr = self.cache.nodes[global_idx].swap(node_ptr, Ordering::Release);
271        if !old_ptr.is_null() {
272            let epoch = GLOBAL_EPOCH.load(Ordering::Relaxed);
273            self.garbage_queue.push((old_ptr, epoch));
274        }
275
276        self.cache.index_store(hash, tag, entry);
277        self.arena.set_hash(global_idx, hash);
278        // Revolution Shield: new items start with MAX_RANK protection
279        self.arena.set_rank(global_idx, MAX_RANK);
280    }
281
282    fn handle_insert_t1(&mut self, k: K, v: V, hash: u64) {
283        let tag = (hash >> 48) as u16;
284
285        let global_idx = if let Some(existing_idx) = self.cache.index_probe(hash, tag) {
286            existing_idx
287        } else {
288            if self.arena.free_list_empty() {
289                self.evict_batch();
290            }
291            if let Some(new_idx) = self.arena.pop_free_slot() {
292                new_idx
293            } else {
294                return;
295            }
296        };
297
298        let entry = (tag as u64) << 48 | (global_idx as u64 & 0x0000_FFFF_FFFF_FFFF);
299
300        let node_ptr = Box::into_raw(Box::new(Node {
301            key: k,
302            value: v,
303            expire_at: self.epoch.load(Ordering::Relaxed) + self.get_duration(),
304            g_idx: global_idx as u32,
305        }));
306
307        let old_ptr = self.cache.nodes[global_idx].swap(node_ptr, Ordering::Release);
308        if !old_ptr.is_null() {
309            let epoch = GLOBAL_EPOCH.load(Ordering::Relaxed);
310            self.garbage_queue.push((old_ptr, epoch));
311        }
312
313        self.cache.index_store(hash, tag, entry);
314        self.arena.set_hash(global_idx, hash);
315        self.arena.set_rank(global_idx, 255); // high initial rank (God mode)
316
317        // Promotion: hot items migrate to T1 immediately
318        self.t1.store_slot(hash, node_ptr);
319    }
320
321    fn get_duration(&self) -> u32 {
322        self.duration
323    }
324
325    fn handle_remove(&mut self, _k: K, hash: u64) {
326        let tag = (hash >> 48) as u16;
327        if let Some(g_idx) = self.cache.index_probe(hash, tag) {
328            let old_ptr =
329                self.cache.nodes[g_idx].swap(core::ptr::null_mut(), Ordering::Release);
330            if !old_ptr.is_null() {
331                let epoch = GLOBAL_EPOCH.load(Ordering::Relaxed);
332                self.garbage_queue.push((old_ptr, epoch));
333                self.t1.clear_if_matches(hash, old_ptr);
334                self.t2.clear_if_matches(hash, old_ptr);
335            }
336            self.cache.index_remove(hash, tag, g_idx);
337            self.arena.set_rank(g_idx, 0); // Fast eviction next cycle
338        }
339    }
340
341    fn handle_clear(&mut self) {
342        self.cache.clear();
343        for i in 0..self.t1.len() {
344            self.t1.clear_at(i);
345        }
346        for i in 0..self.t2.len() {
347            self.t2.clear_at(i);
348        }
349        self.admission.clear();
350        self.arena.clear();
351        self.is_cold_start.store(true, Ordering::Relaxed);
352    }
353
354    fn maintenance(&mut self) {
355        // ── Phase 0: QSBR Garbage Collection ─────────────────────────────
356        if !self.garbage_queue.is_empty() {
357            let current_global = GLOBAL_EPOCH.load(Ordering::Relaxed);
358            GLOBAL_EPOCH.store(current_global + 1, Ordering::Release);
359
360            let mut min_active_epoch = current_global + 1;
361            for state in self.worker_states.iter() {
362                let local = state.local_epoch.load(Ordering::Acquire);
363                if local != 0 && local < min_active_epoch {
364                    min_active_epoch = local;
365                }
366            }
367
368            self.garbage_queue.retain(|&(ptr, epoch)| {
369                if epoch < min_active_epoch {
370                    unsafe { drop(Box::from_raw(ptr)) };
371                    false
372                } else {
373                    true
374                }
375            });
376        }
377
378        // ── Phase 1: Collect hit indices into accumulator ─────────────────
379        while let Some(batch) = self.hit_rx.try_recv() {
380            for &g_idx in batch.iter() {
381                if g_idx < self.arena.capacity && g_idx != usize::MAX {
382                    self.hit_accumulator.push(g_idx);
383                }
384            }
385            if self.hit_accumulator.len() >= 8192 {
386                break;
387            }
388        }
389
390        // ── Phase 2: Sort + Revolution Shield hit processing ──────────────
391        if !self.hit_accumulator.is_empty() {
392            self.hit_accumulator.sort_unstable();
393
394            for &g_idx in &self.hit_accumulator {
395                // Revolution Shield: refill to MAX_RANK on every hit
396                self.arena.set_rank(g_idx, MAX_RANK);
397
398                let hash = self.arena.get_hash(g_idx);
399
400                // Promotion: hot items migrate to T1
401                let ptr = self.cache.nodes[g_idx].load(Ordering::Acquire);
402                if !ptr.is_null() && self.t1.load_slot(hash) != ptr {
403                    self.t1.store_slot(hash, ptr);
404                }
405            }
406
407            self.hit_accumulator.clear();
408        }
409
410        if self.arena.free_list_len() < self.arena.capacity / 10 {
411            self.evict_batch();
412        }
413
414        let cold_start = self.arena.free_list_len() > self.arena.capacity / 20;
415        if self.is_cold_start.load(Ordering::Relaxed) != cold_start {
416            self.is_cold_start.store(cold_start, Ordering::Relaxed);
417        }
418    }
419
420    /// Avg-rank eviction: scan the Pendulum cursor, compare each slot's rank
421    /// with the running average. Guaranteed O(1) amortised candidate search.
422    fn evict_batch(&mut self) {
423        let count = 128;
424        let avg = (self.arena.count_sum() / self.arena.capacity as u64) as u8;
425        let threshold = avg.max(1);
426
427        for _ in 0..count {
428            if self.arena.free_list_len() > self.arena.capacity / 10 {
429                break;
430            }
431
432            let idx = self.arena.cursor();
433            let r = self.arena.get_rank(idx);
434
435            if r <= threshold {
436                // Evict
437                let hash = self.arena.get_hash(idx);
438                let tag = (hash >> 48) as u16;
439
440                let old_ptr =
441                    self.cache.nodes[idx].swap(core::ptr::null_mut(), Ordering::Release);
442                if !old_ptr.is_null() {
443                    let epoch = GLOBAL_EPOCH.load(Ordering::Relaxed);
444                    self.garbage_queue.push((old_ptr, epoch));
445                    self.t1.clear_if_matches(hash, old_ptr);
446                    self.t2.clear_if_matches(hash, old_ptr);
447                }
448
449                self.cache.index_remove(hash, tag, idx);
450
451                // Task 5 — Ghost Set dynamically scaled to capacity:
452                // record_death writes to ghost_set[hash & ghost_mask],
453                // where ghost_mask = capacity - 1 (already aligned).
454                self.admission.record_death(hash);
455                self.arena.push_free_slot(idx);
456                self.arena.set_rank(idx, 0);
457            } else {
458                // Decay — decrement rank by 1
459                self.arena.decrement_rank(idx);
460            }
461            self.arena.advance_cursor();
462        }
463    }
464}
465
466impl<K, V, S> Drop for Daemon<K, V, S> {
467    fn drop(&mut self) {
468        for &(ptr, _) in self.garbage_queue.iter() {
469            if !ptr.is_null() {
470                unsafe {
471                    let _ = Box::from_raw(ptr);
472                }
473            }
474        }
475    }
476}
477
478// ── AdmissionFilter (Ghost Set) ───────────────────────────────────────────
479
480/// Ghost Set — direct-mapped fingerprint array.
481///
482/// Records the 16-bit fingerprint of evicted items so that previously-hot
483/// items bypass TLS probation on re-insertion.
484///
485/// # Task 5 — Capacity Align
486/// Ghost Set size is always equal to the Arena's `capacity` (already a power
487/// of two). When the user sets a small capacity (e.g. 2000 items rounded to
488/// 2048), the Ghost Set is also 2048 × 2 bytes = 4 KB — not the previous
489/// bloated `capacity.next_power_of_two()` of a larger default.
490pub struct AdmissionFilter {
491    pub ghost_mask: usize,
492    pub ghost_set: ArcSlice<AtomicU16>,
493}
494
495impl AdmissionFilter {
496    /// `capacity` must be a power of two (enforced by `Config`).
497    /// Ghost Set is exactly `capacity` entries (2 bytes each).
498    pub fn new(capacity: usize) -> Self {
499        // Capacity is already power-of-two — no extra `.next_power_of_two()`.
500        // Minimum 256 entries to keep the false-positive rate reasonable.
501        let ghost_size = capacity.max(256);
502
503        let mut ghost_vec = Vec::with_capacity(ghost_size);
504        for _ in 0..ghost_size {
505            ghost_vec.push(AtomicU16::new(0));
506        }
507
508        Self {
509            ghost_mask: ghost_size - 1,
510            ghost_set: new_arc_slice(ghost_vec),
511        }
512    }
513
514    /// Called by Daemon on eviction: record this item's 16-bit fingerprint.
515    #[inline(always)]
516    pub fn record_death(&self, hash: u64) {
517        let fp = (hash >> 48) as u16;
518        let idx = (hash as usize) & self.ghost_mask;
519        self.ghost_set[idx].store(fp, Ordering::Relaxed);
520    }
521
522    /// Called by Worker on insert: `true` if the fingerprint matches a
523    /// previously evicted item → bypass TLS probation.
524    #[inline(always)]
525    pub fn check_ghost(&self, hash: u64) -> bool {
526        let fp = (hash >> 48) as u16;
527        let ghost_idx = (hash as usize) & self.ghost_mask;
528        self.ghost_set[ghost_idx].load(Ordering::Relaxed) == fp
529    }
530
531    pub fn clear(&self) {
532        for val in self.ghost_set.iter() {
533            val.store(0, Ordering::Relaxed);
534        }
535    }
536}