1use ahash::AHashMap;
84use std::hash::{DefaultHasher, Hash, Hasher};
85
86use ftui_core::geometry::Size;
87
88use crate::measurable::SizeConstraints;
89
90#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
111pub struct WidgetId(pub u64);
112
113impl WidgetId {
114 #[inline]
124 pub fn from_ptr<T>(ptr: &T) -> Self {
125 Self(ptr as *const T as u64)
126 }
127
128 #[inline]
133 pub fn from_hash<T: Hash>(value: &T) -> Self {
134 let mut hasher = DefaultHasher::new();
135 value.hash(&mut hasher);
136 Self(hasher.finish())
137 }
138}
139
140#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
142struct CacheKey {
143 widget_id: WidgetId,
144 available: Size,
145}
146
147#[derive(Clone, Debug)]
149struct CacheEntry {
150 constraints: SizeConstraints,
152 generation: u64,
154 access_count: u32,
156}
157
158#[derive(Debug, Clone, Default)]
160pub struct CacheStats {
161 pub entries: usize,
163 pub hits: u64,
165 pub misses: u64,
167 pub hit_rate: f64,
169}
170
171#[derive(Debug)]
189pub struct MeasureCache {
190 entries: AHashMap<CacheKey, CacheEntry>,
191 generation: u64,
192 max_entries: usize,
193 hits: u64,
194 misses: u64,
195}
196
197impl MeasureCache {
198 #[inline]
212 pub fn new(max_entries: usize) -> Self {
213 Self {
214 entries: AHashMap::with_capacity(max_entries),
215 generation: 0,
216 max_entries,
217 hits: 0,
218 misses: 0,
219 }
220 }
221
222 pub fn get_or_compute<F>(
244 &mut self,
245 widget_id: WidgetId,
246 available: Size,
247 compute: F,
248 ) -> SizeConstraints
249 where
250 F: FnOnce() -> SizeConstraints,
251 {
252 let key = CacheKey {
253 widget_id,
254 available,
255 };
256
257 if let Some(entry) = self.entries.get_mut(&key)
259 && entry.generation == self.generation
260 {
261 self.hits += 1;
262 entry.access_count = entry.access_count.saturating_add(1);
263 return entry.constraints;
264 }
265
266 self.misses += 1;
268 let constraints = compute();
269
270 if self.entries.len() >= self.max_entries {
272 self.evict_lfu();
273 }
274
275 self.entries.insert(
277 key,
278 CacheEntry {
279 constraints,
280 generation: self.generation,
281 access_count: 1,
282 },
283 );
284
285 constraints
286 }
287
288 #[inline]
305 pub fn invalidate_all(&mut self) {
306 self.generation = self.generation.wrapping_add(1);
307 }
308
309 pub fn invalidate_widget(&mut self, widget_id: WidgetId) {
318 self.entries.retain(|k, _| k.widget_id != widget_id);
319 }
320
321 pub fn stats(&self) -> CacheStats {
334 let total = self.hits + self.misses;
335 CacheStats {
336 entries: self.entries.len(),
337 hits: self.hits,
338 misses: self.misses,
339 hit_rate: if total > 0 {
340 self.hits as f64 / total as f64
341 } else {
342 0.0
343 },
344 }
345 }
346
347 #[inline]
351 pub fn reset_stats(&mut self) {
352 self.hits = 0;
353 self.misses = 0;
354 }
355
356 #[inline]
363 pub fn clear(&mut self) {
364 self.entries.clear();
365 self.generation = self.generation.wrapping_add(1);
366 }
367
368 #[inline]
370 pub fn len(&self) -> usize {
371 self.entries.len()
372 }
373
374 #[inline]
376 pub fn is_empty(&self) -> bool {
377 self.entries.is_empty()
378 }
379
380 #[inline]
382 pub fn capacity(&self) -> usize {
383 self.max_entries
384 }
385
386 fn evict_lfu(&mut self) {
388 if let Some(key) = self
390 .entries
391 .iter()
392 .min_by_key(|(_, e)| e.access_count)
393 .map(|(k, _)| *k)
394 {
395 self.entries.remove(&key);
396 }
397 }
398}
399
400impl Default for MeasureCache {
401 fn default() -> Self {
403 Self::new(256)
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
414 fn widget_id_from_ptr_is_stable() {
415 let widget = 42u64;
416 let id1 = WidgetId::from_ptr(&widget);
417 let id2 = WidgetId::from_ptr(&widget);
418 assert_eq!(id1, id2);
419 }
420
421 #[test]
422 fn widget_id_from_hash_is_stable() {
423 let text = "hello world";
424 let id1 = WidgetId::from_hash(&text);
425 let id2 = WidgetId::from_hash(&text);
426 assert_eq!(id1, id2);
427 }
428
429 #[test]
430 fn widget_id_from_hash_differs_for_different_content() {
431 let id1 = WidgetId::from_hash(&"hello");
432 let id2 = WidgetId::from_hash(&"world");
433 assert_ne!(id1, id2);
434 }
435
436 #[test]
439 fn cache_returns_same_result() {
440 let mut cache = MeasureCache::new(100);
441 let widget_id = WidgetId(42);
442 let available = Size::new(80, 24);
443
444 let mut call_count = 0;
445 let compute = || {
446 call_count += 1;
447 SizeConstraints {
448 min: Size::new(10, 1),
449 preferred: Size::new(50, 5),
450 max: None,
451 }
452 };
453
454 let r1 = cache.get_or_compute(widget_id, available, compute);
455 let r2 = cache.get_or_compute(widget_id, available, || unreachable!("should not call"));
456
457 assert_eq!(r1, r2);
458 assert_eq!(call_count, 1); }
460
461 #[test]
462 fn different_size_is_cache_miss() {
463 let mut cache = MeasureCache::new(100);
464 let widget_id = WidgetId(42);
465
466 let mut call_count = 0;
467 let mut compute = || {
468 call_count += 1;
469 SizeConstraints {
470 min: Size::ZERO,
471 preferred: Size::new(call_count as u16, 1),
472 max: None,
473 }
474 };
475
476 cache.get_or_compute(widget_id, Size::new(80, 24), &mut compute);
477 cache.get_or_compute(widget_id, Size::new(120, 40), &mut compute);
478
479 assert_eq!(call_count, 2); }
481
482 #[test]
483 fn different_widget_is_cache_miss() {
484 let mut cache = MeasureCache::new(100);
485 let available = Size::new(80, 24);
486
487 let mut call_count = 0;
488 let mut compute = || {
489 call_count += 1;
490 SizeConstraints::ZERO
491 };
492
493 cache.get_or_compute(WidgetId(1), available, &mut compute);
494 cache.get_or_compute(WidgetId(2), available, &mut compute);
495
496 assert_eq!(call_count, 2);
497 }
498
499 #[test]
500 fn invalidation_clears_cache() {
501 let mut cache = MeasureCache::new(100);
502 let widget_id = WidgetId(42);
503 let available = Size::new(80, 24);
504
505 let mut call_count = 0;
506 let mut compute = || {
507 call_count += 1;
508 SizeConstraints::ZERO
509 };
510
511 cache.get_or_compute(widget_id, available, &mut compute);
512 cache.invalidate_all();
513 cache.get_or_compute(widget_id, available, &mut compute);
514
515 assert_eq!(call_count, 2); }
517
518 #[test]
519 fn widget_specific_invalidation() {
520 let mut cache = MeasureCache::new(100);
521 let widget1 = WidgetId(1);
522 let widget2 = WidgetId(2);
523 let available = Size::new(80, 24);
524
525 let mut count1 = 0;
526 let mut count2 = 0;
527
528 cache.get_or_compute(widget1, available, || {
529 count1 += 1;
530 SizeConstraints::ZERO
531 });
532 cache.get_or_compute(widget2, available, || {
533 count2 += 1;
534 SizeConstraints::ZERO
535 });
536
537 cache.invalidate_widget(widget1);
539
540 cache.get_or_compute(widget1, available, || {
542 count1 += 1;
543 SizeConstraints::ZERO
544 });
545 cache.get_or_compute(widget2, available, || unreachable!("should hit"));
546
547 assert_eq!(count1, 2);
548 assert_eq!(count2, 1);
549 }
550
551 #[test]
552 fn lfu_eviction_works() {
553 let mut cache = MeasureCache::new(2); cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
557 cache.get_or_compute(WidgetId(2), Size::new(10, 10), || SizeConstraints::ZERO);
558
559 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || {
561 unreachable!("should hit")
562 });
563
564 cache.get_or_compute(WidgetId(3), Size::new(10, 10), || SizeConstraints::ZERO);
566
567 assert_eq!(cache.len(), 2);
568
569 let mut was_called = false;
571 cache.get_or_compute(WidgetId(2), Size::new(10, 10), || {
572 was_called = true;
573 SizeConstraints::ZERO
574 });
575 assert!(was_called, "widget 2 should have been evicted");
576
577 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || {
579 unreachable!("widget 1 should still be cached")
580 });
581 }
582
583 #[test]
584 fn stats_track_hits_and_misses() {
585 let mut cache = MeasureCache::new(100);
586
587 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
588 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || unreachable!("hit"));
589 cache.get_or_compute(WidgetId(2), Size::new(10, 10), || SizeConstraints::ZERO);
590
591 let stats = cache.stats();
592 assert_eq!(stats.hits, 1);
593 assert_eq!(stats.misses, 2);
594 assert!((stats.hit_rate - 1.0 / 3.0).abs() < 0.01);
595 }
596
597 #[test]
598 fn reset_stats_clears_counters() {
599 let mut cache = MeasureCache::new(100);
600
601 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
602 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || unreachable!("hit"));
603
604 let stats = cache.stats();
605 assert_eq!(stats.hits, 1);
606 assert_eq!(stats.misses, 1);
607
608 cache.reset_stats();
609
610 let stats = cache.stats();
611 assert_eq!(stats.hits, 0);
612 assert_eq!(stats.misses, 0);
613 assert_eq!(stats.hit_rate, 0.0);
614 }
615
616 #[test]
617 fn clear_removes_all_entries() {
618 let mut cache = MeasureCache::new(100);
619
620 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
621 cache.get_or_compute(WidgetId(2), Size::new(10, 10), || SizeConstraints::ZERO);
622
623 assert_eq!(cache.len(), 2);
624
625 cache.clear();
626
627 assert_eq!(cache.len(), 0);
628 assert!(cache.is_empty());
629
630 let mut was_called = false;
632 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || {
633 was_called = true;
634 SizeConstraints::ZERO
635 });
636 assert!(was_called);
637 }
638
639 #[test]
640 fn default_capacity_is_256() {
641 let cache = MeasureCache::default();
642 assert_eq!(cache.capacity(), 256);
643 }
644
645 #[test]
646 fn generation_wraps_around() {
647 let mut cache = MeasureCache::new(100);
648 cache.generation = u64::MAX;
649 cache.invalidate_all();
650 assert_eq!(cache.generation, 0);
651 }
652
653 #[test]
656 fn cache_is_deterministic() {
657 let mut cache1 = MeasureCache::new(100);
658 let mut cache2 = MeasureCache::new(100);
659
660 for i in 0..10u64 {
661 let id = WidgetId(i);
662 let size = Size::new((i * 10) as u16, (i * 5) as u16);
663 let c = SizeConstraints {
664 min: Size::new(i as u16, 1),
665 preferred: Size::new((i * 2) as u16, 2),
666 max: None,
667 };
668
669 cache1.get_or_compute(id, size, || c);
670 cache2.get_or_compute(id, size, || c);
671 }
672
673 assert_eq!(cache1.stats().entries, cache2.stats().entries);
675 assert_eq!(cache1.stats().misses, cache2.stats().misses);
676 }
677
678 #[test]
679 fn widget_id_from_ptr_differs_for_different_objects() {
680 let a = 42u64;
681 let b = 42u64;
682 let id_a = WidgetId::from_ptr(&a);
683 let id_b = WidgetId::from_ptr(&b);
684 assert_ne!(id_a, id_b);
685 }
686
687 #[test]
688 fn new_cache_is_empty() {
689 let cache = MeasureCache::new(100);
690 assert!(cache.is_empty());
691 assert_eq!(cache.len(), 0);
692 }
693
694 #[test]
695 fn stats_zero_total_gives_zero_hit_rate() {
696 let cache = MeasureCache::new(100);
697 let stats = cache.stats();
698 assert_eq!(stats.hit_rate, 0.0);
699 assert_eq!(stats.entries, 0);
700 }
701
702 #[test]
703 fn hit_count_increments_on_each_access() {
704 let mut cache = MeasureCache::new(100);
705 let id = WidgetId(42);
706 let size = Size::new(80, 24);
707
708 cache.get_or_compute(id, size, || SizeConstraints::ZERO);
710
711 for _ in 0..5 {
713 cache.get_or_compute(id, size, || unreachable!("should hit"));
714 }
715
716 let stats = cache.stats();
717 assert_eq!(stats.misses, 1);
718 assert_eq!(stats.hits, 5);
719 }
720
721 #[test]
724 fn edge_zero_capacity_cache() {
725 let mut cache = MeasureCache::new(0);
726 let id = WidgetId(1);
727 let size = Size::new(10, 10);
728
729 let mut calls = 0;
731 for _ in 0..3 {
732 cache.get_or_compute(id, size, || {
733 calls += 1;
734 SizeConstraints::ZERO
735 });
736 }
737 let stats = cache.stats();
741 assert_eq!(stats.misses + stats.hits, 3);
742 }
743
744 #[test]
745 fn edge_capacity_one_evicts_on_second_widget() {
746 let mut cache = MeasureCache::new(1);
747
748 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
749 assert_eq!(cache.len(), 1);
750
751 cache.get_or_compute(WidgetId(2), Size::new(10, 10), || SizeConstraints::ZERO);
753 assert_eq!(cache.len(), 1);
754
755 let mut was_called = false;
757 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || {
758 was_called = true;
759 SizeConstraints::ZERO
760 });
761 assert!(was_called, "widget 1 should have been evicted");
762 }
763
764 #[test]
765 fn edge_invalidate_all_multiple_times() {
766 let mut cache = MeasureCache::new(100);
767 let gen_before = cache.generation;
768 cache.invalidate_all();
769 cache.invalidate_all();
770 cache.invalidate_all();
771 assert_eq!(cache.generation, gen_before + 3);
772 }
773
774 #[test]
775 fn edge_invalidate_widget_nonexistent() {
776 let mut cache = MeasureCache::new(100);
777 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
778 assert_eq!(cache.len(), 1);
779
780 cache.invalidate_widget(WidgetId(999));
782 assert_eq!(cache.len(), 1);
783 }
784
785 #[test]
786 fn edge_invalidate_widget_removes_all_sizes() {
787 let mut cache = MeasureCache::new(100);
788 let id = WidgetId(42);
789
790 cache.get_or_compute(id, Size::new(80, 24), || SizeConstraints::ZERO);
792 cache.get_or_compute(id, Size::new(120, 40), || SizeConstraints::ZERO);
793 cache.get_or_compute(id, Size::new(200, 60), || SizeConstraints::ZERO);
794 assert_eq!(cache.len(), 3);
795
796 cache.invalidate_widget(id);
797 assert_eq!(cache.len(), 0);
798 }
799
800 #[test]
801 fn edge_stale_entry_treated_as_miss() {
802 let mut cache = MeasureCache::new(100);
803 let id = WidgetId(1);
804 let size = Size::new(10, 10);
805
806 cache.get_or_compute(id, size, || SizeConstraints::ZERO);
807 assert_eq!(cache.stats().misses, 1);
808 assert_eq!(cache.stats().hits, 0);
809
810 cache.invalidate_all();
812
813 let mut called = false;
815 cache.get_or_compute(id, size, || {
816 called = true;
817 SizeConstraints::ZERO
818 });
819 assert!(called, "stale entry should be treated as miss");
820 assert_eq!(cache.stats().misses, 2);
821 }
822
823 #[test]
824 fn edge_lfu_equal_access_counts() {
825 let mut cache = MeasureCache::new(2);
826
827 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
829 cache.get_or_compute(WidgetId(2), Size::new(10, 10), || SizeConstraints::ZERO);
830
831 cache.get_or_compute(WidgetId(3), Size::new(10, 10), || SizeConstraints::ZERO);
833 assert_eq!(cache.len(), 2);
834
835 let mut evicted = 0;
837 for id_val in [1u64, 2u64] {
838 let mut called = false;
839 cache.get_or_compute(WidgetId(id_val), Size::new(10, 10), || {
840 called = true;
841 SizeConstraints::ZERO
842 });
843 if called {
844 evicted += 1;
845 }
846 }
847 assert!(evicted >= 1, "at least one entry should be evicted");
848 }
849
850 #[test]
851 fn edge_size_zero_as_cache_key() {
852 let mut cache = MeasureCache::new(100);
853 let id = WidgetId(1);
854
855 cache.get_or_compute(id, Size::ZERO, || SizeConstraints::ZERO);
856 cache.get_or_compute(id, Size::ZERO, || unreachable!("should hit for Size::ZERO"));
858 assert_eq!(cache.stats().hits, 1);
859 }
860
861 #[test]
862 fn edge_widget_id_zero() {
863 let mut cache = MeasureCache::new(100);
864 cache.get_or_compute(WidgetId(0), Size::new(10, 10), || SizeConstraints::ZERO);
865 cache.get_or_compute(WidgetId(0), Size::new(10, 10), || {
866 unreachable!("should hit for WidgetId(0)")
867 });
868 assert_eq!(cache.stats().hits, 1);
869 }
870
871 #[test]
872 fn edge_clear_preserves_stats() {
873 let mut cache = MeasureCache::new(100);
874 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
875 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || unreachable!());
876
877 cache.clear();
878 assert!(cache.is_empty());
879 let stats = cache.stats();
881 assert_eq!(stats.hits, 1);
882 assert_eq!(stats.misses, 1);
883 }
884
885 #[test]
886 fn edge_reset_stats_preserves_entries() {
887 let mut cache = MeasureCache::new(100);
888 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
889 assert_eq!(cache.len(), 1);
890
891 cache.reset_stats();
892 assert_eq!(cache.len(), 1);
893 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || {
895 unreachable!("should hit")
896 });
897 assert_eq!(cache.stats().hits, 1);
898 }
899
900 #[test]
901 fn edge_entries_never_exceed_capacity() {
902 let cap = 5;
903 let mut cache = MeasureCache::new(cap);
904
905 for i in 0..100u64 {
906 cache.get_or_compute(WidgetId(i), Size::new(10, 10), || SizeConstraints::ZERO);
907 assert!(
908 cache.len() <= cap,
909 "len {} > capacity {} at i={}",
910 cache.len(),
911 cap,
912 i
913 );
914 }
915 }
916
917 #[test]
918 fn edge_clear_bumps_generation() {
919 let mut cache = MeasureCache::new(100);
920 let gen_before = cache.generation;
921 cache.clear();
922 assert_eq!(cache.generation, gen_before + 1);
923 }
924
925 #[test]
926 fn edge_invalidate_all_stale_but_still_counted_in_len() {
927 let mut cache = MeasureCache::new(100);
928 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
929 cache.get_or_compute(WidgetId(2), Size::new(10, 10), || SizeConstraints::ZERO);
930 assert_eq!(cache.len(), 2);
931
932 cache.invalidate_all();
933 assert_eq!(cache.len(), 2);
935 }
936
937 #[test]
938 fn edge_invalidate_widget_does_not_affect_stats() {
939 let mut cache = MeasureCache::new(100);
940 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || SizeConstraints::ZERO);
941 cache.get_or_compute(WidgetId(1), Size::new(10, 10), || unreachable!());
942 assert_eq!(cache.stats().hits, 1);
943 assert_eq!(cache.stats().misses, 1);
944
945 cache.invalidate_widget(WidgetId(1));
946 assert_eq!(cache.stats().hits, 1);
948 assert_eq!(cache.stats().misses, 1);
949 }
950
951 #[test]
952 fn edge_get_or_compute_returns_computed_value() {
953 let mut cache = MeasureCache::new(100);
954 let expected = SizeConstraints {
955 min: Size::new(5, 3),
956 preferred: Size::new(40, 10),
957 max: Some(Size::new(100, 50)),
958 };
959 let result = cache.get_or_compute(WidgetId(1), Size::new(80, 24), || expected);
960 assert_eq!(result, expected);
961 }
962
963 #[test]
964 fn edge_cache_stats_default() {
965 let stats = CacheStats::default();
966 assert_eq!(stats.entries, 0);
967 assert_eq!(stats.hits, 0);
968 assert_eq!(stats.misses, 0);
969 assert_eq!(stats.hit_rate, 0.0);
970 }
971
972 #[test]
973 fn edge_cache_stats_clone_debug() {
974 let stats = CacheStats {
975 entries: 5,
976 hits: 10,
977 misses: 3,
978 hit_rate: 0.769,
979 };
980 let cloned = stats.clone();
981 assert_eq!(cloned.entries, 5);
982 assert_eq!(cloned.hits, 10);
983 let _ = format!("{stats:?}");
984 }
985
986 #[test]
987 fn edge_measure_cache_debug() {
988 let cache = MeasureCache::new(100);
989 let debug = format!("{cache:?}");
990 assert!(debug.contains("MeasureCache"), "{debug}");
991 }
992
993 #[test]
994 fn edge_widget_id_copy_clone_hash_debug() {
995 let id = WidgetId(42);
996 let copied: WidgetId = id; assert_eq!(copied, id);
998 let _ = format!("{id:?}");
999
1000 use std::collections::HashSet;
1002 let mut set = HashSet::new();
1003 set.insert(id);
1004 assert!(set.contains(&WidgetId(42)));
1005 assert!(!set.contains(&WidgetId(43)));
1006 }
1007}