An eighth sweep, over the part no earlier round named: the 6,300 lines of
tests, and once more over the seventh sweep's own commit, which was itself
unreviewed.
Four of the shrunk fuzz fixtures name one widget two or three times. `width`,
`sized` and `align` set a rule on the widget they are given and return its own
id -- only `pad` and `wrapper` make a new one -- so `let sized =
wrapped.width(76).add(..)` and the `let aligned = sized` beside it are three
names for one text. Each name then went into the list of ids the case compares
warm against cold, so a case that says it checks six boxes checks four, and
three doc comments quote that inflated count as the size of the tree the
shrinker reduced to. Measured: `plant` and `plant_fixed` list 6 and hold 4,
`plant_pair` lists 4 and holds 3, `plant_scrolled` lists 8 and holds 7. The
aliases are gone and the counts say what the fixtures build; each rebuilt
fixture was diffed against the old one, and both the widget slots and every
region are identical, for both settings of `swapped`.
`assert_same_regions` sits at the top of `unsettled.rs` and six tests call it.
Seven more spell its body out instead, byte for byte. They call it now, and it
is `#[track_caller]` so the panic names the case.
`tests/gpu/mod.rs` holds the adapter probe and the surface configuration that
`draw_cost` and `chain_cost` had a copy of each -- `config` identical, and the
probe identical but for the feature it asks for. The leak's justification lived
in one file with the other referring to it; it now sits on the thing it is
about. Shared through `#[path]`, the way `scenario/mod.rs` already is.
The mask a widget is clipped by was resolved in three places, two of them a
byte-identical closure. `mask_bounds` takes the slot rather than the widget,
because the third site deliberately reads the slot it saved before the frame:
that a redraw keeps the slot is what it is checking.
`Layered::_revision` was a field nothing reads, incremented to mark the widget
dirty. Two tests in the same file already do that with
`get_dyn_mut`, which is what the underscore was hiding.
`plan.rs` claimed every simplification is strictly smaller, and asserted `<=`.
Measured: 53 of one tree's 101 simplifications keep the widget count, since a
dropped alignment and a simpler leaf both do. The assertion is right and the
claim was not; the comment now gives the argument that does hold.
`generated.rs` said "Seven that have never failed" and "the nine the others
check" of a ten-seed array. The `should_panic` scroll test ended in an
`h.frame()` that cannot run, since `set_root` lays out and is where the panic
comes from. Two `drop(tree)` at the end of their own scope did nothing.
Format, clippy with and without layout-diagnostics, and the suite (131 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
f8aa0c5 across all 34,490 boxes. No library code changed, so the seed scans
have nothing to find. Both GPU rigs were rebuilt and run: chain cost +470% at
depth 64, draw cost ~4.4 us per layer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
279 lines
9.6 KiB
Rust
279 lines
9.6 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.
|
|
|
|
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);
|
|
|
|
/// 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();
|
|
|
|
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::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 warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
|
let mut harness = Harness::new(OUTPUT);
|
|
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
|
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) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
|
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, _| {
|
|
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| {
|
|
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));
|
|
});
|
|
}
|
|
}
|