Skip to main content

rusty_ssr/cache/
hot.rs

1//! Thread-local "hot" cache optimized for L1/L2 CPU cache
2//!
3//! Two-tier design:
4//! - Ultra-hot: 8 entries in cache-line aligned array (~1-3ns access)
5//! - Hot: HashMap for O(1) lookup on more entries (~5-10ns access)
6//!
7//! Total capacity: 128 entries per thread
8
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13/// Maximum entries in ultra-hot array (fits in 2 cache lines)
14const ULTRA_HOT_SIZE: usize = 8;
15
16/// Maximum entries in hot HashMap
17const HOT_MAP_CAPACITY: usize = 128;
18
19/// Thread-local hot cache with two tiers
20///
21/// Uses `#[repr(align(64))]` to prevent false sharing between threads.
22#[repr(align(64))]
23pub struct HotCache {
24    // Tier 1: Ultra-hot array for most recent entries
25    ultra_hot: [Option<HotEntry>; ULTRA_HOT_SIZE],
26    ultra_hot_next: usize,
27
28    // Tier 2: HashMap for O(1) lookup on more entries
29    hot_map: HashMap<u64, HotEntry>,
30
31    // LRU tracking for hot_map eviction
32    access_order: Vec<u64>,
33
34    ttl: Option<Duration>,
35}
36
37#[derive(Clone)]
38struct HotEntry {
39    url_hash: u64,
40    html: Arc<str>,
41    created_at: Instant,
42}
43
44impl HotCache {
45    /// Create a new empty hot cache
46    pub fn new() -> Self {
47        Self {
48            ultra_hot: Default::default(),
49            ultra_hot_next: 0,
50            hot_map: HashMap::with_capacity(HOT_MAP_CAPACITY),
51            access_order: Vec::with_capacity(HOT_MAP_CAPACITY),
52            ttl: None,
53        }
54    }
55
56    /// Create a hot cache with TTL
57    pub fn with_ttl(ttl_secs: u64) -> Self {
58        Self {
59            ultra_hot: Default::default(),
60            ultra_hot_next: 0,
61            hot_map: HashMap::with_capacity(HOT_MAP_CAPACITY),
62            access_order: Vec::with_capacity(HOT_MAP_CAPACITY),
63            ttl: if ttl_secs > 0 {
64                Some(Duration::from_secs(ttl_secs))
65            } else {
66                None
67            },
68        }
69    }
70
71    /// Look up HTML by URL hash
72    ///
73    /// Checks ultra-hot array first (fastest), then HashMap
74    #[inline(always)]
75    pub fn get(&mut self, url_hash: u64) -> Option<Arc<str>> {
76        // Tier 1: Check ultra-hot array first (linear scan, but only 8 entries)
77        for entry in self.ultra_hot.iter().flatten() {
78            if entry.url_hash == url_hash {
79                if self.is_expired(entry) {
80                    return None;
81                }
82                return Some(Arc::clone(&entry.html));
83            }
84        }
85
86        // Tier 2: Check HashMap (O(1) lookup)
87        if let Some(entry) = self.hot_map.get(&url_hash) {
88            if self.is_expired(entry) {
89                self.hot_map.remove(&url_hash);
90                return None;
91            }
92
93            // Promote to ultra-hot on access (LRU behavior)
94            let html = Arc::clone(&entry.html);
95            self.promote_to_ultra_hot(url_hash, Arc::clone(&html));
96            return Some(html);
97        }
98
99        None
100    }
101
102    /// Look up without promotion (for read-only access)
103    #[inline(always)]
104    pub fn peek(&self, url_hash: u64) -> Option<Arc<str>> {
105        // Check ultra-hot first
106        for entry in self.ultra_hot.iter().flatten() {
107            if entry.url_hash == url_hash {
108                if self.is_expired(entry) {
109                    return None;
110                }
111                return Some(Arc::clone(&entry.html));
112            }
113        }
114
115        // Check HashMap
116        if let Some(entry) = self.hot_map.get(&url_hash) {
117            if self.is_expired(entry) {
118                return None;
119            }
120            return Some(Arc::clone(&entry.html));
121        }
122
123        None
124    }
125
126    /// Insert a new entry
127    #[inline(always)]
128    pub fn insert(&mut self, url_hash: u64, html: Arc<str>) {
129        let entry = HotEntry {
130            url_hash,
131            html,
132            created_at: Instant::now(),
133        };
134
135        // Always insert into ultra-hot first
136        // Evicted entry goes to hot_map
137        if let Some(evicted) = self.ultra_hot[self.ultra_hot_next].take() {
138            // Move evicted to hot_map
139            self.insert_to_hot_map(evicted);
140        }
141
142        self.ultra_hot[self.ultra_hot_next] = Some(entry);
143        self.ultra_hot_next = (self.ultra_hot_next + 1) % ULTRA_HOT_SIZE;
144    }
145
146    /// Insert into hot_map with LRU eviction
147    fn insert_to_hot_map(&mut self, entry: HotEntry) {
148        // Evict oldest if at capacity
149        if self.hot_map.len() >= HOT_MAP_CAPACITY {
150            if let Some(oldest_key) = self.access_order.first().copied() {
151                self.hot_map.remove(&oldest_key);
152                self.access_order.remove(0);
153            }
154        }
155
156        self.access_order.push(entry.url_hash);
157        self.hot_map.insert(entry.url_hash, entry);
158    }
159
160    /// Promote an entry from hot_map to ultra-hot
161    fn promote_to_ultra_hot(&mut self, url_hash: u64, html: Arc<str>) {
162        // Remove from hot_map
163        self.hot_map.remove(&url_hash);
164        if let Some(pos) = self.access_order.iter().position(|&k| k == url_hash) {
165            self.access_order.remove(pos);
166        }
167
168        // Insert into ultra-hot (this will move current ultra-hot entry to hot_map)
169        self.insert(url_hash, html);
170    }
171
172    /// Check if entry is expired
173    #[inline(always)]
174    fn is_expired(&self, entry: &HotEntry) -> bool {
175        if let Some(ttl) = self.ttl {
176            entry.created_at.elapsed() > ttl
177        } else {
178            false
179        }
180    }
181
182    /// Get total number of cached entries
183    #[allow(dead_code)]
184    pub fn len(&self) -> usize {
185        let ultra_hot_count = self.ultra_hot.iter().flatten().count();
186        ultra_hot_count + self.hot_map.len()
187    }
188
189    /// Check if cache is empty
190    #[allow(dead_code)]
191    pub fn is_empty(&self) -> bool {
192        self.len() == 0
193    }
194
195    /// Clear all entries
196    #[allow(dead_code)]
197    pub fn clear(&mut self) {
198        self.ultra_hot = Default::default();
199        self.ultra_hot_next = 0;
200        self.hot_map.clear();
201        self.access_order.clear();
202    }
203}
204
205impl Default for HotCache {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_basic_operations() {
217        let mut cache = HotCache::new();
218        let html: Arc<str> = "test".into();
219
220        cache.insert(123, Arc::clone(&html));
221
222        assert!(cache.get(123).is_some());
223        assert!(cache.get(456).is_none());
224    }
225
226    #[test]
227    fn test_ultra_hot_eviction() {
228        let mut cache = HotCache::new();
229
230        // Insert more than 8 entries
231        for i in 0..10u64 {
232            let html: Arc<str> = format!("html{}", i).into();
233            cache.insert(i, html);
234        }
235
236        // First 2 should be in hot_map, not ultra_hot
237        // But still accessible via get()
238        assert!(cache.get(0).is_some(), "Entry 0 should be in hot_map");
239        assert!(cache.get(1).is_some(), "Entry 1 should be in hot_map");
240        assert!(cache.get(9).is_some(), "Entry 9 should be in ultra_hot");
241    }
242
243    #[test]
244    fn test_promotion() {
245        let mut cache = HotCache::new();
246
247        // Fill ultra_hot
248        for i in 0..8u64 {
249            cache.insert(i, format!("html{}", i).into());
250        }
251
252        // Add more to push to hot_map
253        for i in 8..16u64 {
254            cache.insert(i, format!("html{}", i).into());
255        }
256
257        // Entry 0 should be in hot_map now
258        // Accessing it should promote it back to ultra_hot
259        let _ = cache.get(0);
260
261        // Verify it's accessible
262        assert!(cache.peek(0).is_some());
263    }
264
265    #[test]
266    fn test_capacity() {
267        let mut cache = HotCache::new();
268
269        // Insert 200 entries (more than 128 capacity)
270        for i in 0..200u64 {
271            cache.insert(i, format!("html{}", i).into());
272        }
273
274        // Should have at most 128 + 8 = 136 entries
275        assert!(cache.len() <= ULTRA_HOT_SIZE + HOT_MAP_CAPACITY);
276    }
277
278    #[test]
279    fn test_hashmap_lookup_speed() {
280        let mut cache = HotCache::new();
281
282        // Fill with 100 entries
283        for i in 0..100u64 {
284            cache.insert(i, format!("html{}", i).into());
285        }
286
287        // Access entry that's definitely in hot_map
288        // This should be O(1), not O(n)
289        assert!(cache.peek(50).is_some());
290    }
291}