use std::hash::{Hash, Hasher};
use ftui_core::geometry::Rect;
use rustc_hash::FxHashMap;
use crate::{Constraint, Direction, LayoutSizeHint};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct LayoutCacheKey {
pub area_x: u16,
pub area_y: u16,
pub area_width: u16,
pub area_height: u16,
pub constraints_hash: u64,
pub constraints_hash_fx: u64,
pub constraints_len: u16,
pub direction: Direction,
pub intrinsics_hash: Option<u64>,
pub intrinsics_hash_fx: Option<u64>,
}
impl LayoutCacheKey {
pub fn new(
area: Rect,
constraints: &[Constraint],
direction: Direction,
intrinsics: Option<&[LayoutSizeHint]>,
) -> Self {
let (ch1, ch2) = Self::hash_constraints(constraints);
let intr_hashes = intrinsics.map(Self::hash_intrinsics);
Self {
area_x: area.x,
area_y: area.y,
area_width: area.width,
area_height: area.height,
constraints_hash: ch1,
constraints_hash_fx: ch2,
constraints_len: constraints.len() as u16,
direction,
intrinsics_hash: intr_hashes.map(|h| h.0),
intrinsics_hash_fx: intr_hashes.map(|h| h.1),
}
}
#[inline]
pub fn area(&self) -> Rect {
Rect::new(self.area_x, self.area_y, self.area_width, self.area_height)
}
fn hash_constraints(constraints: &[Constraint]) -> (u64, u64) {
let mut h1 = std::collections::hash_map::DefaultHasher::new();
let mut h2 = rustc_hash::FxHasher::default();
for c in constraints {
std::mem::discriminant(c).hash(&mut h1);
std::mem::discriminant(c).hash(&mut h2);
match c {
Constraint::Fixed(v) => {
v.hash(&mut h1);
v.hash(&mut h2);
}
Constraint::Percentage(p) => {
p.to_bits().hash(&mut h1);
p.to_bits().hash(&mut h2);
}
Constraint::Min(v) => {
v.hash(&mut h1);
v.hash(&mut h2);
}
Constraint::Max(v) => {
v.hash(&mut h1);
v.hash(&mut h2);
}
Constraint::Ratio(n, d) => {
fn gcd(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
let divisor = gcd(*n, *d);
if let (Some(n_div), Some(d_div)) =
(n.checked_div(divisor), d.checked_div(divisor))
{
n_div.hash(&mut h1);
d_div.hash(&mut h1);
n_div.hash(&mut h2);
d_div.hash(&mut h2);
} else {
n.hash(&mut h1);
d.hash(&mut h1);
n.hash(&mut h2);
d.hash(&mut h2);
}
}
Constraint::Fill => {}
Constraint::FitContent => {}
Constraint::FitContentBounded { min, max } => {
min.hash(&mut h1);
max.hash(&mut h1);
min.hash(&mut h2);
max.hash(&mut h2);
}
Constraint::FitMin => {}
}
}
(h1.finish(), h2.finish())
}
fn hash_intrinsics(intrinsics: &[LayoutSizeHint]) -> (u64, u64) {
let mut h1 = std::collections::hash_map::DefaultHasher::new();
let mut h2 = rustc_hash::FxHasher::default();
for hint in intrinsics {
hint.min.hash(&mut h1);
hint.preferred.hash(&mut h1);
hint.max.hash(&mut h1);
hint.min.hash(&mut h2);
hint.preferred.hash(&mut h2);
hint.max.hash(&mut h2);
}
(h1.finish(), h2.finish())
}
}
#[derive(Clone, Debug)]
struct CachedLayoutEntry {
chunks: crate::Rects,
generation: u64,
access_count: u32,
}
#[derive(Debug, Clone, Default)]
pub struct LayoutCacheStats {
pub entries: usize,
pub hits: u64,
pub misses: u64,
pub hit_rate: f64,
}
#[derive(Debug)]
pub struct LayoutCache {
entries: FxHashMap<LayoutCacheKey, CachedLayoutEntry>,
generation: u64,
max_entries: usize,
hits: u64,
misses: u64,
}
impl LayoutCache {
#[inline]
pub fn new(max_entries: usize) -> Self {
Self {
entries: FxHashMap::with_capacity_and_hasher(max_entries, Default::default()),
generation: 0,
max_entries,
hits: 0,
misses: 0,
}
}
pub fn get_or_compute<F>(&mut self, key: LayoutCacheKey, compute: F) -> crate::Rects
where
F: FnOnce() -> crate::Rects,
{
if let Some(entry) = self.entries.get_mut(&key)
&& entry.generation == self.generation
{
self.hits += 1;
entry.access_count = entry.access_count.saturating_add(1);
return entry.chunks.clone();
}
self.misses += 1;
let chunks = compute();
if self.entries.len() >= self.max_entries {
self.evict_lru();
}
self.entries.insert(
key,
CachedLayoutEntry {
chunks: chunks.clone(),
generation: self.generation,
access_count: 1,
},
);
chunks
}
#[inline]
pub fn invalidate_all(&mut self) {
self.generation = self.generation.wrapping_add(1);
}
pub fn stats(&self) -> LayoutCacheStats {
let total = self.hits + self.misses;
LayoutCacheStats {
entries: self.entries.len(),
hits: self.hits,
misses: self.misses,
hit_rate: if total > 0 {
self.hits as f64 / total as f64
} else {
0.0
},
}
}
#[inline]
pub fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
#[inline]
pub fn clear(&mut self) {
self.entries.clear();
self.generation = self.generation.wrapping_add(1);
}
#[inline]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[inline]
pub fn capacity(&self) -> usize {
self.max_entries
}
fn evict_lru(&mut self) {
if let Some(key) = self
.entries
.iter()
.min_by_key(|(_, e)| e.access_count)
.map(|(k, _)| *k)
{
self.entries.remove(&key);
}
}
}
impl Default for LayoutCache {
fn default() -> Self {
Self::new(64)
}
}
#[derive(Debug)]
pub struct S3FifoLayoutCache {
cache: ftui_core::s3_fifo::S3Fifo<LayoutCacheKey, CachedLayoutEntry>,
generation: u64,
max_entries: usize,
hits: u64,
misses: u64,
}
impl S3FifoLayoutCache {
#[inline]
pub fn new(max_entries: usize) -> Self {
Self {
cache: ftui_core::s3_fifo::S3Fifo::new(max_entries.max(2)),
generation: 0,
max_entries: max_entries.max(2),
hits: 0,
misses: 0,
}
}
pub fn get_or_compute<F>(&mut self, key: LayoutCacheKey, compute: F) -> crate::Rects
where
F: FnOnce() -> crate::Rects,
{
if let Some(entry) = self.cache.get(&key) {
if entry.generation == self.generation {
self.hits += 1;
return entry.chunks.clone();
}
self.cache.remove(&key);
}
self.misses += 1;
let chunks = compute();
self.cache.insert(
key,
CachedLayoutEntry {
chunks: chunks.clone(),
generation: self.generation,
access_count: 1,
},
);
chunks
}
#[inline]
pub fn invalidate_all(&mut self) {
self.generation = self.generation.wrapping_add(1);
}
pub fn stats(&self) -> LayoutCacheStats {
let total = self.hits + self.misses;
LayoutCacheStats {
entries: self.cache.len(),
hits: self.hits,
misses: self.misses,
hit_rate: if total > 0 {
self.hits as f64 / total as f64
} else {
0.0
},
}
}
#[inline]
pub fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
#[inline]
pub fn clear(&mut self) {
self.cache.clear();
self.generation = self.generation.wrapping_add(1);
}
#[inline]
pub fn len(&self) -> usize {
self.cache.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
#[inline]
pub fn capacity(&self) -> usize {
self.max_entries
}
}
impl Default for S3FifoLayoutCache {
fn default() -> Self {
Self::new(64)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct CoherenceId {
pub constraints_hash: u64,
pub direction: Direction,
}
impl CoherenceId {
pub fn new(constraints: &[Constraint], direction: Direction) -> Self {
Self {
constraints_hash: LayoutCacheKey::hash_constraints(constraints).0,
direction,
}
}
pub fn from_cache_key(key: &LayoutCacheKey) -> Self {
Self {
constraints_hash: key.constraints_hash,
direction: key.direction,
}
}
}
#[derive(Debug, Clone)]
pub struct CoherenceCache {
entries: FxHashMap<CoherenceId, CoherenceEntry>,
max_entries: usize,
tick: u64,
}
#[derive(Debug, Clone)]
struct CoherenceEntry {
allocation: crate::Sizes,
last_stored: u64,
}
impl CoherenceCache {
pub fn new(max_entries: usize) -> Self {
Self {
entries: FxHashMap::with_capacity_and_hasher(max_entries.min(256), Default::default()),
max_entries,
tick: 0,
}
}
#[inline]
pub fn get(&self, id: &CoherenceId) -> Option<crate::Sizes> {
self.entries.get(id).map(|e| e.allocation.clone())
}
pub fn store(&mut self, id: CoherenceId, allocation: crate::Sizes) {
self.tick = self.tick.wrapping_add(1);
if self.entries.len() >= self.max_entries && !self.entries.contains_key(&id) {
self.evict_oldest();
}
self.entries.insert(
id,
CoherenceEntry {
allocation,
last_stored: self.tick,
},
);
}
#[inline]
pub fn clear(&mut self) {
self.entries.clear();
}
#[inline]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn displacement(&self, id: &CoherenceId, new_alloc: &[u16]) -> (u64, u32) {
match self.entries.get(id) {
Some(entry) => {
let prev = &entry.allocation;
let len = prev.len().min(new_alloc.len());
let mut sum: u64 = 0;
let mut max: u32 = 0;
for i in 0..len {
let d = (new_alloc[i] as i32 - prev[i] as i32).unsigned_abs();
sum += u64::from(d);
max = max.max(d);
}
for &v in &prev[len..] {
sum += u64::from(v);
max = max.max(u32::from(v));
}
for &v in &new_alloc[len..] {
sum += u64::from(v);
max = max.max(u32::from(v));
}
(sum, max)
}
None => (0, 0),
}
}
fn evict_oldest(&mut self) {
if let Some(key) = self
.entries
.iter()
.min_by_key(|(_, e)| e.last_stored)
.map(|(k, _)| *k)
{
self.entries.remove(&key);
}
}
}
impl Default for CoherenceCache {
fn default() -> Self {
Self::new(64)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_key(width: u16, height: u16) -> LayoutCacheKey {
LayoutCacheKey::new(
Rect::new(0, 0, width, height),
&[Constraint::Percentage(50.0), Constraint::Fill],
Direction::Horizontal,
None,
)
}
fn should_not_call(label: &str) -> crate::Rects {
unreachable!("{label}");
}
#[test]
fn same_params_produce_same_key() {
let k1 = make_key(80, 24);
let k2 = make_key(80, 24);
assert_eq!(k1, k2);
}
#[test]
fn different_area_different_key() {
let k1 = make_key(80, 24);
let k2 = make_key(120, 40);
assert_ne!(k1, k2);
}
#[test]
fn different_constraints_different_key() {
let k1 = LayoutCacheKey::new(
Rect::new(0, 0, 80, 24),
&[Constraint::Fixed(20)],
Direction::Horizontal,
None,
);
let k2 = LayoutCacheKey::new(
Rect::new(0, 0, 80, 24),
&[Constraint::Fixed(30)],
Direction::Horizontal,
None,
);
assert_ne!(k1, k2);
}
#[test]
fn different_direction_different_key() {
let k1 = LayoutCacheKey::new(
Rect::new(0, 0, 80, 24),
&[Constraint::Fill],
Direction::Horizontal,
None,
);
let k2 = LayoutCacheKey::new(
Rect::new(0, 0, 80, 24),
&[Constraint::Fill],
Direction::Vertical,
None,
);
assert_ne!(k1, k2);
}
#[test]
fn different_intrinsics_different_key() {
let hints1 = [LayoutSizeHint {
min: 10,
preferred: 20,
max: None,
}];
let hints2 = [LayoutSizeHint {
min: 10,
preferred: 30,
max: None,
}];
let k1 = LayoutCacheKey::new(
Rect::new(0, 0, 80, 24),
&[Constraint::FitContent],
Direction::Horizontal,
Some(&hints1),
);
let k2 = LayoutCacheKey::new(
Rect::new(0, 0, 80, 24),
&[Constraint::FitContent],
Direction::Horizontal,
Some(&hints2),
);
assert_ne!(k1, k2);
}
#[test]
fn cache_returns_same_result() {
let mut cache = LayoutCache::new(100);
let key = make_key(80, 24);
let mut compute_count = 0;
let compute = || {
compute_count += 1;
smallvec::smallvec![Rect::new(0, 0, 40, 24), Rect::new(40, 0, 40, 24)]
};
let r1 = cache.get_or_compute(key, compute);
let r2 = cache.get_or_compute(key, || should_not_call("should not call"));
assert_eq!(r1, r2);
assert_eq!(compute_count, 1);
}
#[test]
fn different_area_is_cache_miss() {
let mut cache = LayoutCache::new(100);
let mut compute_count = 0;
let mut compute = || {
compute_count += 1;
smallvec::smallvec![Rect::default()]
};
let k1 = make_key(80, 24);
let k2 = make_key(120, 40);
cache.get_or_compute(k1, &mut compute);
cache.get_or_compute(k2, &mut compute);
assert_eq!(compute_count, 2);
}
#[test]
fn invalidation_clears_cache() {
let mut cache = LayoutCache::new(100);
let key = make_key(80, 24);
let mut compute_count = 0;
let mut compute = || {
compute_count += 1;
smallvec::smallvec![]
};
cache.get_or_compute(key, &mut compute);
cache.invalidate_all();
cache.get_or_compute(key, &mut compute);
assert_eq!(compute_count, 2);
}
#[test]
fn lru_eviction_works() {
let mut cache = LayoutCache::new(2);
let k1 = make_key(10, 10);
let k2 = make_key(20, 20);
let k3 = make_key(30, 30);
cache.get_or_compute(k1, || smallvec::smallvec![Rect::new(0, 0, 10, 10)]);
cache.get_or_compute(k2, || smallvec::smallvec![Rect::new(0, 0, 20, 20)]);
cache.get_or_compute(k1, || should_not_call("k1 should hit"));
cache.get_or_compute(k3, || smallvec::smallvec![Rect::new(0, 0, 30, 30)]);
assert_eq!(cache.len(), 2);
let mut was_called = false;
cache.get_or_compute(k2, || {
was_called = true;
smallvec::smallvec![]
});
assert!(was_called, "k2 should have been evicted");
cache.get_or_compute(k1, || should_not_call("k1 should still be cached"));
}
#[test]
fn stats_track_hits_and_misses() {
let mut cache = LayoutCache::new(100);
let k1 = make_key(80, 24);
let k2 = make_key(120, 40);
cache.get_or_compute(k1, crate::Rects::new); cache.get_or_compute(k1, || should_not_call("hit")); cache.get_or_compute(k2, crate::Rects::new);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 2);
assert!((stats.hit_rate - 1.0 / 3.0).abs() < 0.01);
}
#[test]
fn reset_stats_clears_counters() {
let mut cache = LayoutCache::new(100);
let key = make_key(80, 24);
cache.get_or_compute(key, crate::Rects::new);
cache.get_or_compute(key, || should_not_call("hit"));
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
cache.reset_stats();
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.hit_rate, 0.0);
}
#[test]
fn clear_removes_all_entries() {
let mut cache = LayoutCache::new(100);
cache.get_or_compute(make_key(80, 24), crate::Rects::new);
cache.get_or_compute(make_key(120, 40), crate::Rects::new);
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
let mut was_called = false;
cache.get_or_compute(make_key(80, 24), || {
was_called = true;
smallvec::smallvec![]
});
assert!(was_called);
}
#[test]
fn default_capacity_is_64() {
let cache = LayoutCache::default();
assert_eq!(cache.capacity(), 64);
}
#[test]
fn generation_wraps_around() {
let mut cache = LayoutCache::new(100);
cache.generation = u64::MAX;
cache.invalidate_all();
assert_eq!(cache.generation, 0);
}
#[test]
fn constraint_hash_is_stable() {
let constraints = [
Constraint::Fixed(20),
Constraint::Percentage(50.0),
Constraint::Min(10),
];
let h1 = LayoutCacheKey::hash_constraints(&constraints);
let h2 = LayoutCacheKey::hash_constraints(&constraints);
assert_eq!(h1, h2);
}
#[test]
fn different_constraint_values_different_hash() {
let c1 = [Constraint::Fixed(20)];
let c2 = [Constraint::Fixed(30)];
let h1 = LayoutCacheKey::hash_constraints(&c1);
let h2 = LayoutCacheKey::hash_constraints(&c2);
assert_ne!(h1, h2);
}
#[test]
fn different_constraint_types_different_hash() {
let c1 = [Constraint::Fixed(20)];
let c2 = [Constraint::Min(20)];
let h1 = LayoutCacheKey::hash_constraints(&c1);
let h2 = LayoutCacheKey::hash_constraints(&c2);
assert_ne!(h1, h2);
}
#[test]
fn fit_content_bounded_values_in_hash() {
let c1 = [Constraint::FitContentBounded { min: 10, max: 50 }];
let c2 = [Constraint::FitContentBounded { min: 10, max: 60 }];
let h1 = LayoutCacheKey::hash_constraints(&c1);
let h2 = LayoutCacheKey::hash_constraints(&c2);
assert_ne!(h1, h2);
}
#[test]
fn intrinsics_hash_is_stable() {
let hints = [
LayoutSizeHint {
min: 10,
preferred: 20,
max: Some(30),
},
LayoutSizeHint {
min: 5,
preferred: 15,
max: None,
},
];
let h1 = LayoutCacheKey::hash_intrinsics(&hints);
let h2 = LayoutCacheKey::hash_intrinsics(&hints);
assert_eq!(h1, h2);
}
#[test]
fn different_intrinsics_different_hash() {
let h1 = [LayoutSizeHint {
min: 10,
preferred: 20,
max: None,
}];
let h2 = [LayoutSizeHint {
min: 10,
preferred: 25,
max: None,
}];
let hash1 = LayoutCacheKey::hash_intrinsics(&h1);
let hash2 = LayoutCacheKey::hash_intrinsics(&h2);
assert_ne!(hash1, hash2);
}
#[test]
fn cache_is_deterministic() {
let mut cache1 = LayoutCache::new(100);
let mut cache2 = LayoutCache::new(100);
for i in 0..10u16 {
let key = make_key(i * 10, i * 5);
let result = smallvec::smallvec![Rect::new(0, 0, i, i)];
cache1.get_or_compute(key, || result.clone());
cache2.get_or_compute(key, || result);
}
assert_eq!(cache1.stats().entries, cache2.stats().entries);
assert_eq!(cache1.stats().misses, cache2.stats().misses);
}
#[test]
fn hit_count_increments_on_each_access() {
let mut cache = LayoutCache::new(100);
let key = make_key(80, 24);
cache.get_or_compute(key, crate::Rects::new);
for _ in 0..5 {
cache.get_or_compute(key, || should_not_call("should hit"));
}
let stats = cache.stats();
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 5);
}
fn make_coherence_id(n: u16) -> CoherenceId {
CoherenceId::new(
&[Constraint::Fixed(n), Constraint::Fill],
Direction::Horizontal,
)
}
#[test]
fn coherence_store_and_get() {
let mut cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
assert!(cc.get(&id).is_none());
cc.store(id, smallvec::smallvec![30u16, 50u16]);
assert_eq!(cc.get(&id), Some(smallvec::smallvec![30u16, 50u16]));
}
#[test]
fn coherence_update_replaces_allocation() {
let mut cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
cc.store(id, smallvec::smallvec![30u16, 50u16]);
cc.store(id, smallvec::smallvec![31u16, 49u16]);
assert_eq!(cc.get(&id), Some(smallvec::smallvec![31u16, 49u16]));
assert_eq!(cc.len(), 1);
}
#[test]
fn coherence_different_ids_are_separate() {
let mut cc = CoherenceCache::new(64);
let id1 = make_coherence_id(1);
let id2 = make_coherence_id(2);
cc.store(id1, smallvec::smallvec![40u16, 40u16]);
cc.store(id2, smallvec::smallvec![30u16, 50u16]);
assert_eq!(cc.get(&id1), Some(smallvec::smallvec![40u16, 40u16]));
assert_eq!(cc.get(&id2), Some(smallvec::smallvec![30u16, 50u16]));
}
#[test]
fn coherence_eviction_at_capacity() {
let mut cc = CoherenceCache::new(2);
let id1 = make_coherence_id(1);
let id2 = make_coherence_id(2);
let id3 = make_coherence_id(3);
cc.store(id1, smallvec::smallvec![10u16]);
cc.store(id2, smallvec::smallvec![20u16]);
cc.store(id3, smallvec::smallvec![30u16]);
assert_eq!(cc.len(), 2);
assert!(cc.get(&id1).is_none());
assert_eq!(cc.get(&id2), Some(smallvec::smallvec![20u16]));
assert_eq!(cc.get(&id3), Some(smallvec::smallvec![30u16]));
}
#[test]
fn coherence_clear() {
let mut cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
cc.store(id, smallvec::smallvec![10u16, 20u16]);
assert_eq!(cc.len(), 1);
cc.clear();
assert!(cc.is_empty());
assert!(cc.get(&id).is_none());
}
#[test]
fn coherence_displacement_with_previous() {
let mut cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
cc.store(id, smallvec::smallvec![30u16, 50u16]);
let (sum, max) = cc.displacement(&id, &[32, 48]);
assert_eq!(sum, 4); assert_eq!(max, 2);
}
#[test]
fn coherence_displacement_without_previous() {
let cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
let (sum, max) = cc.displacement(&id, &[30, 50]);
assert_eq!(sum, 0);
assert_eq!(max, 0);
}
#[test]
fn coherence_displacement_different_lengths() {
let mut cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
cc.store(id, smallvec::smallvec![30u16, 50u16]);
let (sum, max) = cc.displacement(&id, &[30, 50, 10]);
assert_eq!(sum, 10);
assert_eq!(max, 10);
}
#[test]
fn coherence_from_cache_key() {
let key = make_key(80, 24);
let id = CoherenceId::from_cache_key(&key);
let key2 = make_key(120, 40);
let id2 = CoherenceId::from_cache_key(&key2);
assert_eq!(id, id2);
}
#[test]
fn unit_cache_reuse_unchanged_constraints_yield_identical_layout() {
use crate::round_layout_stable;
let mut cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
let targets = [26.67, 26.67, 26.66];
let total = 80;
let alloc1 = round_layout_stable(&targets, total, cc.get(&id));
cc.store(id, alloc1.clone());
let alloc2 = round_layout_stable(&targets, total, cc.get(&id));
assert_eq!(alloc1, alloc2, "Same inputs should yield identical layout");
}
#[test]
fn e2e_resize_sweep_bounded_displacement() {
use crate::round_layout_stable;
let mut cc = CoherenceCache::new(64);
let id = make_coherence_id(1);
let mut max_displacement_ever: u32 = 0;
let mut total_displacement_sum: u64 = 0;
let steps = 61;
for width in 60u16..=120 {
let third = f64::from(width) / 3.0;
let targets = [third, third, third];
let prev = cc.get(&id);
let alloc = round_layout_stable(&targets, width, prev);
let (d_sum, d_max) = cc.displacement(&id, &alloc);
total_displacement_sum += d_sum;
max_displacement_ever = max_displacement_ever.max(d_max);
cc.store(id, alloc);
}
assert!(
max_displacement_ever <= 2,
"Max single-step displacement should be <=2 cells, got {}",
max_displacement_ever
);
let avg = total_displacement_sum as f64 / steps as f64;
assert!(
avg < 3.0,
"Average displacement per step should be <3 cells, got {:.2}",
avg
);
}
#[test]
fn e2e_resize_sweep_deterministic() {
use crate::round_layout_stable;
let sweep = |seed: u16| -> Vec<(u16, crate::Sizes, u64, u32)> {
let mut cc = CoherenceCache::new(64);
let id = CoherenceId::new(
&[Constraint::Percentage(30.0), Constraint::Fill],
Direction::Horizontal,
);
let mut log = Vec::new();
for width in (40 + seed)..(100 + seed) {
let targets = [f64::from(width) * 0.3, f64::from(width) * 0.7];
let prev = cc.get(&id);
let alloc = round_layout_stable(&targets, width, prev);
let (d_sum, d_max) = cc.displacement(&id, &alloc);
cc.store(id, alloc.clone());
log.push((width, alloc, d_sum, d_max));
}
log
};
let log1 = sweep(0);
let log2 = sweep(0);
assert_eq!(log1, log2, "Identical sweeps should produce identical logs");
}
#[test]
fn default_coherence_cache_capacity_is_64() {
let cc = CoherenceCache::default();
assert_eq!(cc.max_entries, 64);
}
fn s3_fifo_test_key(x: u16, w: u16) -> LayoutCacheKey {
LayoutCacheKey {
area_x: x,
area_y: 0,
area_width: w,
area_height: 24,
constraints_hash: 42,
constraints_hash_fx: 42,
constraints_len: 2,
direction: Direction::Horizontal,
intrinsics_hash: None,
intrinsics_hash_fx: None,
}
}
#[test]
fn s3fifo_layout_new_is_empty() {
let cache = S3FifoLayoutCache::new(64);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert_eq!(cache.capacity(), 64);
}
#[test]
fn s3fifo_layout_default_capacity() {
let cache = S3FifoLayoutCache::default();
assert_eq!(cache.capacity(), 64);
}
#[test]
fn s3fifo_layout_get_or_compute_caches() {
let mut cache = S3FifoLayoutCache::new(64);
let key = s3_fifo_test_key(0, 80);
let rects1 = cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
let rects2 = cache.get_or_compute(key, || panic!("should not recompute"));
assert_eq!(rects1, rects2);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[test]
fn s3fifo_layout_generation_invalidation() {
let mut cache = S3FifoLayoutCache::new(64);
let key = s3_fifo_test_key(0, 80);
cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
cache.invalidate_all();
let rects = cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 80, 24)]);
assert_eq!(rects.as_slice(), &[Rect::new(0, 0, 80, 24)]);
let stats = cache.stats();
assert_eq!(stats.misses, 2);
}
#[test]
fn s3fifo_layout_clear() {
let mut cache = S3FifoLayoutCache::new(64);
let key = s3_fifo_test_key(0, 80);
cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn s3fifo_layout_different_keys() {
let mut cache = S3FifoLayoutCache::new(64);
let k1 = s3_fifo_test_key(0, 80);
let k2 = s3_fifo_test_key(0, 120);
cache.get_or_compute(k1, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
cache.get_or_compute(k2, || smallvec::smallvec![Rect::new(0, 0, 60, 24)]);
assert_eq!(cache.len(), 2);
}
#[test]
fn s3fifo_layout_reset_stats() {
let mut cache = S3FifoLayoutCache::new(64);
let key = s3_fifo_test_key(0, 80);
cache.get_or_compute(key, crate::Rects::new);
cache.get_or_compute(key, crate::Rects::new);
cache.reset_stats();
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn s3fifo_layout_produces_same_results_as_lru() {
let mut lru = LayoutCache::new(64);
let mut s3 = S3FifoLayoutCache::new(64);
for w in [80, 100, 120, 160, 200] {
let key = s3_fifo_test_key(0, w);
let expected = smallvec::smallvec![Rect::new(0, 0, w / 2, 24)];
let lru_r = lru.get_or_compute(key, || expected.clone());
let s3_r = s3.get_or_compute(key, || expected.clone());
assert_eq!(lru_r, s3_r, "mismatch for width={w}");
}
}
}