//! Retained CPU-layout diagnostics on one reproducible random tree. //! //! Counters and phase timers: //! //! cargo test --release --features layout-diagnostics \ //! --test layout_diagnostics -- --ignored --nocapture //! //! Uninstrumented hardware totals for one phase: //! //! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \ //! -e cycles:u,instructions:u cargo test --release \ //! --test 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. use iris::harness::Harness; use iris::prelude::*; use iris::random::{Edits, Tree, grow}; use std::time::Instant; const OUTPUT: (f32, f32) = (1920.0, 1200.0); #[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).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(); let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id()); let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id()); harness.frame(); let report = diagnostics::take(); assert!( report .traces() .iter() .any(|event| matches!(event, TraceEvent::Placed { 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(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::() .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) {} fn warm(seed: u64, depth: usize) -> (Harness, Tree) { let mut harness = Harness::new(OUTPUT); let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); 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, _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::(), ); #[cfg(feature = "layout-diagnostics")] { let diagnostics = iris::core::layout_diagnostics::take(); print!("{}", diagnostics.per_frame(frames)); 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) = grow(&mut harness.rsc, seed, depth, &Edits::default()); 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, |_, _| {}); drop(tree); } 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, _| { let _ = harness.rsc.widgets_mut().get_dyn_mut(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().get_dyn_mut(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| { harness.rsc[sized].x = Some(Len::px(100.0 + (frame % 2) as f32 * 40.0)); }); } 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)); }); drop(tree); } }