Files
iris/tests/allocation_cost.rs
T
iris-ai 97fca76108 Answer "have I asked this child?" in one read
A container's draw asked it once per child by searching the list of children
it had added so far, and four other per-child steps searched a list too, so
one draw cost the square of its children: 70% of a 1,600-child redraw was
those searches. A draw takes a DrawId and leaves it on every widget it asks
about; one note per widget is enough because the handle a container holds a
child by cannot be cloned. tests/children_cost.rs is the rig that shows it,
and it is the only one here that varies width: 3.680 ms to 0.811 ms at 1,600
children, and flat per child.

Beside it, the rest of the fourteenth sweep of #19: a mask's rectangle
resolved once per fragment instead of once per instance, which takes the
storage buffers out of the fragment stage and is 8.8x on a screenful of
deeply nested clips; TextBuffer::shape copying its attrs before the check
that would not need them, which allocated once per named-family text per
frame; a should_panic test on a debug assertion that made cargo test
--release fail; Fixed::div, reached only by its own test; Moves::remove
re-uploading an array it cannot have changed; and two comments the branch
itself falsified.

docs/LAYOUT_LOG.md has all eight with their measurements, the five things
looked at and left, and what was verified.
2026-09-20 23:56:12 -04:00

125 lines
4.0 KiB
Rust

use iris::{harness::Harness, prelude::*};
use std::{
alloc::{GlobalAlloc, Layout, System},
cell::Cell,
};
struct Counting;
thread_local! {
static COUNT: Cell<Option<usize>> = 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<StrongWidget> = 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);
}