diff --git a/tests/generated.rs b/tests/generated.rs index 5d561d0..e572c7d 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -44,24 +44,37 @@ fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree { tree } +fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens { + let lens = [ + Some(Len::abs(20.0 + rng.below(180) as f32)), + Some(Len::abs(20.0 + rng.below(180) as f32)), + ]; + let sized = &mut h.rsc[tree.sized[idx]]; + sized.x = lens[0]; + sized.y = lens[1]; + lens +} + /// Changes a few of the declared sizes, and says which, so the cold tree can /// be grown with the same ones. fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap { let mut edits = HashMap::new(); for _ in 0..4 { let idx = rng.below(tree.sized.len()); - let lens = [ - Some(Len::abs(20.0 + rng.below(180) as f32)), - Some(Len::abs(20.0 + rng.below(180) as f32)), - ]; - edits.insert(idx, lens); - let sized = &mut h.rsc[tree.sized[idx]]; - sized.x = lens[0]; - sized.y = lens[1]; + edits.insert(idx, resize_one(h, tree, idx, rng)); } edits } +/// Every declared size at once, so every reader of a size in the tree has a +/// changed descendant in the same frame and the whole dirty set has to settle +/// together. +fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap { + (0..tree.sized.len()) + .map(|idx| (idx, resize_one(h, tree, idx, rng))) + .collect() +} + /// A way of changing what a span holds. Each is a shape worth its own case: /// taking a child out of the middle is not the same as emptying a span, and /// adding one is not the same as adding three. @@ -239,6 +252,52 @@ fn reshuffled(seed: u64, shuffle: Shuffle) { assert_same(seed, &what, (&warm, &grown), (&cold, &same)); } +fn changed_every_size(seed: u64) { + let mut warm = Harness::new((900, 1200)); + let grown = plant(&mut warm, seed, &Edits::default()); + if grown.sized.is_empty() { + return; + } + + let mut rng = Rng::new(seed ^ 0xa11); + let sizes = edit_every(&mut warm, &grown, &mut rng); + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let same = plant( + &mut cold, + seed, + &Edits { + sizes, + ..Default::default() + }, + ); + + assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same)); +} + +/// Marks a spread of widgets for redraw at once. Nothing changes, so no box +/// may either; what this exercises is the order a frame settles a dirty set +/// in, which the other cases reach one dependency path at a time. +fn repainted_together(seed: u64) { + let mut warm = Harness::new((900, 1200)); + let grown = plant(&mut warm, seed, &Edits::default()); + for &id in grown.ids.iter().step_by(5) { + warm.rsc.widgets_mut().get_dyn_mut(id); + } + assert!( + !warm.rsc.widgets().needs_redraw.is_empty(), + "seed {seed}: nothing was marked" + ); + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let same = plant(&mut cold, seed, &Edits::default()); + + let what = "many repaints at once"; + assert_same(seed, what, (&warm, &grown), (&cold, &same)); +} + fn resized(seed: u64) { let mut warm = Harness::new((1920, 1200)); let grown = plant(&mut warm, seed, &Edits::default()); @@ -280,6 +339,16 @@ fn a_changed_size_lands_where_growing_it_that_way_would() { SEEDS.into_iter().for_each(changed_size); } +#[test] +fn every_size_changing_at_once_lands_where_growing_it_that_way_would() { + SEEDS.into_iter().for_each(changed_every_size); +} + +#[test] +fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() { + SEEDS.into_iter().for_each(repainted_together); +} + #[test] fn a_resize_lands_where_starting_at_that_size_would() { SEEDS.into_iter().for_each(resized); @@ -328,6 +397,8 @@ fn a_long_run_of_seeds_agrees() { .unwrap_or(1..=100); for seed in seeds { changed_size(seed); + changed_every_size(seed); + repainted_together(seed); resized(seed); resized_then_changed(seed); for shuffle in SHUFFLES { diff --git a/tests/layout_diagnostics.rs b/tests/layout_diagnostics.rs index ac404ab..9240324 100644 --- a/tests/layout_diagnostics.rs +++ b/tests/layout_diagnostics.rs @@ -11,8 +11,9 @@ //! -e cycles:u,instructions:u cargo test --release \ //! --test layout_diagnostics -- --ignored --nocapture //! -//! `IRIS_PHASE` is `cold`, `repaint`, `size`, `scroll`, `resize`, or `all`. -//! `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load. +//! `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::*; @@ -165,7 +166,7 @@ fn layout_cost() { assert!(frames > 0, "IRIS_FRAMES must be greater than zero"); let phase = env("IRIS_PHASE", String::from("all")); assert!( - ["all", "cold", "repaint", "size", "scroll", "resize"].contains(&phase.as_str()), + ["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()), "unknown IRIS_PHASE {phase:?}" ); let selected = |name| phase == "all" || phase == name; @@ -194,6 +195,22 @@ fn layout_cost() { }); } + 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);