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