The eleventh sweep, over the built-in bounds work in2ac0843. `Painter::size_hint` refused to answer for a bounded widget by returning above the diagnostics, so that read was neither a hint hit nor a miss and `hint_read` recorded nothing. It is a miss now, with the reason on it. The two `for axis in Axis::BOTH` loops that `draw_widget` grew, both writing `own_holds`, are one loop, and the comment about combining the ask's holds no longer sits between a comment and the code it describes. `Declared::from_axes` lost its only caller with `Widgets::declared_lens`; `Bounds::from_axes` and `SizeRule::declared` never had one. The scenario shrinker printed a rule with derived `Debug`, which is 130 characters an axis in a line that carries every ancestor, in the one function whose job is output a tree can be rebuilt from. It prints its parts again. `bounds_cost` invented three environment-reading spellings where four copies of one `env` helper already existed; there is now one, in `tests/rig`, and the four copies are gone. It also verified 128 regions inside its measured loop, which the other rigs deliberately do before theirs; that measured 0.65% of the total, and none of it is layout. The 250-window row with a 300 cap was built by two tests, and the one that still explained itself tested less; they are one. The half of `a_cap_attribute_narrows_the_widgets_box` that the wrapper's removal left without its deciding assertion is the allocator's path instead, which nothing at the root covered. Format, workspace clippy under -D warnings with and without layout-diagnostics, 206 ordinary and 210 diagnostic tests, 400 depth-5 trees warm against cold in 64.19s, and the cold dump byte-identical to2ac0843across all 34,986 boxes.
272 lines
8.3 KiB
Rust
272 lines
8.3 KiB
Rust
//! 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
|
|
//!
|
|
//! `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.
|
|
|
|
mod rig;
|
|
|
|
use iris::harness::Harness;
|
|
use iris::prelude::*;
|
|
use rig::env;
|
|
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);
|
|
|
|
/// 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));
|
|
}
|
|
|
|
// 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 {
|
|
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);
|
|
}
|
|
#[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, \
|
|
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");
|
|
}
|
|
|
|
#[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)
|
|
);
|
|
}
|