Files
iris/tests/layout_diagnostics.rs
T

312 lines
11 KiB
Rust

//! Retained CPU-layout diagnostics on one reproducible random tree.
//!
//! Counters and phase timers:
//!
//! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture
//!
//! Build the uninstrumented test with `cargo test --release --test
//! layout_diagnostics --no-run`, then run the emitted executable directly:
//!
//! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
//! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
//!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
//! `IRIS_DIRTY` how many widgets `many` marks at once. `IRIS_UNBOUNDED=1`
//! removes intrinsic bounds while preserving the rest of the generated tree.
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Edits, Tree, build, plan};
use std::time::Instant;
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
/// A scroll whose content fits is the same drawing in every box it still
/// fits in, so a longer or shorter one relays out nothing. Where the content
/// sits in that box is decided by placing its answer in the whole of it,
/// which is a fraction of the box and holds at every length -- so the
/// contract must not turn on the alignment. It did, and at the default
/// alignment, which is the middle, every box change redrew the scroll.
#[cfg(feature = "layout-diagnostics")]
#[test]
fn a_fitting_scroll_holds_for_every_box_its_content_fits_in() {
use iris::core::layout_diagnostics as diag;
for align in [Align::TOP_LEFT, Align::CENTER, Align::BOT_RIGHT] {
let mut harness = Harness::new((400, 200));
let inner = rect(Color::RED).height(50).add(&mut harness.rsc);
harness.set_root(inner.scrollable().align(align));
harness.frame();
let _ = diag::take();
// Still far longer than the 50 the content needs.
harness.resize((400, 180));
harness.frame();
assert_eq!(diag::take().distinct_widgets(), 0, "{align:?}");
}
}
#[cfg(feature = "layout-diagnostics")]
#[test]
fn a_selected_widget_retains_its_layout_events() {
use iris::core::layout_diagnostics::{self as diagnostics, TraceEvent};
diagnostics::clear_traced_widgets();
let _ = diagnostics::take();
let mut harness = Harness::new((400, 200));
let leaf = rect(Color::RED).region_node().add(&mut harness.rsc);
let other = rect(Color::BLUE).add(&mut harness.rsc);
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
harness.set_root(root);
diagnostics::trace_widget(leaf.id());
let _ = diagnostics::take();
harness.rsc.widgets_mut().mark_for_redraw(root.id());
harness.rsc.widgets_mut().mark_for_redraw(leaf.id());
harness.frame();
let report = diagnostics::take();
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::RegionNode { id, .. } if *id == leaf.id()))
);
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::DrawRequest { id, .. } if *id == leaf.id()))
);
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::SizeRead { id, .. } if *id == leaf.id()))
);
assert!(
report
.traces()
.iter()
.any(|event| matches!(event, TraceEvent::SizeReported { id, .. } if *id == leaf.id()))
);
diagnostics::clear_traced_widgets();
}
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
#[cfg(feature = "layout-diagnostics")]
fn trace_selected(tree: &Tree) {
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
return;
};
let index = value
.parse::<usize>()
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
let id = tree.ids[index];
iris::core::layout_diagnostics::trace_widget(id);
println!("tracing tree.ids[{index}] = {id:?}");
}
#[cfg(not(feature = "layout-diagnostics"))]
fn trace_selected(_: &Tree) {}
/// The shape a cost is measured on must not depend on what layout measured,
/// or two commits are compared on two different trees. See `Edits`.
fn rig_edits() -> Edits {
Edits {
fixed_branches: true,
..Default::default()
}
}
fn fixture(harness: &mut Harness, seed: u64, depth: usize) -> (StrongWidget, Tree) {
let mut plan = plan(seed, depth, &rig_edits());
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
plan.walk_mut(&mut |node| {
if let Some(rules) = &mut node.size {
for axis in Axis::BOTH {
if rules[axis].bound() != Bound::ANY {
rules[axis] = SizeRule::Free;
}
}
}
});
}
build(&mut harness.rsc, &plan)
}
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
let mut harness = Harness::new(OUTPUT);
let (root, tree) = fixture(&mut harness, seed, depth);
harness.state.root = Some(root);
harness.frame();
println!(
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
tree.ids.len(),
harness.render.active_widgets()
);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
(harness, tree)
}
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
let frames = elapsed.len();
// The worst frame is the stutter somebody sees, so it goes beside the
// median; p99 says whether it is the load or a single interruption.
println!(
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
max {:.3} ms, total {:.1} ms",
elapsed[0],
elapsed[frames / 2],
elapsed[frames * 99 / 100],
elapsed[frames - 1],
elapsed.iter().sum::<f64>(),
);
#[cfg(feature = "layout-diagnostics")]
{
let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames));
for event in diagnostics.traces() {
println!(" {event:?}");
}
for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new();
let mut id = Some(callsite.id);
while let Some(widget) = id {
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
id = _harness
.render
.active
.get(&widget)
.and_then(|active| active.parent);
}
println!(" text ancestry: {}", ancestry.join(" < "));
}
}
}
fn run(
label: &str,
frames: usize,
harness: &mut Harness,
mut change: impl FnMut(&mut Harness, usize),
) {
let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames {
change(harness, frame);
let start = Instant::now();
harness.frame();
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
}
report(label, elapsed, harness);
}
#[test]
#[ignore = "measurement, not a check"]
fn layout_cost() {
let seed = env("IRIS_SEED", 1_u64);
let depth = env("IRIS_DEPTH", 7_usize);
let frames = env("IRIS_FRAMES", 100_usize);
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
let phase = env("IRIS_PHASE", String::from("all"));
assert!(
["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()),
"unknown IRIS_PHASE {phase:?}"
);
let selected = |name| phase == "all" || phase == name;
if selected("cold") {
let mut harness = Harness::new(OUTPUT);
let (root, tree) = fixture(&mut harness, seed, depth);
harness.state.root = Some(root);
println!(
"fixture: seed {seed}, depth {depth}, {} widgets",
tree.ids.len()
);
trace_selected(&tree);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
run("cold", 1, &mut harness, |_, _| {});
}
if selected("repaint") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let leaf = tree.ids[0];
run("repaint", frames, &mut harness, move |harness, _| {
harness.rsc.widgets_mut().mark_for_redraw(leaf);
});
}
if selected("many") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
// Spread through the tree rather than taken from one subtree, so the
// dependency paths the frame settles overlap.
let wanted = env("IRIS_DIRTY", 32_usize).max(1);
let step = (tree.ids.len() / wanted).max(1);
let dirty: Vec<_> = tree.ids.iter().copied().step_by(step).collect();
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
run("many", frames, &mut harness, move |harness, _| {
for &id in &dirty {
harness.rsc.widgets_mut().mark_for_redraw(id);
}
});
}
if selected("size") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let sized = tree.sized[0];
run("size", frames, &mut harness, move |harness, frame| {
let len = LayoutLen::px(100.0 + (frame % 2) as f32 * 40.0);
harness
.rsc
.widgets_mut()
.set_size_rule(sized, Axis::X, SizeRule::Exact(len));
});
}
if selected("scroll") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let scroll = tree.scrolls[0];
run("scroll", frames, &mut harness, move |harness, frame| {
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
});
}
if selected("resize") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
run("resize", frames, &mut harness, |harness, frame| {
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
});
}
}
#[cfg(feature = "layout-diagnostics")]
#[test]
fn repainting_measured_text_does_not_invalidate_its_span() {
use iris::core::layout_diagnostics as diag;
let mut h = Harness::new((400, 200));
let text = wtext("a paragraph that fits").wrap(true).add(&mut h.rsc);
h.set_root((text, wtext("another paragraph")).span(Dir::DOWN));
let _ = diag::take();
h.rsc.widgets_mut().mark_for_redraw(text);
h.frame();
let report = diag::take();
assert_eq!(report.distinct_widgets(), 1);
assert_eq!(report.hot_widgets()[0].id, text.id());
}