Skip to main content

presentar_layout/
cache.rs

1//! Layout caching for memoization.
2
3use presentar_core::Size;
4use std::collections::HashMap;
5
6/// Cache key combining constraints hash and widget identity.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub struct CacheKey {
9    /// Widget identity hash
10    pub widget_id: u64,
11    /// Constraints hash
12    pub constraints_hash: u64,
13}
14
15/// Cached layout result.
16#[derive(Debug, Clone, Copy)]
17pub(crate) struct CacheEntry {
18    /// Computed size
19    pub size: Size,
20    /// Frame when this entry was last used
21    pub last_used_frame: u64,
22}
23
24/// Layout cache for memoizing measure results.
25#[derive(Debug, Default)]
26pub struct LayoutCache {
27    entries: HashMap<CacheKey, CacheEntry>,
28    current_frame: u64,
29    hits: usize,
30    misses: usize,
31}
32
33impl LayoutCache {
34    /// Create a new empty cache.
35    #[must_use]
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Look up a cached size.
41    #[must_use]
42    pub fn get(&mut self, key: CacheKey) -> Option<Size> {
43        if let Some(entry) = self.entries.get_mut(&key) {
44            entry.last_used_frame = self.current_frame;
45            self.hits += 1;
46            Some(entry.size)
47        } else {
48            self.misses += 1;
49            None
50        }
51    }
52
53    /// Insert a computed size into the cache.
54    pub fn insert(&mut self, key: CacheKey, size: Size) {
55        self.entries.insert(
56            key,
57            CacheEntry {
58                size,
59                last_used_frame: self.current_frame,
60            },
61        );
62    }
63
64    /// Clear the entire cache.
65    pub fn clear(&mut self) {
66        self.entries.clear();
67        self.hits = 0;
68        self.misses = 0;
69    }
70
71    /// Get the number of cache hits.
72    #[must_use]
73    pub const fn hits(&self) -> usize {
74        self.hits
75    }
76
77    /// Get the number of cache misses.
78    #[must_use]
79    pub const fn misses(&self) -> usize {
80        self.misses
81    }
82
83    /// Advance to the next frame and evict stale entries.
84    pub fn advance_frame(&mut self) {
85        self.current_frame += 1;
86
87        // Evict entries not used in the last 2 frames
88        let threshold = self.current_frame.saturating_sub(2);
89        self.entries
90            .retain(|_, entry| entry.last_used_frame >= threshold);
91    }
92
93    /// Get the number of cached entries.
94    #[must_use]
95    pub fn len(&self) -> usize {
96        self.entries.len()
97    }
98
99    /// Check if the cache is empty.
100    #[must_use]
101    pub fn is_empty(&self) -> bool {
102        self.entries.is_empty()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_cache_new() {
112        let cache = LayoutCache::new();
113        assert!(cache.is_empty());
114        assert_eq!(cache.len(), 0);
115    }
116
117    #[test]
118    fn test_cache_insert_get() {
119        let mut cache = LayoutCache::new();
120        let key = CacheKey {
121            widget_id: 1,
122            constraints_hash: 100,
123        };
124        let size = Size::new(50.0, 50.0);
125
126        cache.insert(key, size);
127        assert_eq!(cache.get(key), Some(size));
128        assert_eq!(cache.len(), 1);
129    }
130
131    #[test]
132    fn test_cache_miss() {
133        let mut cache = LayoutCache::new();
134        let key = CacheKey {
135            widget_id: 1,
136            constraints_hash: 100,
137        };
138
139        assert_eq!(cache.get(key), None);
140    }
141
142    #[test]
143    fn test_cache_clear() {
144        let mut cache = LayoutCache::new();
145        let key = CacheKey {
146            widget_id: 1,
147            constraints_hash: 100,
148        };
149
150        cache.insert(key, Size::new(10.0, 10.0));
151        assert!(!cache.is_empty());
152
153        cache.clear();
154        assert!(cache.is_empty());
155    }
156
157    #[test]
158    fn test_cache_eviction() {
159        let mut cache = LayoutCache::new();
160        let key = CacheKey {
161            widget_id: 1,
162            constraints_hash: 100,
163        };
164
165        cache.insert(key, Size::new(10.0, 10.0));
166
167        // Advance 3 frames without using the entry
168        cache.advance_frame();
169        cache.advance_frame();
170        cache.advance_frame();
171
172        // Entry should be evicted
173        assert!(cache.is_empty());
174    }
175
176    #[test]
177    fn test_cache_not_evicted_when_used() {
178        let mut cache = LayoutCache::new();
179        let key = CacheKey {
180            widget_id: 1,
181            constraints_hash: 100,
182        };
183
184        cache.insert(key, Size::new(10.0, 10.0));
185
186        // Advance frames but keep using the entry
187        for _ in 0..5 {
188            cache.advance_frame();
189            let _ = cache.get(key); // Touch the entry
190        }
191
192        assert!(!cache.is_empty());
193    }
194
195    #[test]
196    fn test_cache_hits_and_misses() {
197        let mut cache = LayoutCache::new();
198        let key = CacheKey {
199            widget_id: 1,
200            constraints_hash: 100,
201        };
202
203        assert_eq!(cache.hits(), 0);
204        assert_eq!(cache.misses(), 0);
205
206        // Miss
207        let _ = cache.get(key);
208        assert_eq!(cache.hits(), 0);
209        assert_eq!(cache.misses(), 1);
210
211        // Insert and hit
212        cache.insert(key, Size::new(10.0, 10.0));
213        let _ = cache.get(key);
214        assert_eq!(cache.hits(), 1);
215        assert_eq!(cache.misses(), 1);
216
217        // Another hit
218        let _ = cache.get(key);
219        assert_eq!(cache.hits(), 2);
220        assert_eq!(cache.misses(), 1);
221    }
222
223    #[test]
224    fn test_cache_clear_resets_stats() {
225        let mut cache = LayoutCache::new();
226        let key = CacheKey {
227            widget_id: 1,
228            constraints_hash: 100,
229        };
230
231        cache.insert(key, Size::new(10.0, 10.0));
232        let _ = cache.get(key);
233        let _ = cache.get(CacheKey {
234            widget_id: 2,
235            constraints_hash: 200,
236        });
237
238        assert_eq!(cache.hits(), 1);
239        assert_eq!(cache.misses(), 1);
240
241        cache.clear();
242        assert_eq!(cache.hits(), 0);
243        assert_eq!(cache.misses(), 0);
244    }
245
246    // =========================================================================
247    // CacheKey Tests
248    // =========================================================================
249
250    #[test]
251    fn test_cache_key_equality() {
252        let key1 = CacheKey {
253            widget_id: 1,
254            constraints_hash: 100,
255        };
256        let key2 = CacheKey {
257            widget_id: 1,
258            constraints_hash: 100,
259        };
260        assert_eq!(key1, key2);
261    }
262
263    #[test]
264    fn test_cache_key_inequality_widget_id() {
265        let key1 = CacheKey {
266            widget_id: 1,
267            constraints_hash: 100,
268        };
269        let key2 = CacheKey {
270            widget_id: 2,
271            constraints_hash: 100,
272        };
273        assert_ne!(key1, key2);
274    }
275
276    #[test]
277    fn test_cache_key_inequality_constraints() {
278        let key1 = CacheKey {
279            widget_id: 1,
280            constraints_hash: 100,
281        };
282        let key2 = CacheKey {
283            widget_id: 1,
284            constraints_hash: 200,
285        };
286        assert_ne!(key1, key2);
287    }
288
289    #[test]
290    fn test_cache_key_clone() {
291        let key = CacheKey {
292            widget_id: 42,
293            constraints_hash: 999,
294        };
295        let cloned = key;
296        assert_eq!(key, cloned);
297    }
298
299    #[test]
300    fn test_cache_key_debug() {
301        let key = CacheKey {
302            widget_id: 1,
303            constraints_hash: 100,
304        };
305        let debug = format!("{:?}", key);
306        assert!(debug.contains("widget_id"));
307        assert!(debug.contains("constraints_hash"));
308    }
309
310    // =========================================================================
311    // Multiple Entries Tests
312    // =========================================================================
313
314    #[test]
315    fn test_cache_multiple_entries() {
316        let mut cache = LayoutCache::new();
317
318        for i in 0..10 {
319            let key = CacheKey {
320                widget_id: i,
321                constraints_hash: i * 100,
322            };
323            cache.insert(key, Size::new(i as f32, i as f32));
324        }
325
326        assert_eq!(cache.len(), 10);
327
328        // Verify each entry
329        for i in 0..10 {
330            let key = CacheKey {
331                widget_id: i,
332                constraints_hash: i * 100,
333            };
334            assert_eq!(cache.get(key), Some(Size::new(i as f32, i as f32)));
335        }
336    }
337
338    #[test]
339    fn test_cache_overwrite_entry() {
340        let mut cache = LayoutCache::new();
341        let key = CacheKey {
342            widget_id: 1,
343            constraints_hash: 100,
344        };
345
346        cache.insert(key, Size::new(10.0, 10.0));
347        assert_eq!(cache.get(key), Some(Size::new(10.0, 10.0)));
348
349        // Overwrite with new size
350        cache.insert(key, Size::new(20.0, 20.0));
351        assert_eq!(cache.get(key), Some(Size::new(20.0, 20.0)));
352        assert_eq!(cache.len(), 1);
353    }
354
355    // =========================================================================
356    // Eviction Edge Cases
357    // =========================================================================
358
359    #[test]
360    fn test_cache_eviction_threshold() {
361        let mut cache = LayoutCache::new();
362
363        let key1 = CacheKey {
364            widget_id: 1,
365            constraints_hash: 100,
366        };
367        let key2 = CacheKey {
368            widget_id: 2,
369            constraints_hash: 200,
370        };
371
372        cache.insert(key1, Size::new(10.0, 10.0));
373        cache.advance_frame();
374        cache.insert(key2, Size::new(20.0, 20.0));
375        cache.advance_frame();
376
377        // Both should still be present (threshold is 2 frames)
378        assert_eq!(cache.len(), 2);
379
380        // One more frame without touching key1
381        let _ = cache.get(key2);
382        cache.advance_frame();
383
384        // key1 should be evicted (not used in last 2 frames)
385        assert_eq!(cache.len(), 1);
386        assert_eq!(cache.get(key2), Some(Size::new(20.0, 20.0)));
387    }
388
389    #[test]
390    fn test_cache_eviction_empty_cache() {
391        let mut cache = LayoutCache::new();
392
393        // Advancing frames on empty cache should not panic
394        for _ in 0..10 {
395            cache.advance_frame();
396        }
397
398        assert!(cache.is_empty());
399    }
400
401    // =========================================================================
402    // Default Implementation
403    // =========================================================================
404
405    #[test]
406    fn test_cache_default() {
407        let cache = LayoutCache::default();
408        assert!(cache.is_empty());
409        assert_eq!(cache.hits(), 0);
410        assert_eq!(cache.misses(), 0);
411    }
412
413    // =========================================================================
414    // Debug Format
415    // =========================================================================
416
417    #[test]
418    fn test_cache_debug() {
419        let cache = LayoutCache::new();
420        let debug = format!("{:?}", cache);
421        assert!(debug.contains("LayoutCache"));
422    }
423
424    // =========================================================================
425    // Size Values
426    // =========================================================================
427
428    #[test]
429    fn test_cache_with_zero_size() {
430        let mut cache = LayoutCache::new();
431        let key = CacheKey {
432            widget_id: 1,
433            constraints_hash: 100,
434        };
435
436        cache.insert(key, Size::new(0.0, 0.0));
437        assert_eq!(cache.get(key), Some(Size::new(0.0, 0.0)));
438    }
439
440    #[test]
441    fn test_cache_with_large_size() {
442        let mut cache = LayoutCache::new();
443        let key = CacheKey {
444            widget_id: 1,
445            constraints_hash: 100,
446        };
447
448        cache.insert(key, Size::new(10000.0, 10000.0));
449        assert_eq!(cache.get(key), Some(Size::new(10000.0, 10000.0)));
450    }
451
452    #[test]
453    fn test_cache_with_fractional_size() {
454        let mut cache = LayoutCache::new();
455        let key = CacheKey {
456            widget_id: 1,
457            constraints_hash: 100,
458        };
459
460        cache.insert(key, Size::new(10.5, 20.75));
461        assert_eq!(cache.get(key), Some(Size::new(10.5, 20.75)));
462    }
463
464    // =========================================================================
465    // Hash Collision Resistance
466    // =========================================================================
467
468    #[test]
469    fn test_cache_different_widget_same_constraints() {
470        let mut cache = LayoutCache::new();
471
472        let key1 = CacheKey {
473            widget_id: 1,
474            constraints_hash: 100,
475        };
476        let key2 = CacheKey {
477            widget_id: 2,
478            constraints_hash: 100,
479        };
480
481        cache.insert(key1, Size::new(10.0, 10.0));
482        cache.insert(key2, Size::new(20.0, 20.0));
483
484        assert_eq!(cache.get(key1), Some(Size::new(10.0, 10.0)));
485        assert_eq!(cache.get(key2), Some(Size::new(20.0, 20.0)));
486        assert_eq!(cache.len(), 2);
487    }
488
489    #[test]
490    fn test_cache_same_widget_different_constraints() {
491        let mut cache = LayoutCache::new();
492
493        let key1 = CacheKey {
494            widget_id: 1,
495            constraints_hash: 100,
496        };
497        let key2 = CacheKey {
498            widget_id: 1,
499            constraints_hash: 200,
500        };
501
502        cache.insert(key1, Size::new(10.0, 10.0));
503        cache.insert(key2, Size::new(20.0, 20.0));
504
505        assert_eq!(cache.get(key1), Some(Size::new(10.0, 10.0)));
506        assert_eq!(cache.get(key2), Some(Size::new(20.0, 20.0)));
507        assert_eq!(cache.len(), 2);
508    }
509
510    // =========================================================================
511    // Frame Counter
512    // =========================================================================
513
514    #[test]
515    fn test_cache_frame_counter_overflow() {
516        let mut cache = LayoutCache::new();
517        let key = CacheKey {
518            widget_id: 1,
519            constraints_hash: 100,
520        };
521
522        cache.insert(key, Size::new(10.0, 10.0));
523
524        // Advance many frames while keeping entry fresh
525        for _ in 0..100 {
526            let _ = cache.get(key);
527            cache.advance_frame();
528        }
529
530        // Entry should still exist
531        assert_eq!(cache.get(key), Some(Size::new(10.0, 10.0)));
532    }
533}