A ninth sweep, over the part no earlier round named -- the widget vocabulary and the builder methods, `Widgets`, the examples, the `util` additions and the manifests -- and once more over `77ed7a2`, the eighth sweep's own commit and so itself unreviewed. A hint overrode a rule. `declared_lens` asked `rules[axis].declared()` first and fell through to the widget's own `size_hint` whenever that answered `None` -- which it does for a share, since a share is not a declaration. So a widget carrying `width(leftover(1))` and hinting a pixel length of its own was handed a box of the hint, against the rule and against the comment inside the function: "a hint still narrows the box where no rule does". `Painter::size_hint` spells the same rule-else-hint step three hundred lines up and gets it right, with the reason written on it; both read `Widgets::exact_len` now, and `declared_lens` is the part of its answer that needs nobody to divide it. `Image` is the only widget here whose hint is a declared length, and neither the tests nor the generator builds one, so nothing in this repository could reach the difference -- which is why the dump is unchanged and why the test builds a widget of its own. It records the box it was asked in: 400 with the rule and 50 without, and 50 either way before this. Marking a widget for redraw had no name. Twenty-one sites under `tests/` said it as `widgets_mut().get_dyn_mut(id);` with the widget thrown away, five with a `let _ =` in front, one with a comment explaining what the line was for, and one wrapped in a local function called `mark`. `Widgets::mark_for_redraw` says it. `revision_cost.rs` keeps the long spelling and now says why in place: it is deliberately in the API subset an old worktree also has. `assert_same_regions` could not see the defect the eighth sweep had just fixed. It zips the warm and cold id lists, so a list naming one widget twice -- which is what `width`, `sized` and `align` giving back their own argument produces -- compares fewer boxes than it lists and says nothing about it. It now rejects a repeated id and two lists of different lengths, which also checks the nine fixtures that round left alone: all eighteen cases pass. Bare pairs where the framework has named ones. `random.rs`'s `Lens` and `Aligns` were `[Option<LayoutLen>; 2]` and `[Option<AxisAlign>; 2]`, read as `[0]`/`[1]` and zipped against a hand-written `[Axis::X, Axis::Y]`. They are `SizeRules` and `Align`; `Align` took the `Index<Axis>` every other per-axis pair on this branch has, and `RegionAlign::from` does the "an axis left out is centred" step two rigs were spelling per axis. The three sites that wrote the axis pair out say `Axis::BOTH`, which is what the rest of the layout code says. `BothAxis<T>`, `AxisT`, `XAxis` and `YAxis` -- 45 lines with a const trait, two marker types and three accessors -- have no user anywhere in the workspace. They are the mechanism `impl_axis_index!` replaced, in the file this branch took `Vec2::axis`/`axis_mut` out of. Deleted, which is a drive-by in a block the branch was already rewriting; drop it if the scope matters more. Smaller things, each in its own place: `Wrapper` arrived beside core's `WidgetWrapper`, one word for a widget that wraps a child and for a dynamic borrow guard, so the alias is gone and its two uses name `DynBorrower` -- which is what they are. `Wrapper::new`, `Wrapper::empty` and its `Default` were three names for one value, two of them unused. `Arena::get_mut` was the only `pub(crate)` among `pub` siblings on a public type. `Selector` rounded the pointer onto the pixel grid to do arithmetic on two values already there, losing the precision the platform gave it for nothing; the step between the regions is taken on the grid instead. And the two `debug` profile settings carry their reason where the next reader looks rather than only in the commit that made them, one of which was about renaming `rest`. Format, clippy with and without layout-diagnostics, and the suite (132 + 19 + 13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to `77ed7a2` across all 34,488 boxes, and all three seed scans pass: 400 at depth 5 in 63.27s, 1,000 at depth 6 in 160.45s, 2,000 at depth 4 in 302.52s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206 lines
6.1 KiB
Rust
206 lines
6.1 KiB
Rust
//! 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.
|
|
//!
|
|
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
|
//! -- --ignored --nocapture resize_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.
|
|
|
|
use iris::harness::Harness;
|
|
use iris::prelude::*;
|
|
use std::time::Instant;
|
|
|
|
/// xorshift64, so one seed is one set of paragraphs on any machine.
|
|
struct Rng(u64);
|
|
|
|
impl Rng {
|
|
fn bits(&mut self) -> u64 {
|
|
self.0 ^= self.0 << 13;
|
|
self.0 ^= self.0 >> 7;
|
|
self.0 ^= self.0 << 17;
|
|
self.0
|
|
}
|
|
|
|
fn below(&mut self, n: usize) -> usize {
|
|
(self.bits() % n as u64) as usize
|
|
}
|
|
}
|
|
|
|
const WORDS: [&str; 24] = [
|
|
"wrapping",
|
|
"shapes",
|
|
"one",
|
|
"source",
|
|
"into",
|
|
"as",
|
|
"many",
|
|
"lines",
|
|
"as",
|
|
"the",
|
|
"box",
|
|
"leaves",
|
|
"room",
|
|
"for",
|
|
"paragraph",
|
|
"height",
|
|
"answer",
|
|
"setting",
|
|
"container",
|
|
"width",
|
|
"before",
|
|
"knows",
|
|
"measured",
|
|
"again",
|
|
];
|
|
|
|
/// A run of its own words, so nothing here is fast for two texts being the
|
|
/// same string.
|
|
fn words(rng: &mut Rng, least: usize, most: usize) -> String {
|
|
let words = least + rng.below(most - least);
|
|
let mut out = String::new();
|
|
for _ in 0..words {
|
|
if !out.is_empty() {
|
|
out.push(' ');
|
|
}
|
|
out.push_str(WORDS[rng.below(WORDS.len())]);
|
|
}
|
|
out
|
|
}
|
|
|
|
const OUTPUT: (f32, f32) = (900.0, 1200.0);
|
|
|
|
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)
|
|
}
|
|
|
|
/// A row of a fixed-width rect beside a column of one wrapping and one
|
|
/// overflowing text: the shape that makes a container measure a child in a
|
|
/// box it will not keep.
|
|
fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
|
|
let mut rng = Rng(1);
|
|
let mut paragraphs = Vec::new();
|
|
let mut col = Span::empty(Dir::DOWN);
|
|
for _ in 0..rows {
|
|
let mut row = Span::empty(Dir::RIGHT);
|
|
row.push(
|
|
rect(Color::RED)
|
|
.width(LayoutLen::px(40.0))
|
|
.add_strong(&mut h.rsc),
|
|
);
|
|
let mut body = Span::empty(Dir::DOWN);
|
|
let para = wtext(words(&mut rng, 12, 52))
|
|
.size(16)
|
|
.wrap(true)
|
|
.add_strong(&mut h.rsc);
|
|
paragraphs.push(para.id());
|
|
body.push(para);
|
|
body.push(
|
|
// Short, or its unwrapped width decides the row and the
|
|
// paragraph beside it never wraps.
|
|
wtext(words(&mut rng, 2, 6))
|
|
.size(16)
|
|
.wrap(false)
|
|
.add_strong(&mut h.rsc),
|
|
);
|
|
row.push(body.add_strong(&mut h.rsc));
|
|
col.push(row.add_strong(&mut h.rsc));
|
|
}
|
|
let root = col.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
paragraphs
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "measurement, not a check"]
|
|
fn resize_cost() {
|
|
let rows = env("ROWS", 40_usize);
|
|
let frames = env("FRAMES", 500_usize);
|
|
let mut h = Harness::new(OUTPUT);
|
|
let paragraphs = build(&mut h, rows);
|
|
// What it cost is only half the comparison: the old code is cheaper
|
|
// partly because it wraps at the container's whole width rather than the
|
|
// part left beside the rect, and draws past the edge of the output.
|
|
println!("output width {}", OUTPUT.0);
|
|
for (at, id) in paragraphs.iter().enumerate().take(3) {
|
|
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.
|
|
let sweep = env("SWEEP", 0_usize) != 0;
|
|
let mut elapsed = Vec::with_capacity(frames);
|
|
for frame in 0..frames {
|
|
let narrower = match sweep {
|
|
true => (frame % 256) as f32,
|
|
false => ((frame + 1) % 2) as f32 * 8.0,
|
|
};
|
|
h.resize((OUTPUT.0 - narrower, OUTPUT.1));
|
|
let start = Instant::now();
|
|
h.frame();
|
|
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
|
|
}
|
|
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
println!(
|
|
"resize: {frames} frames, 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>()
|
|
);
|
|
}
|
|
|
|
fn kb(field: &str) -> u64 {
|
|
std::fs::read_to_string("/proc/self/status")
|
|
.unwrap()
|
|
.lines()
|
|
.find(|line| line.starts_with(field))
|
|
.and_then(|line| line.split_whitespace().nth(1)?.parse().ok())
|
|
.unwrap()
|
|
}
|
|
|
|
fn report(label: &str) {
|
|
println!(
|
|
"{label:24} rss {:>7} kB peak {:>7} kB",
|
|
kb("VmRSS:"),
|
|
kb("VmHWM:")
|
|
);
|
|
}
|
|
|
|
/// Run this one on its own: the figures are the whole process's.
|
|
#[test]
|
|
#[ignore = "measurement, not a check"]
|
|
fn text_memory() {
|
|
let rows = env("ROWS", 2000_usize);
|
|
report("before");
|
|
let mut h = Harness::new(OUTPUT);
|
|
let paragraphs = build(&mut h, rows);
|
|
report("after cold frame");
|
|
for frame in 0..40 {
|
|
h.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
|
h.frame();
|
|
}
|
|
report("after 40 resizes");
|
|
// Settled: the output holds still and one leaf repaints per frame. Marked
|
|
// by taking it mutably because the revision at the top of this file has no
|
|
// `mark_for_redraw`, and the same source has to build against both.
|
|
for _ in 0..10 {
|
|
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
|
|
h.frame();
|
|
}
|
|
report("after settling");
|
|
}
|