1use std::hash::{Hash, Hasher};
54
55use ftui_core::geometry::Rect;
56use rustc_hash::{FxHashMap, FxHasher};
57
58use crate::{Constraint, Direction, LayoutSizeHint};
59
60#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
68pub struct LayoutCacheKey {
69 pub area_x: u16,
71 pub area_y: u16,
73 pub area_width: u16,
75 pub area_height: u16,
77 pub constraints_hash: u64,
79 pub direction: Direction,
81 pub intrinsics_hash: Option<u64>,
83}
84
85impl LayoutCacheKey {
86 pub fn new(
95 area: Rect,
96 constraints: &[Constraint],
97 direction: Direction,
98 intrinsics: Option<&[LayoutSizeHint]>,
99 ) -> Self {
100 Self {
101 area_x: area.x,
102 area_y: area.y,
103 area_width: area.width,
104 area_height: area.height,
105 constraints_hash: Self::hash_constraints(constraints),
106 direction,
107 intrinsics_hash: intrinsics.map(Self::hash_intrinsics),
108 }
109 }
110
111 #[inline]
113 pub fn area(&self) -> Rect {
114 Rect::new(self.area_x, self.area_y, self.area_width, self.area_height)
115 }
116
117 fn hash_constraints(constraints: &[Constraint]) -> u64 {
119 let mut hasher = FxHasher::default();
120 for c in constraints {
121 std::mem::discriminant(c).hash(&mut hasher);
123 match c {
124 Constraint::Fixed(v) => v.hash(&mut hasher),
125 Constraint::Percentage(p) => p.to_bits().hash(&mut hasher),
126 Constraint::Min(v) => v.hash(&mut hasher),
127 Constraint::Max(v) => v.hash(&mut hasher),
128 Constraint::Ratio(n, d) => {
129 fn gcd(mut a: u32, mut b: u32) -> u32 {
130 while b != 0 {
131 let t = b;
132 b = a % b;
133 a = t;
134 }
135 a
136 }
137 let divisor = gcd(*n, *d);
138 if let (Some(n_div), Some(d_div)) =
139 (n.checked_div(divisor), d.checked_div(divisor))
140 {
141 n_div.hash(&mut hasher);
142 d_div.hash(&mut hasher);
143 } else {
144 n.hash(&mut hasher);
145 d.hash(&mut hasher);
146 }
147 }
148 Constraint::Fill => {}
149 Constraint::FitContent => {}
150 Constraint::FitContentBounded { min, max } => {
151 min.hash(&mut hasher);
152 max.hash(&mut hasher);
153 }
154 Constraint::FitMin => {}
155 }
156 }
157 hasher.finish()
158 }
159
160 fn hash_intrinsics(intrinsics: &[LayoutSizeHint]) -> u64 {
162 let mut hasher = FxHasher::default();
163 for hint in intrinsics {
164 hint.min.hash(&mut hasher);
165 hint.preferred.hash(&mut hasher);
166 hint.max.hash(&mut hasher);
167 }
168 hasher.finish()
169 }
170}
171
172#[derive(Clone, Debug)]
174struct CachedLayoutEntry {
175 chunks: Vec<Rect>,
177 generation: u64,
179 access_count: u32,
181}
182
183#[derive(Debug, Clone, Default)]
185pub struct LayoutCacheStats {
186 pub entries: usize,
188 pub hits: u64,
190 pub misses: u64,
192 pub hit_rate: f64,
194}
195
196#[derive(Debug)]
213pub struct LayoutCache {
214 entries: FxHashMap<LayoutCacheKey, CachedLayoutEntry>,
215 generation: u64,
216 max_entries: usize,
217 hits: u64,
218 misses: u64,
219}
220
221impl LayoutCache {
222 #[inline]
236 pub fn new(max_entries: usize) -> Self {
237 Self {
238 entries: FxHashMap::with_capacity_and_hasher(max_entries, Default::default()),
239 generation: 0,
240 max_entries,
241 hits: 0,
242 misses: 0,
243 }
244 }
245
246 pub fn get_or_compute<F>(&mut self, key: LayoutCacheKey, compute: F) -> Vec<Rect>
264 where
265 F: FnOnce() -> Vec<Rect>,
266 {
267 if let Some(entry) = self.entries.get_mut(&key)
269 && entry.generation == self.generation
270 {
271 self.hits += 1;
272 entry.access_count = entry.access_count.saturating_add(1);
273 return entry.chunks.clone();
274 }
275
276 self.misses += 1;
278 let chunks = compute();
279
280 if self.entries.len() >= self.max_entries {
282 self.evict_lru();
283 }
284
285 self.entries.insert(
287 key,
288 CachedLayoutEntry {
289 chunks: chunks.clone(),
290 generation: self.generation,
291 access_count: 1,
292 },
293 );
294
295 chunks
296 }
297
298 #[inline]
314 pub fn invalidate_all(&mut self) {
315 self.generation = self.generation.wrapping_add(1);
316 }
317
318 pub fn stats(&self) -> LayoutCacheStats {
322 let total = self.hits + self.misses;
323 LayoutCacheStats {
324 entries: self.entries.len(),
325 hits: self.hits,
326 misses: self.misses,
327 hit_rate: if total > 0 {
328 self.hits as f64 / total as f64
329 } else {
330 0.0
331 },
332 }
333 }
334
335 #[inline]
339 pub fn reset_stats(&mut self) {
340 self.hits = 0;
341 self.misses = 0;
342 }
343
344 #[inline]
350 pub fn clear(&mut self) {
351 self.entries.clear();
352 self.generation = self.generation.wrapping_add(1);
353 }
354
355 #[inline]
357 pub fn len(&self) -> usize {
358 self.entries.len()
359 }
360
361 #[inline]
363 pub fn is_empty(&self) -> bool {
364 self.entries.is_empty()
365 }
366
367 #[inline]
369 pub fn capacity(&self) -> usize {
370 self.max_entries
371 }
372
373 fn evict_lru(&mut self) {
375 if let Some(key) = self
376 .entries
377 .iter()
378 .min_by_key(|(_, e)| e.access_count)
379 .map(|(k, _)| *k)
380 {
381 self.entries.remove(&key);
382 }
383 }
384}
385
386impl Default for LayoutCache {
387 fn default() -> Self {
389 Self::new(64)
390 }
391}
392
393#[derive(Debug)]
406pub struct S3FifoLayoutCache {
407 cache: ftui_core::s3_fifo::S3Fifo<LayoutCacheKey, CachedLayoutEntry>,
408 generation: u64,
409 max_entries: usize,
410 hits: u64,
411 misses: u64,
412}
413
414impl S3FifoLayoutCache {
415 #[inline]
417 pub fn new(max_entries: usize) -> Self {
418 Self {
419 cache: ftui_core::s3_fifo::S3Fifo::new(max_entries.max(2)),
420 generation: 0,
421 max_entries: max_entries.max(2),
422 hits: 0,
423 misses: 0,
424 }
425 }
426
427 pub fn get_or_compute<F>(&mut self, key: LayoutCacheKey, compute: F) -> Vec<Rect>
432 where
433 F: FnOnce() -> Vec<Rect>,
434 {
435 if let Some(entry) = self.cache.get(&key) {
436 if entry.generation == self.generation {
437 self.hits += 1;
438 return entry.chunks.clone();
439 }
440 self.cache.remove(&key);
442 }
443
444 self.misses += 1;
445 let chunks = compute();
446
447 self.cache.insert(
448 key,
449 CachedLayoutEntry {
450 chunks: chunks.clone(),
451 generation: self.generation,
452 access_count: 1,
453 },
454 );
455
456 chunks
457 }
458
459 #[inline]
461 pub fn invalidate_all(&mut self) {
462 self.generation = self.generation.wrapping_add(1);
463 }
464
465 pub fn stats(&self) -> LayoutCacheStats {
467 let total = self.hits + self.misses;
468 LayoutCacheStats {
469 entries: self.cache.len(),
470 hits: self.hits,
471 misses: self.misses,
472 hit_rate: if total > 0 {
473 self.hits as f64 / total as f64
474 } else {
475 0.0
476 },
477 }
478 }
479
480 #[inline]
482 pub fn reset_stats(&mut self) {
483 self.hits = 0;
484 self.misses = 0;
485 }
486
487 #[inline]
489 pub fn clear(&mut self) {
490 self.cache.clear();
491 self.generation = self.generation.wrapping_add(1);
492 }
493
494 #[inline]
496 pub fn len(&self) -> usize {
497 self.cache.len()
498 }
499
500 #[inline]
502 pub fn is_empty(&self) -> bool {
503 self.cache.is_empty()
504 }
505
506 #[inline]
508 pub fn capacity(&self) -> usize {
509 self.max_entries
510 }
511}
512
513impl Default for S3FifoLayoutCache {
514 fn default() -> Self {
515 Self::new(64)
516 }
517}
518
519#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
530pub struct CoherenceId {
531 pub constraints_hash: u64,
533 pub direction: Direction,
535}
536
537impl CoherenceId {
538 pub fn new(constraints: &[Constraint], direction: Direction) -> Self {
540 Self {
541 constraints_hash: LayoutCacheKey::hash_constraints(constraints),
542 direction,
543 }
544 }
545
546 pub fn from_cache_key(key: &LayoutCacheKey) -> Self {
548 Self {
549 constraints_hash: key.constraints_hash,
550 direction: key.direction,
551 }
552 }
553}
554
555#[derive(Debug)]
583pub struct CoherenceCache {
584 entries: FxHashMap<CoherenceId, CoherenceEntry>,
585 max_entries: usize,
586 tick: u64,
588}
589
590#[derive(Debug, Clone)]
591struct CoherenceEntry {
592 allocation: Vec<u16>,
594 last_stored: u64,
596}
597
598impl CoherenceCache {
599 pub fn new(max_entries: usize) -> Self {
601 Self {
602 entries: FxHashMap::with_capacity_and_hasher(max_entries.min(256), Default::default()),
603 max_entries,
604 tick: 0,
605 }
606 }
607
608 #[inline]
613 pub fn get(&self, id: &CoherenceId) -> Option<Vec<u16>> {
614 self.entries.get(id).map(|e| e.allocation.clone())
615 }
616
617 pub fn store(&mut self, id: CoherenceId, allocation: Vec<u16>) {
621 self.tick = self.tick.wrapping_add(1);
622
623 if self.entries.len() >= self.max_entries && !self.entries.contains_key(&id) {
624 self.evict_oldest();
625 }
626
627 self.entries.insert(
628 id,
629 CoherenceEntry {
630 allocation,
631 last_stored: self.tick,
632 },
633 );
634 }
635
636 #[inline]
638 pub fn clear(&mut self) {
639 self.entries.clear();
640 }
641
642 #[inline]
644 pub fn len(&self) -> usize {
645 self.entries.len()
646 }
647
648 #[inline]
650 pub fn is_empty(&self) -> bool {
651 self.entries.is_empty()
652 }
653
654 pub fn displacement(&self, id: &CoherenceId, new_alloc: &[u16]) -> (u64, u32) {
659 match self.entries.get(id) {
660 Some(entry) => {
661 let prev = &entry.allocation;
662 let len = prev.len().min(new_alloc.len());
663 let mut sum: u64 = 0;
664 let mut max: u32 = 0;
665 for i in 0..len {
666 let d = (new_alloc[i] as i32 - prev[i] as i32).unsigned_abs();
667 sum += u64::from(d);
668 max = max.max(d);
669 }
670 for &v in &prev[len..] {
672 sum += u64::from(v);
673 max = max.max(u32::from(v));
674 }
675 for &v in &new_alloc[len..] {
676 sum += u64::from(v);
677 max = max.max(u32::from(v));
678 }
679 (sum, max)
680 }
681 None => (0, 0),
682 }
683 }
684
685 fn evict_oldest(&mut self) {
686 if let Some(key) = self
687 .entries
688 .iter()
689 .min_by_key(|(_, e)| e.last_stored)
690 .map(|(k, _)| *k)
691 {
692 self.entries.remove(&key);
693 }
694 }
695}
696
697impl Default for CoherenceCache {
698 fn default() -> Self {
699 Self::new(64)
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 fn make_key(width: u16, height: u16) -> LayoutCacheKey {
708 LayoutCacheKey::new(
709 Rect::new(0, 0, width, height),
710 &[Constraint::Percentage(50.0), Constraint::Fill],
711 Direction::Horizontal,
712 None,
713 )
714 }
715
716 fn should_not_call(label: &str) -> Vec<Rect> {
717 unreachable!("{label}");
718 }
719
720 #[test]
723 fn same_params_produce_same_key() {
724 let k1 = make_key(80, 24);
725 let k2 = make_key(80, 24);
726 assert_eq!(k1, k2);
727 }
728
729 #[test]
730 fn different_area_different_key() {
731 let k1 = make_key(80, 24);
732 let k2 = make_key(120, 40);
733 assert_ne!(k1, k2);
734 }
735
736 #[test]
737 fn different_constraints_different_key() {
738 let k1 = LayoutCacheKey::new(
739 Rect::new(0, 0, 80, 24),
740 &[Constraint::Fixed(20)],
741 Direction::Horizontal,
742 None,
743 );
744 let k2 = LayoutCacheKey::new(
745 Rect::new(0, 0, 80, 24),
746 &[Constraint::Fixed(30)],
747 Direction::Horizontal,
748 None,
749 );
750 assert_ne!(k1, k2);
751 }
752
753 #[test]
754 fn different_direction_different_key() {
755 let k1 = LayoutCacheKey::new(
756 Rect::new(0, 0, 80, 24),
757 &[Constraint::Fill],
758 Direction::Horizontal,
759 None,
760 );
761 let k2 = LayoutCacheKey::new(
762 Rect::new(0, 0, 80, 24),
763 &[Constraint::Fill],
764 Direction::Vertical,
765 None,
766 );
767 assert_ne!(k1, k2);
768 }
769
770 #[test]
771 fn different_intrinsics_different_key() {
772 let hints1 = [LayoutSizeHint {
773 min: 10,
774 preferred: 20,
775 max: None,
776 }];
777 let hints2 = [LayoutSizeHint {
778 min: 10,
779 preferred: 30,
780 max: None,
781 }];
782
783 let k1 = LayoutCacheKey::new(
784 Rect::new(0, 0, 80, 24),
785 &[Constraint::FitContent],
786 Direction::Horizontal,
787 Some(&hints1),
788 );
789 let k2 = LayoutCacheKey::new(
790 Rect::new(0, 0, 80, 24),
791 &[Constraint::FitContent],
792 Direction::Horizontal,
793 Some(&hints2),
794 );
795 assert_ne!(k1, k2);
796 }
797
798 #[test]
801 fn cache_returns_same_result() {
802 let mut cache = LayoutCache::new(100);
803 let key = make_key(80, 24);
804
805 let mut compute_count = 0;
806 let compute = || {
807 compute_count += 1;
808 vec![Rect::new(0, 0, 40, 24), Rect::new(40, 0, 40, 24)]
809 };
810
811 let r1 = cache.get_or_compute(key, compute);
812 let r2 = cache.get_or_compute(key, || should_not_call("should not call"));
813
814 assert_eq!(r1, r2);
815 assert_eq!(compute_count, 1);
816 }
817
818 #[test]
819 fn different_area_is_cache_miss() {
820 let mut cache = LayoutCache::new(100);
821
822 let mut compute_count = 0;
823 let mut compute = || {
824 compute_count += 1;
825 vec![Rect::default()]
826 };
827
828 let k1 = make_key(80, 24);
829 let k2 = make_key(120, 40);
830
831 cache.get_or_compute(k1, &mut compute);
832 cache.get_or_compute(k2, &mut compute);
833
834 assert_eq!(compute_count, 2);
835 }
836
837 #[test]
838 fn invalidation_clears_cache() {
839 let mut cache = LayoutCache::new(100);
840 let key = make_key(80, 24);
841
842 let mut compute_count = 0;
843 let mut compute = || {
844 compute_count += 1;
845 vec![]
846 };
847
848 cache.get_or_compute(key, &mut compute);
849 cache.invalidate_all();
850 cache.get_or_compute(key, &mut compute);
851
852 assert_eq!(compute_count, 2);
853 }
854
855 #[test]
856 fn lru_eviction_works() {
857 let mut cache = LayoutCache::new(2);
858
859 let k1 = make_key(10, 10);
860 let k2 = make_key(20, 20);
861 let k3 = make_key(30, 30);
862
863 cache.get_or_compute(k1, || vec![Rect::new(0, 0, 10, 10)]);
865 cache.get_or_compute(k2, || vec![Rect::new(0, 0, 20, 20)]);
866
867 cache.get_or_compute(k1, || should_not_call("k1 should hit"));
869
870 cache.get_or_compute(k3, || vec![Rect::new(0, 0, 30, 30)]);
872
873 assert_eq!(cache.len(), 2);
874
875 let mut was_called = false;
877 cache.get_or_compute(k2, || {
878 was_called = true;
879 vec![]
880 });
881 assert!(was_called, "k2 should have been evicted");
882
883 cache.get_or_compute(k1, || should_not_call("k1 should still be cached"));
885 }
886
887 #[test]
888 fn stats_track_hits_and_misses() {
889 let mut cache = LayoutCache::new(100);
890
891 let k1 = make_key(80, 24);
892 let k2 = make_key(120, 40);
893
894 cache.get_or_compute(k1, Vec::new); cache.get_or_compute(k1, || should_not_call("hit")); cache.get_or_compute(k2, Vec::new); let stats = cache.stats();
899 assert_eq!(stats.hits, 1);
900 assert_eq!(stats.misses, 2);
901 assert!((stats.hit_rate - 1.0 / 3.0).abs() < 0.01);
902 }
903
904 #[test]
905 fn reset_stats_clears_counters() {
906 let mut cache = LayoutCache::new(100);
907 let key = make_key(80, 24);
908
909 cache.get_or_compute(key, Vec::new);
910 cache.get_or_compute(key, || should_not_call("hit"));
911
912 let stats = cache.stats();
913 assert_eq!(stats.hits, 1);
914 assert_eq!(stats.misses, 1);
915
916 cache.reset_stats();
917
918 let stats = cache.stats();
919 assert_eq!(stats.hits, 0);
920 assert_eq!(stats.misses, 0);
921 assert_eq!(stats.hit_rate, 0.0);
922 }
923
924 #[test]
925 fn clear_removes_all_entries() {
926 let mut cache = LayoutCache::new(100);
927
928 cache.get_or_compute(make_key(80, 24), Vec::new);
929 cache.get_or_compute(make_key(120, 40), Vec::new);
930
931 assert_eq!(cache.len(), 2);
932
933 cache.clear();
934
935 assert_eq!(cache.len(), 0);
936 assert!(cache.is_empty());
937
938 let mut was_called = false;
940 cache.get_or_compute(make_key(80, 24), || {
941 was_called = true;
942 vec![]
943 });
944 assert!(was_called);
945 }
946
947 #[test]
948 fn default_capacity_is_64() {
949 let cache = LayoutCache::default();
950 assert_eq!(cache.capacity(), 64);
951 }
952
953 #[test]
954 fn generation_wraps_around() {
955 let mut cache = LayoutCache::new(100);
956 cache.generation = u64::MAX;
957 cache.invalidate_all();
958 assert_eq!(cache.generation, 0);
959 }
960
961 #[test]
964 fn constraint_hash_is_stable() {
965 let constraints = [
966 Constraint::Fixed(20),
967 Constraint::Percentage(50.0),
968 Constraint::Min(10),
969 ];
970
971 let h1 = LayoutCacheKey::hash_constraints(&constraints);
972 let h2 = LayoutCacheKey::hash_constraints(&constraints);
973
974 assert_eq!(h1, h2);
975 }
976
977 #[test]
978 fn different_constraint_values_different_hash() {
979 let c1 = [Constraint::Fixed(20)];
980 let c2 = [Constraint::Fixed(30)];
981
982 let h1 = LayoutCacheKey::hash_constraints(&c1);
983 let h2 = LayoutCacheKey::hash_constraints(&c2);
984
985 assert_ne!(h1, h2);
986 }
987
988 #[test]
989 fn different_constraint_types_different_hash() {
990 let c1 = [Constraint::Fixed(20)];
991 let c2 = [Constraint::Min(20)];
992
993 let h1 = LayoutCacheKey::hash_constraints(&c1);
994 let h2 = LayoutCacheKey::hash_constraints(&c2);
995
996 assert_ne!(h1, h2);
997 }
998
999 #[test]
1000 fn fit_content_bounded_values_in_hash() {
1001 let c1 = [Constraint::FitContentBounded { min: 10, max: 50 }];
1002 let c2 = [Constraint::FitContentBounded { min: 10, max: 60 }];
1003
1004 let h1 = LayoutCacheKey::hash_constraints(&c1);
1005 let h2 = LayoutCacheKey::hash_constraints(&c2);
1006
1007 assert_ne!(h1, h2);
1008 }
1009
1010 #[test]
1013 fn intrinsics_hash_is_stable() {
1014 let hints = [
1015 LayoutSizeHint {
1016 min: 10,
1017 preferred: 20,
1018 max: Some(30),
1019 },
1020 LayoutSizeHint {
1021 min: 5,
1022 preferred: 15,
1023 max: None,
1024 },
1025 ];
1026
1027 let h1 = LayoutCacheKey::hash_intrinsics(&hints);
1028 let h2 = LayoutCacheKey::hash_intrinsics(&hints);
1029
1030 assert_eq!(h1, h2);
1031 }
1032
1033 #[test]
1034 fn different_intrinsics_different_hash() {
1035 let h1 = [LayoutSizeHint {
1036 min: 10,
1037 preferred: 20,
1038 max: None,
1039 }];
1040 let h2 = [LayoutSizeHint {
1041 min: 10,
1042 preferred: 25,
1043 max: None,
1044 }];
1045
1046 let hash1 = LayoutCacheKey::hash_intrinsics(&h1);
1047 let hash2 = LayoutCacheKey::hash_intrinsics(&h2);
1048
1049 assert_ne!(hash1, hash2);
1050 }
1051
1052 #[test]
1055 fn cache_is_deterministic() {
1056 let mut cache1 = LayoutCache::new(100);
1057 let mut cache2 = LayoutCache::new(100);
1058
1059 for i in 0..10u16 {
1060 let key = make_key(i * 10, i * 5);
1061 let result = vec![Rect::new(0, 0, i, i)];
1062
1063 cache1.get_or_compute(key, || result.clone());
1064 cache2.get_or_compute(key, || result);
1065 }
1066
1067 assert_eq!(cache1.stats().entries, cache2.stats().entries);
1068 assert_eq!(cache1.stats().misses, cache2.stats().misses);
1069 }
1070
1071 #[test]
1072 fn hit_count_increments_on_each_access() {
1073 let mut cache = LayoutCache::new(100);
1074 let key = make_key(80, 24);
1075
1076 cache.get_or_compute(key, Vec::new);
1078
1079 for _ in 0..5 {
1081 cache.get_or_compute(key, || should_not_call("should hit"));
1082 }
1083
1084 let stats = cache.stats();
1085 assert_eq!(stats.misses, 1);
1086 assert_eq!(stats.hits, 5);
1087 }
1088
1089 fn make_coherence_id(n: u16) -> CoherenceId {
1094 CoherenceId::new(
1095 &[Constraint::Fixed(n), Constraint::Fill],
1096 Direction::Horizontal,
1097 )
1098 }
1099
1100 #[test]
1101 fn coherence_store_and_get() {
1102 let mut cc = CoherenceCache::new(64);
1103 let id = make_coherence_id(1);
1104
1105 assert!(cc.get(&id).is_none());
1106
1107 cc.store(id, vec![30, 50]);
1108 assert_eq!(cc.get(&id), Some(vec![30, 50]));
1109 }
1110
1111 #[test]
1112 fn coherence_update_replaces_allocation() {
1113 let mut cc = CoherenceCache::new(64);
1114 let id = make_coherence_id(1);
1115
1116 cc.store(id, vec![30, 50]);
1117 cc.store(id, vec![31, 49]);
1118
1119 assert_eq!(cc.get(&id), Some(vec![31, 49]));
1120 assert_eq!(cc.len(), 1);
1121 }
1122
1123 #[test]
1124 fn coherence_different_ids_are_separate() {
1125 let mut cc = CoherenceCache::new(64);
1126 let id1 = make_coherence_id(1);
1127 let id2 = make_coherence_id(2);
1128
1129 cc.store(id1, vec![40, 40]);
1130 cc.store(id2, vec![30, 50]);
1131
1132 assert_eq!(cc.get(&id1), Some(vec![40, 40]));
1133 assert_eq!(cc.get(&id2), Some(vec![30, 50]));
1134 }
1135
1136 #[test]
1137 fn coherence_eviction_at_capacity() {
1138 let mut cc = CoherenceCache::new(2);
1139
1140 let id1 = make_coherence_id(1);
1141 let id2 = make_coherence_id(2);
1142 let id3 = make_coherence_id(3);
1143
1144 cc.store(id1, vec![10]);
1145 cc.store(id2, vec![20]);
1146 cc.store(id3, vec![30]);
1147
1148 assert_eq!(cc.len(), 2);
1149 assert!(cc.get(&id1).is_none());
1151 assert_eq!(cc.get(&id2), Some(vec![20]));
1152 assert_eq!(cc.get(&id3), Some(vec![30]));
1153 }
1154
1155 #[test]
1156 fn coherence_clear() {
1157 let mut cc = CoherenceCache::new(64);
1158 let id = make_coherence_id(1);
1159
1160 cc.store(id, vec![10, 20]);
1161 assert_eq!(cc.len(), 1);
1162
1163 cc.clear();
1164 assert!(cc.is_empty());
1165 assert!(cc.get(&id).is_none());
1166 }
1167
1168 #[test]
1169 fn coherence_displacement_with_previous() {
1170 let mut cc = CoherenceCache::new(64);
1171 let id = make_coherence_id(1);
1172
1173 cc.store(id, vec![30, 50]);
1174
1175 let (sum, max) = cc.displacement(&id, &[32, 48]);
1177 assert_eq!(sum, 4); assert_eq!(max, 2);
1179 }
1180
1181 #[test]
1182 fn coherence_displacement_without_previous() {
1183 let cc = CoherenceCache::new(64);
1184 let id = make_coherence_id(1);
1185
1186 let (sum, max) = cc.displacement(&id, &[30, 50]);
1187 assert_eq!(sum, 0);
1188 assert_eq!(max, 0);
1189 }
1190
1191 #[test]
1192 fn coherence_displacement_different_lengths() {
1193 let mut cc = CoherenceCache::new(64);
1194 let id = make_coherence_id(1);
1195
1196 cc.store(id, vec![30, 50]);
1197
1198 let (sum, max) = cc.displacement(&id, &[30, 50, 10]);
1200 assert_eq!(sum, 10);
1201 assert_eq!(max, 10);
1202 }
1203
1204 #[test]
1205 fn coherence_from_cache_key() {
1206 let key = make_key(80, 24);
1207 let id = CoherenceId::from_cache_key(&key);
1208
1209 let key2 = make_key(120, 40);
1211 let id2 = CoherenceId::from_cache_key(&key2);
1212
1213 assert_eq!(id, id2);
1214 }
1215
1216 #[test]
1219 fn unit_cache_reuse_unchanged_constraints_yield_identical_layout() {
1220 use crate::round_layout_stable;
1221
1222 let mut cc = CoherenceCache::new(64);
1223 let id = make_coherence_id(1);
1224
1225 let targets = [26.67, 26.67, 26.66];
1227 let total = 80;
1228 let alloc1 = round_layout_stable(&targets, total, cc.get(&id));
1229 cc.store(id, alloc1.clone());
1230
1231 let alloc2 = round_layout_stable(&targets, total, cc.get(&id));
1233 assert_eq!(alloc1, alloc2, "Same inputs should yield identical layout");
1234 }
1235
1236 #[test]
1239 fn e2e_resize_sweep_bounded_displacement() {
1240 use crate::round_layout_stable;
1241
1242 let mut cc = CoherenceCache::new(64);
1243 let id = make_coherence_id(1);
1244
1245 let mut max_displacement_ever: u32 = 0;
1248 let mut total_displacement_sum: u64 = 0;
1249 let steps = 61; for width in 60u16..=120 {
1252 let third = f64::from(width) / 3.0;
1253 let targets = [third, third, third];
1254
1255 let prev = cc.get(&id);
1256 let alloc = round_layout_stable(&targets, width, prev);
1257
1258 let (d_sum, d_max) = cc.displacement(&id, &alloc);
1259 total_displacement_sum += d_sum;
1260 max_displacement_ever = max_displacement_ever.max(d_max);
1261
1262 cc.store(id, alloc);
1263 }
1264
1265 assert!(
1268 max_displacement_ever <= 2,
1269 "Max single-step displacement should be <=2 cells, got {}",
1270 max_displacement_ever
1271 );
1272
1273 let avg = total_displacement_sum as f64 / steps as f64;
1275 assert!(
1276 avg < 3.0,
1277 "Average displacement per step should be <3 cells, got {:.2}",
1278 avg
1279 );
1280 }
1281
1282 #[test]
1283 fn e2e_resize_sweep_deterministic() {
1284 use crate::round_layout_stable;
1285
1286 let sweep = |seed: u16| -> Vec<(u16, Vec<u16>, u64, u32)> {
1288 let mut cc = CoherenceCache::new(64);
1289 let id = CoherenceId::new(
1290 &[Constraint::Percentage(30.0), Constraint::Fill],
1291 Direction::Horizontal,
1292 );
1293
1294 let mut log = Vec::new();
1295 for width in (40 + seed)..(100 + seed) {
1296 let targets = [f64::from(width) * 0.3, f64::from(width) * 0.7];
1297 let prev = cc.get(&id);
1298 let alloc = round_layout_stable(&targets, width, prev);
1299 let (d_sum, d_max) = cc.displacement(&id, &alloc);
1300 cc.store(id, alloc.clone());
1301 log.push((width, alloc, d_sum, d_max));
1302 }
1303 log
1304 };
1305
1306 let log1 = sweep(0);
1307 let log2 = sweep(0);
1308 assert_eq!(log1, log2, "Identical sweeps should produce identical logs");
1309 }
1310
1311 #[test]
1312 fn default_coherence_cache_capacity_is_64() {
1313 let cc = CoherenceCache::default();
1314 assert_eq!(cc.max_entries, 64);
1315 }
1316
1317 fn s3_fifo_test_key(x: u16, w: u16) -> LayoutCacheKey {
1320 LayoutCacheKey {
1321 area_x: x,
1322 area_y: 0,
1323 area_width: w,
1324 area_height: 24,
1325 constraints_hash: 42,
1326 direction: Direction::Horizontal,
1327 intrinsics_hash: None,
1328 }
1329 }
1330
1331 #[test]
1332 fn s3fifo_layout_new_is_empty() {
1333 let cache = S3FifoLayoutCache::new(64);
1334 assert!(cache.is_empty());
1335 assert_eq!(cache.len(), 0);
1336 assert_eq!(cache.capacity(), 64);
1337 }
1338
1339 #[test]
1340 fn s3fifo_layout_default_capacity() {
1341 let cache = S3FifoLayoutCache::default();
1342 assert_eq!(cache.capacity(), 64);
1343 }
1344
1345 #[test]
1346 fn s3fifo_layout_get_or_compute_caches() {
1347 let mut cache = S3FifoLayoutCache::new(64);
1348 let key = s3_fifo_test_key(0, 80);
1349 let rects1 = cache.get_or_compute(key, || vec![Rect::new(0, 0, 40, 24)]);
1350 let rects2 = cache.get_or_compute(key, || panic!("should not recompute"));
1351 assert_eq!(rects1, rects2);
1352 let stats = cache.stats();
1353 assert_eq!(stats.hits, 1);
1354 assert_eq!(stats.misses, 1);
1355 }
1356
1357 #[test]
1358 fn s3fifo_layout_generation_invalidation() {
1359 let mut cache = S3FifoLayoutCache::new(64);
1360 let key = s3_fifo_test_key(0, 80);
1361 cache.get_or_compute(key, || vec![Rect::new(0, 0, 40, 24)]);
1362
1363 cache.invalidate_all();
1364
1365 let rects = cache.get_or_compute(key, || vec![Rect::new(0, 0, 80, 24)]);
1367 assert_eq!(rects, vec![Rect::new(0, 0, 80, 24)]);
1368 let stats = cache.stats();
1369 assert_eq!(stats.misses, 2);
1370 }
1371
1372 #[test]
1373 fn s3fifo_layout_clear() {
1374 let mut cache = S3FifoLayoutCache::new(64);
1375 let key = s3_fifo_test_key(0, 80);
1376 cache.get_or_compute(key, || vec![Rect::new(0, 0, 40, 24)]);
1377 cache.clear();
1378 assert!(cache.is_empty());
1379 }
1380
1381 #[test]
1382 fn s3fifo_layout_different_keys() {
1383 let mut cache = S3FifoLayoutCache::new(64);
1384 let k1 = s3_fifo_test_key(0, 80);
1385 let k2 = s3_fifo_test_key(0, 120);
1386 cache.get_or_compute(k1, || vec![Rect::new(0, 0, 40, 24)]);
1387 cache.get_or_compute(k2, || vec![Rect::new(0, 0, 60, 24)]);
1388 assert_eq!(cache.len(), 2);
1389 }
1390
1391 #[test]
1392 fn s3fifo_layout_reset_stats() {
1393 let mut cache = S3FifoLayoutCache::new(64);
1394 let key = s3_fifo_test_key(0, 80);
1395 cache.get_or_compute(key, Vec::new);
1396 cache.get_or_compute(key, Vec::new);
1397 cache.reset_stats();
1398 let stats = cache.stats();
1399 assert_eq!(stats.hits, 0);
1400 assert_eq!(stats.misses, 0);
1401 }
1402
1403 #[test]
1404 fn s3fifo_layout_produces_same_results_as_lru() {
1405 let mut lru = LayoutCache::new(64);
1406 let mut s3 = S3FifoLayoutCache::new(64);
1407
1408 for w in [80, 100, 120, 160, 200] {
1409 let key = s3_fifo_test_key(0, w);
1410 let expected = vec![Rect::new(0, 0, w / 2, 24)];
1411 let lru_r = lru.get_or_compute(key, || expected.clone());
1412 let s3_r = s3.get_or_compute(key, || expected.clone());
1413 assert_eq!(lru_r, s3_r, "mismatch for width={w}");
1414 }
1415 }
1416}