use iris::{harness::Harness, prelude::*}; use std::{ alloc::{GlobalAlloc, Layout, System}, cell::Cell, }; struct Counting; thread_local! { static COUNT: Cell> = const { Cell::new(None) }; } fn count() { COUNT.with(|count| { if let Some(n) = count.get() { count.set(Some(n + 1)); } }); } // The wrapper preserves System's allocation and deallocation contracts; // observing calls here also counts allocations hidden inside layout helpers. unsafe impl GlobalAlloc for Counting { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { count(); unsafe { System.alloc(layout) } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe { System.dealloc(ptr, layout) } } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 { count(); unsafe { System.realloc(ptr, layout, size) } } } #[global_allocator] static ALLOCATOR: Counting = Counting; #[test] fn unchanged_tree_reuses_layout_storage() { for rule in [ SizeRule::FREE, leftover(1).clamp(20, 80).into(), SizeRule::clamp(20.into(), 80.into()), ] { let mut h = Harness::new((600, 200)); let mut children: Vec = Vec::new(); for _ in 0..8 { let a = rect(Color::RED).add(&mut h.rsc); h.rsc.widgets_mut().set_size_rule(a, Axis::X, rule.clone()); let row = (a, rect(Color::BLUE)) .span(Dir::RIGHT) .add_strong(&mut h.rsc); children.push(row); } let root = h.rsc.widgets_mut().add_strong(Span { children, dir: Dir::DOWN, gap: Px::ZERO, }); h.state.root = Some(root); h.frame(); let ids: Vec<_> = h.render.active.keys().copied().collect(); for frame in 0..8 { for &id in &ids { h.rsc.widgets_mut().mark_for_redraw(id); } h.resize((600 + frame % 2, 200)); h.frame(); } COUNT.set(Some(0)); for frame in 0..100 { for &id in &ids { h.rsc.widgets_mut().mark_for_redraw(id); } h.resize((600 + frame % 2, 200)); h.frame(); } let allocations = COUNT.replace(None).unwrap(); println!("rule={rule:?}: {allocations} allocations over 100 resize frames"); assert_eq!(allocations, 0); } } /// A text drawn again at the width it already has places no glyphs and shapes /// nothing, so the frame costs nothing at all -- which is what the shaping /// cache is for, and a copy of the attrs made to ask it undid for any text /// naming its font family. /// /// Only that case: a text drawn at a width it has not seen places its glyphs, /// and placing them allocates a list to hold them. #[test] fn redrawing_a_text_at_one_width_allocates_nothing() { let mut h = Harness::new((600, 200)); let mut col = Span::empty(Dir::DOWN); let mut texts = Vec::new(); for _ in 0..8 { let text = wtext("wrapping shapes one source into as many lines as the box leaves room for") .size(16) // Named rather than generic, because a named one is the family // that costs an allocation to copy. .family(Family::Named("sans-serif".into())) .wrap(true) .add_strong(&mut h.rsc); texts.push(text.id()); col.push(text); } let root = col.add(&mut h.rsc); h.set_root(root); let redraw = |h: &mut Harness| { for &id in &texts { h.rsc.widgets_mut().mark_for_redraw(id); } h.frame(); }; for _ in 0..8 { redraw(&mut h); } COUNT.set(Some(0)); for _ in 0..100 { redraw(&mut h); } let allocations = COUNT.replace(None).unwrap(); println!("text: {allocations} allocations over 100 redraws of 8 texts"); assert_eq!(allocations, 0); }