Delete OrthoSize, and run the seeds in parallel

A span is as long across itself as its longest child, unless a rule beside it
already says how long it is -- and then reading the children answers nothing
and only makes its size depend on theirs. `OrthoSize::Full` was that second
case written twice, once as an enum on the span and once as the rule that
actually decides; `Painter::ruled` lets the span ask which it is in. The
widget under a rule still does not learn what the rule says, only that its
answer for that axis is not wanted.

The fuzzers grow, lay out and drop a tree within one seed, so the seeds share
nothing and take a thread each, one short of every core. Measured here: the
generated oracle's hundred seeds went from 68 s to 10 s, and a shrinker case
at 300 seeds from 18 s to 3.5 s. A seed that fails still shrinks and panics
on its own thread, and `std::thread::scope` carries that out.

The shrinker now allows the two steps the oracle already did -- the deeper
trees these grow reach a second composition, and a step is a thousandth of a
pixel.

Checked: fmt, clippy, 102 tests, all five shrinker cases at 300 seeds, 100
generated seeds, and five examples byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 03:18:49 -04:00
1 parent cb955f1023
commit 9d8415d65f
8 files changed
+94 -74

No files matched your search

+5 -14
View File
@@ -21,27 +21,21 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
}
#[test]
fn a_full_ortho_span_reports_relative_full() {
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200));
let child = rect(Color::RED).height(40).add(&mut h.rsc);
let span = (child,)
.span(Dir::RIGHT)
.ortho(OrthoSize::Full)
.add(&mut h.rsc);
let span = (child,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, Len::rel(1.0));
}
#[test]
fn a_children_ortho_span_reports_its_tallest_fixed_child() {
fn a_span_reports_its_tallest_fixed_child() {
let mut h = Harness::new((400, 200));
let short = rect(Color::RED).height(40).add(&mut h.rsc);
let tall = rect(Color::BLUE).height(70).add(&mut h.rsc);
let span = (short, tall)
.span(Dir::RIGHT)
.ortho(OrthoSize::Children)
.add(&mut h.rsc);
let span = (short, tall).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(span);
assert_eq!(h.render.active[&span.id()].size.y, Len::px(70.0));
@@ -211,10 +205,7 @@ fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
let filler = rect(Color::GREEN).add(&mut h.rsc);
// This column is an item in a row, so it takes the width left for it
// rather than asking for a full row-width in addition to the bar.
let column = (row, filler)
.span(Dir::DOWN)
.ortho(OrthoSize::Children)
.add(&mut h.rsc);
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30));
+2 -5
View File
@@ -300,13 +300,10 @@ fn a_resize_does_not_redraw_what_the_shader_can_move() {
}
#[test]
fn a_full_ortho_span_moves_its_child_without_redrawing_it() {
fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
let span = (leaf,)
.span(Dir::RIGHT)
.ortho(OrthoSize::Full)
.add(&mut h.rsc);
let span = (leaf,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
h.set_root(span);
let settled = draws.get();
-5
View File
@@ -160,7 +160,6 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
children,
dir: Dir::RIGHT,
gap: Px::ZERO,
ortho: OrthoSize::Children,
}
.add(&mut h.rsc);
let span_handle = span;
@@ -214,7 +213,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
children: inner_children,
dir: Dir::RIGHT,
gap: Px::ZERO,
ortho: OrthoSize::Children,
}
.add(&mut h.rsc);
let block = rect(Color::RED).add(&mut h.rsc);
@@ -228,7 +226,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
children: outer_children,
dir: Dir::RIGHT,
gap: Px::ZERO,
ortho: OrthoSize::Children,
}
.add(&mut h.rsc);
// Carried no rule even before rules were a property: it is here to be a
@@ -337,7 +334,6 @@ fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
children: pair,
dir: Dir::DOWN,
gap: Px::ZERO,
ortho: OrthoSize::Children,
}
.add(&mut h.rsc);
// Takes the whole box on its own, so the span above has nothing left to
@@ -357,7 +353,6 @@ fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
children: inner_children,
dir: Dir::DOWN,
gap: Px::ZERO,
ortho: OrthoSize::Children,
}
.add(&mut h.rsc);
h.rsc
+21 -4
View File
@@ -216,10 +216,9 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
if let Some(w) = any.downcast_ref::<Span>() {
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
return format!(
"Span{{dir:{:?}{sign},gap:{},ortho:{:?},n:{}}}",
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
w.dir.axis,
w.gap,
w.ortho,
w.children.len()
);
}
@@ -555,7 +554,7 @@ fn a_long_run_of_seeds_agrees() {
.and_then(|seed| seed.parse().ok())
.map(|seed| seed..=seed)
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
for seed in seeds {
over_seeds(seeds.collect(), |seed| {
changed_size(seed);
changed_every_size(seed);
repainted_together(seed);
@@ -564,5 +563,23 @@ fn a_long_run_of_seeds_agrees() {
for shuffle in SHUFFLES {
reshuffled(seed, shuffle);
}
}
});
}
/// Every seed on its own thread's share of them. A tree is grown, laid out
/// and dropped inside one call, so seeds share nothing, and this is most of
/// the time a run takes. A thread that fails takes the scope down with it,
/// which is the same panic libtest would have seen.
///
/// One core short of all of them, so the machine this runs on stays usable.
pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
let threads =
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
let chunk = seeds.len().div_ceil(threads).max(1);
std::thread::scope(|scope| {
for part in seeds.chunks(chunk) {
let run = &run;
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
}
});
}
+35 -11
View File
@@ -51,6 +51,10 @@ const WORDS: &[&str] = &[
const ONE_LINE: &str = "one line, overflowing whatever it is given";
const OUTER: (f32, f32) = (1920.0, 1200.0);
/// Steps of the grid two ways of reaching a box may differ by. See
/// `docs/HANDOFF.md`'s "Fixed point" in `ai-app-2` for where the last of
/// them is.
const AGREE_STEPS: i32 = 2;
const INNER: (f32, f32) = (640.0, 900.0);
#[derive(Clone, Debug, PartialEq)]
@@ -120,12 +124,15 @@ impl Node {
children,
dir: dir(*down),
gap: Px::from_f32(*gap),
ortho: match down {
true => OrthoSize::Children,
false => OrthoSize::Full,
},
}
.add(&mut h.rsc);
// A row takes the height it is given; a column is as wide
// as its widest child, which needs no rule.
if !*down {
h.rsc
.widgets_mut()
.set_size_rules(handle, None, Some(Len::rel(1.0)));
}
spans.push(handle);
handle.add_strong(&mut h.rsc)
}
@@ -515,12 +522,13 @@ fn diverges(node: &Node, case: Case) -> Option<String> {
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
// To one step of the grid. A move or a resize lands on the same
// number now; a length measured one way and composed another can
// still be a step apart.
// To a couple of steps of the grid, each a thousandth of a pixel: a
// move or a resize lands on the same number now, and a length
// measured one way against the same length composed another can
// still be a step out per composition between them.
let same = match (got, want) {
(Some(g), Some(c)) => {
let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP;
let d = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
d(g.top_left.x, c.top_left.x)
&& d(g.top_left.y, c.top_left.y)
&& d(g.bot_right.x, c.bot_right.x)
@@ -550,6 +558,22 @@ fn shrink(mut node: Node, case: Case) -> Node {
}
}
/// One thread per core but one, each taking a share of the seeds: a tree is
/// grown, laid out and dropped within a seed, so nothing is shared. A seed
/// that fails shrinks on its own thread and panics there, which brings the
/// scope down with it.
fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
let threads =
std::thread::available_parallelism().map_or(1, |n| n.get().saturating_sub(1).max(1));
let chunk = seeds.len().div_ceil(threads).max(1);
std::thread::scope(|scope| {
for part in seeds.chunks(chunk) {
let run = &run;
scope.spawn(move || part.iter().for_each(|&seed| run(seed)));
}
});
}
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
@@ -570,10 +594,10 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
_ => Case::Resize,
};
for seed in 1..=seeds {
over_seeds((1..=seeds).collect(), |seed| {
let node = grow(&mut Rng::new(seed), depth);
let Some(how) = diverges(&node, case) else {
continue;
return;
};
let small = shrink(node.clone(), case);
println!(
@@ -582,7 +606,7 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
small.size()
);
panic!("seed {seed} lays out differently warm than cold");
}
});
let sizes: Vec<usize> = (1..=seeds)
.map(|seed| grow(&mut Rng::new(seed), depth).size())
.collect();