Retain request dependencies only when discovery supplies the answer
This commit is contained in:
1 parent
8780b40bb7
commit
0e838e9dd1
7 files changed
+183
-50
No files matched your search
+38
-18
@@ -87,29 +87,37 @@ impl<'a> Painter<'a> {
|
||||
child: &StrongWidget<W>,
|
||||
axis: Axis,
|
||||
) -> Option<RequestedLen> {
|
||||
self.request_deps.push(child.id());
|
||||
if self.rsc.widgets().size_rules(child.id())[axis].bound() == crate::Bound::ANY
|
||||
&& let Some(len) = self.size_hint(child, axis)
|
||||
{
|
||||
self.request_deps.push(child.id());
|
||||
return Some(len.into());
|
||||
}
|
||||
let rel_base = self.rel_base(axis);
|
||||
let start = self.request_deps.len();
|
||||
let mut requests = SizeRequests {
|
||||
arena: &mut self.state.requests,
|
||||
measured: None,
|
||||
widgets: self.rsc.widgets(),
|
||||
dependencies: &mut self.request_deps,
|
||||
rel_base,
|
||||
rel_base: self.rel_base[axis],
|
||||
};
|
||||
let request = requests.widget(child, axis)?;
|
||||
// An intrinsic fixed answer still draws in the offered room and is
|
||||
// moved afterwards. Only a declaration or a share chooses its ask box.
|
||||
(request.has_leftover()
|
||||
|| matches!(
|
||||
self.rsc.widgets().size_rules(child.id())[axis],
|
||||
crate::SizeRule::Request(_)
|
||||
))
|
||||
.then_some(request)
|
||||
// Intrinsic fixed content must keep its offered box for wrapping;
|
||||
// only a declaration or a share chooses the box it is drawn in.
|
||||
let request = requests.widget(child, axis).filter(|request| {
|
||||
request.has_leftover()
|
||||
|| matches!(
|
||||
self.rsc.widgets().size_rules(child.id())[axis],
|
||||
crate::SizeRule::Request(_)
|
||||
)
|
||||
});
|
||||
if request.is_some() {
|
||||
self.rel_base(axis);
|
||||
} else {
|
||||
// A discarded request contributes no dependency: the measured
|
||||
// draw below records the size and box it actually used instead.
|
||||
self.request_deps.truncate(start);
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
/// Completes discovery after a child was measured. Only this call may use
|
||||
@@ -120,20 +128,32 @@ impl<'a> Painter<'a> {
|
||||
axis: Axis,
|
||||
len: LayoutLen,
|
||||
) -> RequestedLen {
|
||||
let rel_base = self.rel_base(axis);
|
||||
let start = self.request_deps.len();
|
||||
let bound = self.rsc.widgets().size_rules(child.id())[axis].bound();
|
||||
let mut requests = SizeRequests {
|
||||
arena: &mut self.state.requests,
|
||||
measured: Some(&self.state.active),
|
||||
widgets: self.rsc.widgets(),
|
||||
dependencies: &mut self.request_deps,
|
||||
rel_base,
|
||||
rel_base: self.rel_base[axis],
|
||||
};
|
||||
match requests.widget(child, axis) {
|
||||
Some(request) if request.linear().is_none() && request.has_leftover() => request,
|
||||
_ if len.leftover > Weight::ZERO => requests.bounded(len.into(), bound),
|
||||
_ => len.into(),
|
||||
if let Some(request) = requests.widget(child, axis)
|
||||
&& request.linear().is_none()
|
||||
&& request.has_leftover()
|
||||
{
|
||||
self.rel_base(axis);
|
||||
return request;
|
||||
}
|
||||
let request = if len.leftover > Weight::ZERO {
|
||||
requests.bounded(len.into(), bound)
|
||||
} else {
|
||||
len.into()
|
||||
};
|
||||
self.request_deps.truncate(start);
|
||||
if len.leftover > Weight::ZERO && bound != crate::Bound::ANY {
|
||||
self.rel_base(axis);
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
/// A deferred comparison reads this window when the allocation is solved.
|
||||
|
||||
@@ -429,7 +429,7 @@ impl UiRenderState {
|
||||
// rel base is the answer wherever the ask declared a length: it was
|
||||
// resolved into the rel base when the widget was asked, and resolving
|
||||
// it again here would take the fraction of a fraction.
|
||||
let rules = rsc.widgets().size_rules(id);
|
||||
let rules = rsc.widgets().size_rules(id).clone();
|
||||
let ruled = |axis: Axis, reported: LayoutLen| {
|
||||
if matches!(rules[axis], crate::SizeRule::Request(_)) {
|
||||
return info.rel_base[axis].into();
|
||||
|
||||
@@ -128,8 +128,8 @@ impl Widgets {
|
||||
}
|
||||
|
||||
/// The length rules whoever draws this widget applies to its box.
|
||||
pub fn size_rules(&self, id: impl IdLike) -> SizeRules {
|
||||
self.data(id).unwrap().size.clone()
|
||||
pub fn size_rules(&self, id: impl IdLike) -> &SizeRules {
|
||||
&self.data(id).unwrap().size
|
||||
}
|
||||
|
||||
/// Sets one axis's rule. The widget is marked rather than its parent
|
||||
|
||||
+22
-12
@@ -92,18 +92,28 @@ fn request_edits_in_a_nested_child_reach_the_allocator() {
|
||||
|
||||
#[test]
|
||||
fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = rect(Color::RED).add(&mut h.rsc);
|
||||
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((a, b).span(Dir::RIGHT));
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(a, Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||
h.frame();
|
||||
assert_corners!(h, a, (0, 0), (80, 100));
|
||||
assert_corners!(h, b, (80, 0), (400, 100));
|
||||
for hinted in [true, false] {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let a = if hinted {
|
||||
rect(Color::RED).add_strong(&mut h.rsc).any()
|
||||
} else {
|
||||
h.rsc.widgets_mut().add_strong(Unhinted).any()
|
||||
};
|
||||
let id = a.id();
|
||||
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||
let mut row = Span::empty(Dir::RIGHT);
|
||||
row.push(a);
|
||||
row.push(b.add_strong(&mut h.rsc));
|
||||
h.set_root(row);
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(id, Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||
h.frame();
|
||||
assert_corners!(h, id, (0, 0), (80, 100));
|
||||
assert_corners!(h, b, (80, 0), (400, 100));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
//!
|
||||
//! `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_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, grow};
|
||||
use iris::random::{Edits, Tree, build, plan};
|
||||
use std::time::Instant;
|
||||
|
||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||
@@ -125,9 +126,25 @@ fn rig_edits() -> Edits {
|
||||
}
|
||||
}
|
||||
|
||||
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) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
||||
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||
harness.state.root = Some(root);
|
||||
harness.frame();
|
||||
println!(
|
||||
@@ -209,7 +226,7 @@ fn layout_cost() {
|
||||
|
||||
if selected("cold") {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
||||
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||
harness.state.root = Some(root);
|
||||
println!(
|
||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||
@@ -276,3 +293,19 @@ fn layout_cost() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
+82
-12
@@ -1,19 +1,22 @@
|
||||
//! What a resize frame costs and what it holds, on a tree the revision before
|
||||
//! #16 also builds.
|
||||
//!
|
||||
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
|
||||
//! the same source can be dropped into an old worktree and measured there:
|
||||
//! that is the only like-for-like comparison with the code the retained
|
||||
//! layout replaced. The random tree cannot carry one, because the generator
|
||||
//! itself changed with the work.
|
||||
//! Text-layout workloads with stable paragraphs for comparisons across revisions.
|
||||
//! PR #19's base uses the older spelling of the fixed 40-pixel width and has
|
||||
//! no diagnostics. The random generator changed with layout, so it cannot
|
||||
//! provide the same workload across the full PR.
|
||||
//!
|
||||
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
||||
//! -- --ignored --nocapture resize_cost
|
||||
//! PHASE=edit ROWS=40 FRAMES=2000 cargo test --release --test revision_cost \
|
||||
//! -- --ignored --nocapture text_updates_cost
|
||||
//! ROWS=2000 cargo test --release --test revision_cost \
|
||||
//! -- --ignored --nocapture text_memory
|
||||
//!
|
||||
//! Wall time on this machine varies with CPU frequency; take the number from
|
||||
//! `perf stat -e instructions:u` on the test binary directly.
|
||||
//! `text_updates_cost` selects idle, repaint, edit, or scroll with `PHASE`.
|
||||
//! It alternates a short suffix for edits so later frames do not get a longer
|
||||
//! paragraph than earlier ones. These are CPU fixtures, with no GPU submission.
|
||||
//!
|
||||
//! Use repeated `perf stat -e instructions:u` runs on the executable directly;
|
||||
//! process totals include font loading and the cold frame, so compare identical
|
||||
//! row and frame counts. Wall time on this machine is not a stable comparison.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
@@ -137,9 +140,10 @@ fn resize_cost() {
|
||||
println!("paragraph {at}: {:?}", h.region(id));
|
||||
}
|
||||
|
||||
// Two widths in turn is the friendly case for anything that remembers an
|
||||
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
|
||||
// The sweep cycles 256 widths, avoiding the two-width cache-friendly case.
|
||||
let sweep = env("SWEEP", 0_usize) != 0;
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
let mut elapsed = Vec::with_capacity(frames);
|
||||
for frame in 0..frames {
|
||||
let narrower = match sweep {
|
||||
@@ -151,6 +155,11 @@ fn resize_cost() {
|
||||
h.frame();
|
||||
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
print!(
|
||||
"{}",
|
||||
iris::core::layout_diagnostics::take().per_frame(frames)
|
||||
);
|
||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
println!(
|
||||
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
||||
@@ -203,3 +212,64 @@ fn text_memory() {
|
||||
}
|
||||
report("after settling");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn text_updates_cost() {
|
||||
let rows = env("ROWS", 40_usize);
|
||||
let frames = env("FRAMES", 1000_usize);
|
||||
let phase = env("PHASE", String::from("edit"));
|
||||
assert!(rows > 0 && frames > 0);
|
||||
assert!(["idle", "repaint", "edit", "scroll"].contains(&phase.as_str()));
|
||||
let mut h = Harness::new(OUTPUT);
|
||||
let mut rng = Rng(1);
|
||||
let mut col = Span::empty(Dir::DOWN);
|
||||
let first = wtext(words(&mut rng, 12, 52))
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.add(&mut h.rsc);
|
||||
col.push(first.add_strong(&mut h.rsc));
|
||||
for _ in 1..rows {
|
||||
col.push(
|
||||
wtext(words(&mut rng, 12, 52))
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.add_strong(&mut h.rsc),
|
||||
);
|
||||
}
|
||||
let root = col.scrollable().add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
let original = h.rsc[first].content.to_string();
|
||||
let alternate = format!("{original} another word");
|
||||
let start = Instant::now();
|
||||
for frame in 0..frames {
|
||||
match phase.as_str() {
|
||||
"idle" => {}
|
||||
"repaint" => {
|
||||
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
||||
}
|
||||
"edit" => {
|
||||
h.rsc[first].content.clear();
|
||||
h.rsc[first].content.push_str(if frame % 2 == 0 {
|
||||
&alternate
|
||||
} else {
|
||||
&original
|
||||
});
|
||||
}
|
||||
"scroll" => h.rsc[root].scroll(if frame % 2 == 0 { -12.0 } else { 12.0 }),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
h.frame();
|
||||
}
|
||||
println!(
|
||||
"{phase}: {rows} rows, {frames} frames, {:.1} ms",
|
||||
start.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
print!(
|
||||
"{}",
|
||||
iris::core::layout_diagnostics::take().per_frame(frames)
|
||||
);
|
||||
}
|
||||
@@ -356,7 +356,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
|
||||
/// out by hand. A failure is a lead; the fast test that replaces it has to be
|
||||
/// buildable from what the failure printed.
|
||||
fn describe(id: WidgetId, h: &Harness) -> String {
|
||||
let rules = h.rsc.widgets().size_rules(id);
|
||||
let rules = h.rsc.widgets().size_rules(id).clone();
|
||||
// A bound prints as itself: a failure is reproduced from what it printed,
|
||||
// and a rule shown as "no rule" cannot be written out again.
|
||||
let rule = |r: SizeRule| match r {
|
||||
|
||||
Reference in new issue
Block a user