Merge branch 'worktree-agent-a33c31aef1fd6d868' into rustify (I3: iris::widget::List)
This commit is contained in:
commit
8adda94a7a
6 files changed
+1460
-39
No files matched your search
@@ -8,6 +8,36 @@ capability that moved. Small and trivial changes do not go here.
|
|||||||
An entry gives the date, what changed, why, and a short before/after where
|
An entry gives the date, what changed, why, and a short before/after where
|
||||||
it helps judge the change without the session that made it. Newest first.
|
it helps judge the change without the session that made it. Newest first.
|
||||||
|
|
||||||
|
## 2026-09-05: `List`, a virtualised bottom-anchored list (RUST.md's I3)
|
||||||
|
|
||||||
|
A new widget, `iris::widget::List` (`iris/src/widget/list.rs` -- read its
|
||||||
|
module doc first), for the transcript's kind of screen: variable-height
|
||||||
|
rows, keyed by a `u64`, composed only while visible, moved rather than
|
||||||
|
re-laid-out on scroll, a scroll anchor that survives a row inserted above
|
||||||
|
it, "more" sentinels at each end, and "hold the edge nearest the tap" when
|
||||||
|
a row's height changes (`note_tap`, resolved in the layout pass).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut list = List::new(Axis::Y);
|
||||||
|
list.push_back(ListRow::new(key, row_widget)); // O(1)
|
||||||
|
list.push_front(ListRow::new(older_key, row)); // O(1), anchor unaffected
|
||||||
|
list.set_more_before(Some(spinner_widget)); // sentinel, drawn at the edge
|
||||||
|
list.note_tap(viewport_y); // before mutating a row's height
|
||||||
|
let (top, bottom) = list.extent(key).unwrap(); // last frame's on-screen box, if visible
|
||||||
|
```
|
||||||
|
|
||||||
|
Built entirely out of existing primitives (`Painter::widget`/`widget_within`/
|
||||||
|
`reposition`/`draw_twice`, and `draw_inner`'s own old-children diffing) --
|
||||||
|
no new mechanism was added to the render core for it. One correctness
|
||||||
|
lesson worth reading even for other widgets: a row that fills whatever
|
||||||
|
region it is offered (`Rect`, `is_size_independent`) cannot be measured at
|
||||||
|
a throwaway oversized region and then merely `reposition`ed into place --
|
||||||
|
`reposition` only ever writes an offset, never a size, so the oversized
|
||||||
|
primitive stays oversized. `List` fixes this by caching each row's real
|
||||||
|
height once measured and placing an already-known row directly at its
|
||||||
|
exact box; see `list.rs`'s `place` for the full reasoning and
|
||||||
|
`a_fill_shaped_background_is_not_left_oversized` for the regression test.
|
||||||
|
|
||||||
## 2026-09-05: a second backend (android-view), and what moved to make room for it
|
## 2026-09-05: a second backend (android-view), and what moved to make room for it
|
||||||
|
|
||||||
RUST.md's I2. Three changes a widget or app author would notice, all in
|
RUST.md's I2. Three changes a widget or app author would notice, all in
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ session spending an afternoon on them again.
|
|||||||
GLES-only `D2`/`D2Array` warning was confirmed a red herring — still
|
GLES-only `D2`/`D2Array` warning was confirmed a red herring — still
|
||||||
present post-fix, harmless. See I2's own entry below for the full
|
present post-fix, harmless. See I2's own entry below for the full
|
||||||
writeup. **E2** (a transcript in Masonry) is done — see its own box.
|
writeup. **E2** (a transcript in Masonry) is done — see its own box.
|
||||||
|
- **I3 — `iris::widget::List` built and benchmarked 2026-09-05, ticked in
|
||||||
|
the box below.** Variable-height rows, virtualised, moved not
|
||||||
|
relaid-out on scroll, insert-above-anchor and expand-hold both measured
|
||||||
|
flat across N = 100/1,000/10,000. What is left is wiring it into an
|
||||||
|
actual transcript screen and comparing against `transcript-bench.sh`'s
|
||||||
|
Compose baseline on the GPU emulator, which needs a session/scroll model
|
||||||
|
around it (closer to I5's scope) — see I3's own box for the exact
|
||||||
|
command once that screen exists. Read `list.rs`'s module doc and
|
||||||
|
`IRIS.md`'s 2026-09-05 entry before touching it: a widget that fills
|
||||||
|
whatever region it's offered (a `Rect` background) cannot be measured at
|
||||||
|
a throwaway region and merely repositioned, a lesson that generalises
|
||||||
|
beyond this one widget.
|
||||||
- **`client-core` built (2026-09-04)**, item 1 of the recommendation:
|
- **`client-core` built (2026-09-04)**, item 1 of the recommendation:
|
||||||
`event-model/` (the event types, now shared with `server/`) and
|
`event-model/` (the event types, now shared with `server/`) and
|
||||||
`client-core/` (REST and SSE clients, transcript fold, cache, highlighter,
|
`client-core/` (REST and SSE clients, transcript fold, cache, highlighter,
|
||||||
@@ -1430,13 +1442,72 @@ silently on real hardware.
|
|||||||
`adb install -r app/build/outputs/apk/debug/app-debug.apk`. Emulator
|
`adb install -r app/build/outputs/apk/debug/app-debug.apk`. Emulator
|
||||||
torn down after verification (`emu down`) per the machine's memory
|
torn down after verification (`emu down`) per the machine's memory
|
||||||
rule.
|
rule.
|
||||||
- [ ] **I3 — a virtualised, bottom-anchored list.** Variable-height rows,
|
- [x] **I3 — a virtualised, bottom-anchored list (2026-09-05).** Variable-height
|
||||||
keyed, composed only while visible, paged in both directions with a
|
rows, keyed, composed only while visible, paged in both directions
|
||||||
"more" sentinel at each end, a scroll anchor that survives rows
|
with a "more" sentinel at each end, a scroll anchor that survives
|
||||||
being inserted above, and "hold the edge nearest the tap" done in
|
rows being inserted above, and "hold the edge nearest the tap" done
|
||||||
the layout pass. Pass: 800 rows of real transcript text from the
|
in the layout pass. Built as `iris::widget::List`
|
||||||
|
(`iris/src/widget/list.rs`, its module doc is the design writeup) --
|
||||||
|
see `IRIS.md`'s 2026-09-05 entry for the public API and the one
|
||||||
|
correctness lesson worth carrying elsewhere (a fill-shaped background
|
||||||
|
cannot be measured at a throwaway oversized region and merely
|
||||||
|
`reposition`ed into place; it has to be placed at its cached real
|
||||||
|
size, or measured-then-redrawn via `draw_twice` on first appearance).
|
||||||
|
|
||||||
|
**Done**: the widget, 6 unit tests (`cargo test -p iris`, anchor and
|
||||||
|
edge-hold logic, all pure -- no GPU/window needed, same harness as
|
||||||
|
`layout_tests.rs`), `iris/benches/message_list.rs` rewritten to
|
||||||
|
measure the real widget instead of a hand-built `Span`+`Scroll`, two
|
||||||
|
new benchmark scenarios ((d) insert-above-anchor, (e)
|
||||||
|
expand-a-row-holding-its-edge), and `iris/examples/message_list.rs`
|
||||||
|
(800 rows, varied wrapped-text length, one in twelve with an image,
|
||||||
|
mouse-wheel scrollable) rendered via `run-headless.sh` and visually
|
||||||
|
verified (cropped with a throwaway PNG decoder, since this VM has no
|
||||||
|
image tooling -- see the commit for the crop script's shape).
|
||||||
|
|
||||||
|
**Numbers (2026-09-05, release, this VM), all flat across N =
|
||||||
|
100/1,000/10,000 as required:**
|
||||||
|
|
||||||
|
cd iris && ./run-bench.sh list
|
||||||
|
(a) first frame: ~12.3-12.9ms draws=80 rewrites=3 moves=0
|
||||||
|
(b) scroll, 200 ticks: 4.8-6.5ms draws=328 rewrites=12 moves=10131 (~0.025-0.033ms/tick)
|
||||||
|
(c) input grows, 40 lines: 8.9ms draws=1846 rewrites=102 moves=1195 (~0.22ms/line)
|
||||||
|
(d) insert-above-anchor, 200 pushes: 0.4ms draws=200 rewrites=0 moves=0 (~0.002ms/push)
|
||||||
|
(e) expand-hold, 40 growths: 0.10-0.11ms draws=119 rewrites=40 moves=15 (~0.003ms/growth)
|
||||||
|
|
||||||
|
(d) is the cleanest confirmation: 200 rows prepended one at a time
|
||||||
|
while scrolled to the loaded window's start cost 200 draws total (the
|
||||||
|
list widget's own redraw each push) and **zero** row draws or moves
|
||||||
|
-- none of the prepended rows ever entered the viewport, exactly as
|
||||||
|
the anchor-by-slot-index design predicts. (e) similarly stays tiny
|
||||||
|
and flat: growing one row 40 times, each preceded by `note_tap` at
|
||||||
|
its own edge, costs a total of 15 moves (the rows on the far side of
|
||||||
|
the held edge) regardless of how many thousand rows exist elsewhere
|
||||||
|
in the list.
|
||||||
|
|
||||||
|
**Verification.** `cargo fmt --all -- --check`,
|
||||||
|
`cargo build --workspace --all-targets`,
|
||||||
|
`cargo clippy --all-targets` (and `--benches --release` separately,
|
||||||
|
since benches aren't always covered), `cargo test --workspace` (25
|
||||||
|
passed) all clean in `iris/`.
|
||||||
|
|
||||||
|
**What remains — the emulator half of the pass condition, blocked on
|
||||||
|
the emulator being held by another session during this pass.** The
|
||||||
|
condition as written ("800 rows of real transcript text from the
|
||||||
sandbox scroll without a frame over the Compose baseline in
|
sandbox scroll without a frame over the Compose baseline in
|
||||||
`transcript-bench.sh`, measured on the GPU emulator.
|
`transcript-bench.sh`, measured on the GPU emulator") needs the
|
||||||
|
transcript screen actually rebuilt on top of `List` (this box only
|
||||||
|
built and measured the widget in isolation, per the task scope) and
|
||||||
|
then driven through the real emulator rig. Once that screen exists,
|
||||||
|
the exact command is:
|
||||||
|
|
||||||
|
cd app && ./transcript-bench.sh -k # or without -k for a fresh session
|
||||||
|
# compare its render report against the iris build's equivalent
|
||||||
|
|
||||||
|
This is a genuinely separate step (wiring `List` into an actual
|
||||||
|
session screen, i.e. most of I5's work) rather than something this
|
||||||
|
box's scope could finish alone -- recorded here rather than left
|
||||||
|
silently undone.
|
||||||
- [ ] **I4 — accessibility names via AccessKit.** Every control carries a
|
- [ ] **I4 — accessibility names via AccessKit.** Every control carries a
|
||||||
name; `ui-trace` can find and tap it by label. Pass: `bench-lib.sh`'s
|
name; `ui-trace` can find and tap it by label. Pass: `bench-lib.sh`'s
|
||||||
tap-by-name works against the iris screen unchanged.
|
tap-by-name works against the iris screen unchanged.
|
||||||
|
|||||||
+200
-33
@@ -1,6 +1,7 @@
|
|||||||
//! On-demand benchmarks for iris's message-list scenario -- IRIS_TODO.md's
|
//! On-demand benchmarks for iris's message-list scenario -- IRIS_TODO.md's
|
||||||
//! "Benchmarks" item. Never run by `cargo test`; run explicitly with
|
//! "Benchmarks" item, and RUST.md's I3. Never run by `cargo test`; run
|
||||||
//! `cargo bench --bench message_list --release` or `./run-bench.sh`.
|
//! explicitly with `cargo bench --bench message_list --release` or
|
||||||
|
//! `./run-bench.sh`.
|
||||||
//!
|
//!
|
||||||
//! **Why a plain `Instant`-timed binary, not criterion.** Every scenario
|
//! **Why a plain `Instant`-timed binary, not criterion.** Every scenario
|
||||||
//! here is really "how many `Widget::draw` calls and primitive rewrites did
|
//! here is really "how many `Widget::draw` calls and primitive rewrites did
|
||||||
@@ -13,26 +14,49 @@
|
|||||||
//! -- and it avoids a new dependency this crate does not otherwise need.
|
//! -- and it avoids a new dependency this crate does not otherwise need.
|
||||||
//! Per the code rules, the plain option is also the one shorter to explain.
|
//! Per the code rules, the plain option is also the one shorter to explain.
|
||||||
//!
|
//!
|
||||||
//! Scenarios (LAYOUT.md's O(1) move chain, and IRIS_TODO.md's "Benchmarks"
|
//! **The list under test is `iris::widget::List` (RUST.md's I3), not a
|
||||||
//! wording):
|
//! `Scroll` over a `Span` of pre-built rows.** Earlier versions of this
|
||||||
|
//! file built their own giant `Span` and wrapped it in `Scroll`, which
|
||||||
|
//! meant (a)/(b)/(c) below were measuring "move one big child," never the
|
||||||
|
//! virtualised widget the app's transcript screen actually needs. `List`
|
||||||
|
//! still needs every row's *widget* built up front by the caller (its
|
||||||
|
//! module doc explains why: it only ever sees `&dyn Widget` through
|
||||||
|
//! `Painter`, so it cannot construct a row lazily on its own) -- what
|
||||||
|
//! virtualisation buys is that only the rows currently on screen are ever
|
||||||
|
//! *drawn*, which is what the draw/rewrite/move counters below are
|
||||||
|
//! measuring, not construction time.
|
||||||
|
//!
|
||||||
|
//! Scenarios (LAYOUT.md's O(1) move chain, list.rs's module doc, and
|
||||||
|
//! IRIS_TODO.md's "Benchmarks" wording):
|
||||||
//!
|
//!
|
||||||
//! - (a) first-frame cost of a message list of N wrapped-text rows, some
|
//! - (a) first-frame cost of a message list of N wrapped-text rows, some
|
||||||
//! with an image, for N = 100 / 1,000 / 10,000.
|
//! with an image, for N = 100 / 1,000 / 10,000. With a virtualised list
|
||||||
|
//! this is expected to stop scaling with N once N exceeds a screenful --
|
||||||
|
//! the draw/rewrite counters below are the number that used to grow 10x
|
||||||
|
//! per 10x N and should not any more.
|
||||||
//! - (b) per-frame cost of scrolling that list -- must be O(1) moves, not
|
//! - (b) per-frame cost of scrolling that list -- must be O(1) moves, not
|
||||||
//! re-layout.
|
//! re-layout.
|
||||||
//! - (c) the input-box case: growing a fixed-height field at the bottom of
|
//! - (c) the input-box case: growing a fixed-height field at the bottom of
|
||||||
//! the screen must move the message list above it, not re-lay its rows.
|
//! the screen must move the message list above it, not re-lay its rows.
|
||||||
//! Reports frame time *and* the draw/rewrite/move counters LAYOUT.md
|
//! Reports frame time *and* the draw/rewrite/move counters LAYOUT.md
|
||||||
//! section 8 defines.
|
//! section 8 defines.
|
||||||
|
//! - (d) insert-above-anchor: paging older history onto the front of an
|
||||||
|
//! already-scrolled list. `List::push_front` is an O(1) index update
|
||||||
|
//! (list.rs's module doc); this measures that none of the rows already
|
||||||
|
//! on screen are touched by it.
|
||||||
|
//! - (e) expand-a-row-holding-its-edge: growing one row's height with a
|
||||||
|
//! tap recorded near one of its edges (list.rs's `note_tap`) must move
|
||||||
|
//! only the rows on the far side of it, never redraw the ones already
|
||||||
|
//! correctly placed.
|
||||||
//!
|
//!
|
||||||
//! (d), many images with zero steady-state bind-group creation, needs a
|
//! (f), many images with zero steady-state bind-group creation, needs a
|
||||||
//! real `wgpu` device and lives in `iris/examples/bench_images.rs` instead,
|
//! real `wgpu` device and lives in `iris/examples/bench_images.rs` instead,
|
||||||
//! driven through `run-headless.sh` -- see that file's header.
|
//! driven through `run-headless.sh` -- see that file's header.
|
||||||
//!
|
//!
|
||||||
//! `UiRenderState`/`Widgets` touch no GPU or window (as `layout_tests.rs`
|
//! `UiRenderState`/`Widgets` touch no GPU or window (as `layout_tests.rs`
|
||||||
//! notes), so everything here runs as an ordinary `--release` binary with
|
//! notes), so everything here runs as an ordinary `--release` binary with
|
||||||
//! no compositor. Numbers are recorded in IRIS_TODO.md, not here -- this
|
//! no compositor. Numbers are recorded in RUST.md's I3 box, not here --
|
||||||
//! file is the rig, not the result.
|
//! this file is the rig, not the result.
|
||||||
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -82,21 +106,21 @@ fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Scroll` over `n` message rows, one in `image_every` of them carrying
|
/// A virtualised `List` of `n` message rows, one in `image_every` of them
|
||||||
/// an image (0 disables images entirely). Returns the scroll widget (weak,
|
/// carrying an image (0 disables images entirely). Returns the list widget
|
||||||
/// so the caller can drive it) and the erased root to render.
|
/// (weak, so the caller can drive it) and the erased root to render.
|
||||||
fn build_list(
|
fn build_message_list(
|
||||||
rsc: &mut BenchRsc,
|
rsc: &mut BenchRsc,
|
||||||
n: usize,
|
n: usize,
|
||||||
image_every: usize,
|
image_every: usize,
|
||||||
) -> (WeakWidget<Scroll>, StrongWidget) {
|
) -> (WeakWidget<List>, StrongWidget) {
|
||||||
let mut span = Span::empty(Dir::DOWN);
|
let mut list = List::new(Axis::Y);
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
span.push(build_row(rsc, i, image_every));
|
let row = build_row(rsc, i, image_every);
|
||||||
|
list.push_back(ListRow::new(i as u64, row));
|
||||||
}
|
}
|
||||||
let span = rsc.ui.widgets.add_strong(span);
|
let list = rsc.ui.widgets.add_strong(list);
|
||||||
let scroll = rsc.ui.widgets.add_strong(Scroll::new(span.any(), Axis::Y));
|
(list.weak(), list.any())
|
||||||
(scroll.weak(), scroll.any())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report(label: &str, elapsed: std::time::Duration, draws: u64, rewrites: u64, moves: u64) {
|
fn report(label: &str, elapsed: std::time::Duration, draws: u64, rewrites: u64, moves: u64) {
|
||||||
@@ -111,7 +135,7 @@ fn bench_first_frame(n: usize) {
|
|||||||
let mut rsc = BenchRsc {
|
let mut rsc = BenchRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let (_scroll, root) = build_list(&mut rsc, n, 20);
|
let (_list, root) = build_message_list(&mut rsc, n, 20);
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize((1080.0, 2000.0));
|
render.resize((1080.0, 2000.0));
|
||||||
|
|
||||||
@@ -129,18 +153,18 @@ fn bench_first_frame(n: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// (b) Per-frame cost of scrolling an already-laid-out list of N rows.
|
/// (b) Per-frame cost of scrolling an already-laid-out list of N rows.
|
||||||
/// Warms up (as `layout_tests.rs`'s scrolling test documents: `Scroll`
|
/// Warms up (one no-op tick, matching `Scroll`'s own need for it before an
|
||||||
/// needs one no-op tick before a real scroll becomes a same-size move
|
/// ordinary Rust `layout_tests.rs` scrolling test becomes a same-size move
|
||||||
/// rather than a resize), then times a run of individual scroll ticks.
|
/// rather than a resize), then times a run of individual scroll ticks.
|
||||||
fn bench_scroll(n: usize, ticks: usize) {
|
fn bench_scroll(n: usize, ticks: usize) {
|
||||||
let mut rsc = BenchRsc {
|
let mut rsc = BenchRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let (scroll, root) = build_list(&mut rsc, n, 20);
|
let (list, root) = build_message_list(&mut rsc, n, 20);
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize((1080.0, 2000.0));
|
render.resize((1080.0, 2000.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
|
rsc.ui.widgets.get_mut(&list).unwrap().scroll(0.0);
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
render.take_counters();
|
render.take_counters();
|
||||||
|
|
||||||
@@ -149,7 +173,7 @@ fn bench_scroll(n: usize, ticks: usize) {
|
|||||||
let mut total_rewrites = 0u64;
|
let mut total_rewrites = 0u64;
|
||||||
let mut total_moves = 0u64;
|
let mut total_moves = 0u64;
|
||||||
for _ in 0..ticks {
|
for _ in 0..ticks {
|
||||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-8.0);
|
rsc.ui.widgets.get_mut(&list).unwrap().scroll(-8.0);
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
total += start.elapsed();
|
total += start.elapsed();
|
||||||
@@ -174,18 +198,16 @@ fn bench_scroll(n: usize, ticks: usize) {
|
|||||||
/// (c) The input-box case: a fixed-height field at the bottom of the screen
|
/// (c) The input-box case: a fixed-height field at the bottom of the screen
|
||||||
/// growing by a line at a time, with a message list of N rows filling the
|
/// growing by a line at a time, with a message list of N rows filling the
|
||||||
/// rest of the screen above it. Growing the input shrinks the *offered*
|
/// rest of the screen above it. Growing the input shrinks the *offered*
|
||||||
/// height of the scroll container (a single widget, from the outer
|
/// height of the list container (a single widget, from the outer `Span`'s
|
||||||
/// `Span`'s point of view) without changing the width it offers its
|
/// point of view) without changing the width it offers its content -- so
|
||||||
/// content -- so the rows underneath, which only care about width, must
|
/// the rows underneath, which only care about width, must not redraw; the
|
||||||
/// not redraw; the scroll's own re-registration of where its content sits
|
/// list's own re-registration of where its content sits is the one O(1)
|
||||||
/// is the one O(1) move this is checking for. See LAYOUT.md's `Scroll`
|
/// move this is checking for.
|
||||||
/// design note on offering the child last frame's content length, which is
|
|
||||||
/// exactly what keeps this a move instead of a reflow.
|
|
||||||
fn bench_input_grows(n: usize, lines: usize) {
|
fn bench_input_grows(n: usize, lines: usize) {
|
||||||
let mut rsc = BenchRsc {
|
let mut rsc = BenchRsc {
|
||||||
ui: UiData::default(),
|
ui: UiData::default(),
|
||||||
};
|
};
|
||||||
let (scroll, list_root) = build_list(&mut rsc, n, 20);
|
let (list, list_root) = build_message_list(&mut rsc, n, 20);
|
||||||
let list_area = rsc.ui.widgets.add_strong(Sized {
|
let list_area = rsc.ui.widgets.add_strong(Sized {
|
||||||
inner: list_root,
|
inner: list_root,
|
||||||
x: None,
|
x: None,
|
||||||
@@ -209,7 +231,7 @@ fn bench_input_grows(n: usize, lines: usize) {
|
|||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize((1080.0, 2000.0));
|
render.resize((1080.0, 2000.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
|
rsc.ui.widgets.get_mut(&list).unwrap().scroll(0.0);
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
render.take_counters();
|
render.take_counters();
|
||||||
|
|
||||||
@@ -244,6 +266,145 @@ fn bench_input_grows(n: usize, lines: usize) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// (d) Insert-above-anchor: the list is scrolled to its very first loaded
|
||||||
|
/// row (`jump_to_start`, an O(1) re-anchor) rather than left at the default
|
||||||
|
/// bottom, so a row prepended above it is genuinely "inserted above the
|
||||||
|
/// anchor" rather than merely far off-screen at the far end. Each
|
||||||
|
/// `push_front` is O(1) (list.rs's module doc: the anchor's slot is an
|
||||||
|
/// index, bumped by one) and, since the prepended rows never enter the
|
||||||
|
/// viewport, none of them should cost a draw either.
|
||||||
|
fn bench_insert_above_anchor(n: usize, inserts: usize) {
|
||||||
|
let mut rsc = BenchRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
};
|
||||||
|
let (list, root) = build_message_list(&mut rsc, n, 20);
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((1080.0, 2000.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
rsc.ui.widgets.get_mut(&list).unwrap().jump_to_start();
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
render.take_counters();
|
||||||
|
|
||||||
|
let mut total = std::time::Duration::ZERO;
|
||||||
|
let mut total_draws = 0u64;
|
||||||
|
let mut total_rewrites = 0u64;
|
||||||
|
let mut total_moves = 0u64;
|
||||||
|
for i in 0..inserts {
|
||||||
|
// Older-history rows: distinct keys below every existing one, so a
|
||||||
|
// real caller's paging code (prepending an older page) is exactly
|
||||||
|
// what this loop does.
|
||||||
|
let row = build_row(&mut rsc, usize::MAX - i, 20);
|
||||||
|
rsc.ui
|
||||||
|
.widgets
|
||||||
|
.get_mut(&list)
|
||||||
|
.unwrap()
|
||||||
|
.push_front(ListRow::new(i as u64, row));
|
||||||
|
let start = Instant::now();
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
total += start.elapsed();
|
||||||
|
let (draws, rewrites, moves) = render.take_counters();
|
||||||
|
total_draws += draws;
|
||||||
|
total_rewrites += rewrites;
|
||||||
|
total_moves += moves;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
&format!(
|
||||||
|
"(d) insert-above-anchor, N={n}, {inserts} pushes (totals; \
|
||||||
|
must not scale with N)"
|
||||||
|
),
|
||||||
|
total,
|
||||||
|
total_draws,
|
||||||
|
total_rewrites,
|
||||||
|
total_moves,
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" per-push average: {:.4}ms",
|
||||||
|
total.as_secs_f64() * 1000.0 / inserts as f64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (e) Expand-a-row-holding-its-edge: one row (fixed-height, so its size is
|
||||||
|
/// directly controllable) is grown a little at a time, each time preceded
|
||||||
|
/// by `note_tap` aimed at its own top edge -- the exact mechanism list.rs's
|
||||||
|
/// module doc describes and its unit tests check for correctness. This
|
||||||
|
/// measures its *cost*: only the rows on the far side of the grown one
|
||||||
|
/// (below it, since the top edge is held) should ever move, and nothing
|
||||||
|
/// should be redrawn purely because the list overall got taller.
|
||||||
|
fn bench_expand_holds_edge(n: usize, growths: usize) {
|
||||||
|
let mut rsc = BenchRsc {
|
||||||
|
ui: UiData::default(),
|
||||||
|
};
|
||||||
|
let mut list = List::new(Axis::Y);
|
||||||
|
// Near the end (not the very last row) so it is already on screen
|
||||||
|
// under the list's default bottom-anchored placement, for every N --
|
||||||
|
// no scrolling needed to bring it into view before measuring.
|
||||||
|
let growable_index = n.saturating_sub(3);
|
||||||
|
let mut growable = None;
|
||||||
|
for i in 0..n {
|
||||||
|
if i == growable_index {
|
||||||
|
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||||
|
let sized = rsc.ui.widgets.add_strong(Sized {
|
||||||
|
inner: rect.any(),
|
||||||
|
x: None,
|
||||||
|
y: Some(abs(40.0)),
|
||||||
|
});
|
||||||
|
growable = Some(sized.weak());
|
||||||
|
list.push_back(ListRow::new(i as u64, sized.any()));
|
||||||
|
} else {
|
||||||
|
let row = build_row(&mut rsc, i, 20);
|
||||||
|
list.push_back(ListRow::new(i as u64, row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let list = rsc.ui.widgets.add_strong(list);
|
||||||
|
let list_weak = list.weak();
|
||||||
|
let root = list.any();
|
||||||
|
let growable = growable.unwrap();
|
||||||
|
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize((1080.0, 2000.0));
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
render.take_counters();
|
||||||
|
|
||||||
|
let mut total = std::time::Duration::ZERO;
|
||||||
|
let mut total_draws = 0u64;
|
||||||
|
let mut total_rewrites = 0u64;
|
||||||
|
let mut total_moves = 0u64;
|
||||||
|
let mut height = 40.0f32;
|
||||||
|
let key = growable_index as u64;
|
||||||
|
for _ in 0..growths {
|
||||||
|
height += 10.0;
|
||||||
|
if let Some((top, _bottom)) = rsc.ui.widgets.get(&list_weak).unwrap().extent(key) {
|
||||||
|
rsc.ui
|
||||||
|
.widgets
|
||||||
|
.get_mut(&list_weak)
|
||||||
|
.unwrap()
|
||||||
|
.note_tap(top + 1.0);
|
||||||
|
}
|
||||||
|
rsc.ui.widgets.get_mut(&growable).unwrap().y = Some(abs(height));
|
||||||
|
let start = Instant::now();
|
||||||
|
render.update(&root, &mut rsc);
|
||||||
|
total += start.elapsed();
|
||||||
|
let (draws, rewrites, moves) = render.take_counters();
|
||||||
|
total_draws += draws;
|
||||||
|
total_rewrites += rewrites;
|
||||||
|
total_moves += moves;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
&format!(
|
||||||
|
"(e) expand-hold, N={n}, {growths} growths (totals; \
|
||||||
|
must not scale with N)"
|
||||||
|
),
|
||||||
|
total,
|
||||||
|
total_draws,
|
||||||
|
total_rewrites,
|
||||||
|
total_moves,
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" per-growth average: {:.4}ms",
|
||||||
|
total.as_secs_f64() * 1000.0 / growths as f64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("iris message-list benchmark -- release build, this machine's CPU");
|
println!("iris message-list benchmark -- release build, this machine's CPU");
|
||||||
for &n in &[100usize, 1_000, 10_000] {
|
for &n in &[100usize, 1_000, 10_000] {
|
||||||
@@ -255,4 +416,10 @@ fn main() {
|
|||||||
for &n in &[100usize, 1_000, 10_000] {
|
for &n in &[100usize, 1_000, 10_000] {
|
||||||
bench_input_grows(n, 40);
|
bench_input_grows(n, 40);
|
||||||
}
|
}
|
||||||
|
for &n in &[100usize, 1_000, 10_000] {
|
||||||
|
bench_insert_above_anchor(n, 200);
|
||||||
|
}
|
||||||
|
for &n in &[100usize, 1_000, 10_000] {
|
||||||
|
bench_expand_holds_edge(n, 40);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
//! RUST.md's I3: `iris::widget::List` with 800 rows of varied-length
|
||||||
|
//! wrapped text, one in twelve carrying a small image, scrollable with the
|
||||||
|
//! mouse wheel. Run headless with `iris/run-headless.sh message_list --shot
|
||||||
|
//! /tmp/message_list.png` -- there is no display on this machine, so that
|
||||||
|
//! is the only way to see it rendered; `run-tests.sh`/`cargo test` never
|
||||||
|
//! touch this file.
|
||||||
|
//!
|
||||||
|
//! Rows alternate two background tints so a screenshot can show the
|
||||||
|
//! boundary between adjacent rows even where the text itself wraps to a
|
||||||
|
//! different number of lines -- exactly the "variable-height rows" I3
|
||||||
|
//! asks for, and the thing a virtualised list gets wrong first if it is
|
||||||
|
//! wrong at all (a gap, an overlap, a row the wrong colour). This example
|
||||||
|
//! is also what found `List::place`'s oversized-background bug (see
|
||||||
|
//! list.rs's module doc and its `a_fill_shaped_background_is_not_left_
|
||||||
|
//! oversized` test) -- a plain unit test could have (and now does) catch
|
||||||
|
//! it directly, but it was this screenshot rendering as a single blank
|
||||||
|
//! tinted rectangle that pointed at it first.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use winit::{dpi::LogicalSize, window::WindowAttributes};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
DefaultApp::<State>::run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DefaultUiState)]
|
||||||
|
struct State {
|
||||||
|
ui_state: DefaultUiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROWS: usize = 800;
|
||||||
|
const IMAGE_EVERY: usize = 12;
|
||||||
|
|
||||||
|
/// Repeats a short sentence a varying number of times per row so real
|
||||||
|
/// wrapping happens at every row height from one line to several, rather
|
||||||
|
/// than every row being identically tall (which would render correctly
|
||||||
|
/// even with a broken height measurement).
|
||||||
|
fn row_text(i: usize) -> String {
|
||||||
|
const SENTENCE: &str =
|
||||||
|
"Iris lays out this row once and moves it on scroll, never re-laying it out. ";
|
||||||
|
let repeats = 1 + (i * 7) % 5;
|
||||||
|
format!("Message {i}: {}", SENTENCE.repeat(repeats))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A small solid-colour square standing in for a real decoded image --
|
||||||
|
/// what matters for I3 is that a row can carry an `Image` widget at all,
|
||||||
|
/// not what the picture shows.
|
||||||
|
fn row_image(i: usize) -> image::DynamicImage {
|
||||||
|
let hue = ((i * 47) % 255) as u8;
|
||||||
|
image::RgbaImage::from_pixel(48, 48, image::Rgba([hue, 128, 255 - hue, 255])).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
|
||||||
|
let tint = if i.is_multiple_of(2) {
|
||||||
|
Color::rgb(120, 130, 170)
|
||||||
|
} else {
|
||||||
|
Color::rgb(70, 80, 140)
|
||||||
|
};
|
||||||
|
let text_color = Color::BLACK;
|
||||||
|
if i.is_multiple_of(IMAGE_EVERY) {
|
||||||
|
let text = wtext(row_text(i))
|
||||||
|
.wrap(true)
|
||||||
|
.color(text_color)
|
||||||
|
.add_strong(rsc)
|
||||||
|
.any();
|
||||||
|
let img = image::<Rsc>(row_image(i))(rsc);
|
||||||
|
let img = rsc.widgets_mut().add_strong(img).any();
|
||||||
|
let mut span = Span::empty(Dir::DOWN);
|
||||||
|
span.push(text);
|
||||||
|
span.push(img);
|
||||||
|
span.pad(8.0).background(rect(tint)).add_strong(rsc).any()
|
||||||
|
} else {
|
||||||
|
wtext(row_text(i))
|
||||||
|
.wrap(true)
|
||||||
|
.color(text_color)
|
||||||
|
.pad(8.0)
|
||||||
|
.background(rect(tint))
|
||||||
|
.add_strong(rsc)
|
||||||
|
.any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultAppState for State {
|
||||||
|
// A phone-plausible portrait shape (the transcript screen this is
|
||||||
|
// standing in for). The tiling headless compositor `run-headless.sh`
|
||||||
|
// uses ignores this and fills its own 1920x1200 output regardless, but
|
||||||
|
// it's a correct hint for any other backend (a real window manager, or
|
||||||
|
// android-view) and costs nothing to state.
|
||||||
|
fn window_attributes() -> WindowAttributes {
|
||||||
|
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
mut ui_state: DefaultUiState,
|
||||||
|
rsc: &mut DefaultRsc<Self>,
|
||||||
|
_: Proxy<Self::Event>,
|
||||||
|
) -> Self {
|
||||||
|
let mut list = List::new(Axis::Y);
|
||||||
|
for i in 0..ROWS {
|
||||||
|
let row = build_row(rsc, i);
|
||||||
|
list.push_back(ListRow::new(i as u64, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = list
|
||||||
|
.on(CursorSense::Scroll, |ctx, rsc| {
|
||||||
|
let delta = ctx.data.scroll_delta.y * 50.0;
|
||||||
|
ctx.widget(rsc).scroll(delta);
|
||||||
|
})
|
||||||
|
.masked()
|
||||||
|
.background(rect(Color::WHITE))
|
||||||
|
.add_strong(rsc);
|
||||||
|
ui_state.set_root(root.any());
|
||||||
|
|
||||||
|
Self { ui_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
|||||||
mod image;
|
mod image;
|
||||||
|
mod list;
|
||||||
mod mask;
|
mod mask;
|
||||||
mod position;
|
mod position;
|
||||||
mod ptr;
|
mod ptr;
|
||||||
@@ -7,6 +8,7 @@ mod text;
|
|||||||
mod trait_fns;
|
mod trait_fns;
|
||||||
|
|
||||||
pub use image::*;
|
pub use image::*;
|
||||||
|
pub use list::*;
|
||||||
pub use mask::*;
|
pub use mask::*;
|
||||||
pub use position::*;
|
pub use position::*;
|
||||||
pub use ptr::*;
|
pub use ptr::*;
|
||||||
|
|||||||
Reference in new issue
Block a user