use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use telar::{
Color, Component, LayoutStyle, LocalTree, RectStyle, Rectangle, SizeDimension, Text, TextStyle,
UiTree, new_container, reset_layout_runtime, signal,
};
thread_local! {
static ALLOCATED: Cell<usize> = const { Cell::new(0) };
static COUNTING: Cell<bool> = const { Cell::new(false) };
}
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let counting = COUNTING.try_with(Cell::get).unwrap_or(false);
if counting {
let _ = ALLOCATED.try_with(|a| a.set(a.get() + layout.size()));
}
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static ALLOC: Counting = Counting;
fn measure(mut body: impl FnMut()) -> usize {
ALLOCATED.with(|a| a.set(0));
COUNTING.with(|c| c.set(true));
body();
COUNTING.with(|c| c.set(false));
ALLOCATED.with(Cell::get)
}
fn chrome() -> (Box<dyn Component>, telar::RwSignal<i32>) {
reset_layout_runtime();
let ticks = signal(0);
let counter = Text::new(
{
let ticks = ticks;
move || format!("{}", ticks.get())
},
LayoutStyle::new(),
|| TextStyle::new(14.0, Color::rgb(0.9, 0.9, 0.9)),
)
.expect("text builds");
let mut children: Vec<Box<dyn telar::LayoutItem>> = vec![Box::new(counter)];
for _ in 0..8 {
children.push(Box::new(
Rectangle::new(LayoutStyle::new().width(40.0).height(12.0), || {
RectStyle::filled(Color::rgb(0.2, 0.2, 0.25), 2.0)
})
.expect("rectangle builds"),
));
}
let nodes: Vec<telar::NodeId> = children.iter().map(|c| c.layout_node()).collect();
let root = new_container(
LayoutStyle::new()
.flex_column()
.width(SizeDimension::Percent(1.0))
.height(SizeDimension::Percent(1.0)),
&nodes,
)
.expect("container builds");
let _ = root;
struct Chrome {
children: Vec<Box<dyn telar::LayoutItem>>,
}
impl Component for Chrome {
fn view(&self) -> telar::RenderNode {
telar::RenderNode::Group {
children: telar::NodeVec::collect(self.children.iter().map(|c| c.view())),
}
}
fn on_event(&mut self, event: &telar::Event) -> telar::EventResult {
for child in &mut self.children {
if child.on_event(event) == telar::EventResult::Handled {
return telar::EventResult::Handled;
}
}
telar::EventResult::Ignored
}
fn debug_name(&self) -> &'static str {
"Chrome"
}
}
(Box::new(Chrome { children }), ticks)
}
#[test]
fn asking_an_unchanged_tree_for_its_frame_allocates_nothing() {
let (root, _ticks) = chrome();
let tree = LocalTree::new(root);
let _ = tree.frame();
let cost = measure(|| {
let _ = tree.frame();
});
assert_eq!(
cost, 0,
"a clean tree recomposed instead of serving its cache, at {cost} bytes a frame"
);
}
const EVEN: i32 = 2424;
const ODD: i32 = 4242;
fn advance(tree: &LocalTree, ticks: &telar::RwSignal<i32>, value: i32) {
ticks.set(value);
telar::relayout_if_dirty();
let _ = tree.frame();
}
#[test]
fn a_changing_frame_costs_the_same_every_time() {
let (root, ticks) = chrome();
let tree = LocalTree::new(root);
for _ in 0..16 {
advance(&tree, &ticks, EVEN);
advance(&tree, &ticks, ODD);
}
let early = measure(|| advance(&tree, &ticks, EVEN));
for _ in 0..50 {
advance(&tree, &ticks, EVEN);
advance(&tree, &ticks, ODD);
}
let late = measure(|| advance(&tree, &ticks, EVEN));
eprintln!("cost per changed frame: {early} bytes");
assert_eq!(
early, late,
"frame cost moved from {early} to {late} bytes across a hundred frames, so something is \
allocating per frame that should have been reused"
);
}