iris: on-demand message-list/image benchmarks, and two O(N) findings

IRIS_TODO.md's "Benchmarks" item: a message list of N wrapped-text rows
(first-frame cost), scrolling it, and growing an input box above which
the list must move rather than re-layout -- all as a plain, harness=false
`cargo bench` binary (iris/benches/message_list.rs) since UiRenderState
touches no GPU or window, chosen over criterion because every scenario
here reduces to a count take_counters already answers exactly, and a
new dependency wasn't worth it. Scroll (200 ticks) and the input-grow
case (40 lines) are flat across N=100/1,000/10,000: LAYOUT.md's O(1)
move chain holds.

The many-images case (d) needs a real wgpu device, so it's a headless
example (iris/examples/bench_images.rs) plus a new
GpuTextures/UiRenderNode counter, take_image_bind_group_creates,
mirroring take_counters. It found two real non-O(1) costs, recorded as
new Fix items rather than redesigned: bind-group creation takes two
frames to settle after a cold load instead of one, and appending a
single image to an already-loaded 1,000-image list rebuilds all 1,000
existing bind groups (masks/move_offsets buffer growth triggers
rebuild_image_bind_groups unconditionally).

run-bench.sh wraps both. Numbers and commands are in IRIS_TODO.md.

cargo fmt --all -- --check, cargo clippy --all-targets, and
cargo test --workspace (19 passed) all clean; benches are not run by
cargo test.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
irisandClaude Sonnet committed 2026-09-05 00:13:58 -04:00
1 parent fba572427d
commit 288853c094
7 files changed
+531 -11

No files matched your search

+114 -10
View File
@@ -26,18 +26,122 @@ order and what "done" looks like. Tick and date them in place.
still reaches the button — confirmed to fail on the pre-fix code and
pass after.
- [ ] **Appending one image to an already-loaded list rebuilds every other
image's bind group (2026-09-05).** Found by the benchmark below, not
designed against: `GpuTextures::update` (`core/src/render/texture.rs`)
triggers `rebuild_image_bind_groups` — a loop over *every live
standalone image*, rebuilding its `BindGroup` — whenever the shared
`masks` or `move_offsets` GPU buffer is resized (`masks_resized ||
moves_resized` in `UiRenderNode::update`, `core/src/render/mod.rs`), and
a widget getting its *first* move-offset slot (LAYOUT.md section 2 —
every widget gets one on first draw) can be exactly what grows that
buffer. So one new message with one new image, appended to a transcript
that already has N images loaded, does not cost O(1): it costs one
`create_image` for the new image plus one `make_image_bind_group` per
*existing* image, because the new widget's own move slot pushed the
arena past its capacity. Measured directly in
`iris/examples/bench_images.rs`: appending a 1,001st image to 1,000
already-settled ones reports **1,001** bind-group creates for that one
frame, not 1 (`./run-bench.sh images`, frame 5 in the transcript below).
This is the same class of cost LAYOUT.md's move chain exists to avoid
elsewhere in the codebase, just not yet closed off here — the fix is
presumably to size `masks`/`move_offsets` with headroom (the array
texture already grows by doubling, `grow_array`, for the same reason) so
an ordinary append does not cross a capacity boundary, or to stop tying
the *image* bind group's contents to a buffer that changes on every new
widget in the whole tree, image or not. Not designed further here per
the "do not redesign, record it" instruction this benchmark was built
under.
- [ ] **Bind-group creation takes two frames to reach the steady state, not
one (2026-09-05).** Same benchmark: loading 1,000 images cold reports
1,000 creates on frame 1 (expected — this is `create_image`, one per
new image) *and again* 1,000 on frame 2, with nothing between the two
frames marked dirty, before settling to 0 from frame 3. The second
frame's 1,000 is `rebuild_image_bind_groups` again, for the same
masks/move-offsets buffer-growth reason as the item above — the arena
apparently does not finish growing to its steady size within the first
frame the tree is drawn. Not chased further; recorded so whoever fixes
the item above checks whether the fix also closes this one, since they
look like the same root cause measured two different ways.
## Build
- [ ] **Benchmarks**, not unit tests, run on demand (a `benches/` or a
script under `iris/`, never in `cargo test`). The scenario that matters
most is a **message list** — chat apps and this app's transcript alike —
stressed with many messages and many images. One case in particular:
**resizing an input box** (typing enough text to grow it) that pushes a
long list of messages above it must stay very fast and recalculate
almost nothing — a move of everything above, not a re-layout. That is
exactly the O(1) move chain in LAYOUT.md; the benchmark is what proves
it. Done when the numbers are in this file with the command, and the
input-box case reports draws re-run, not just frame time.
- [x] **Benchmarks**, not unit tests, run on demand (2026-09-05; a
`benches/` or a script under `iris/`, never in `cargo test`). The
scenario that matters most is a **message list** — chat apps and this
app's transcript alike — stressed with many messages and many images.
One case in particular: **resizing an input box** (typing enough text to
grow it) that pushes a long list of messages above it must stay very
fast and recalculate almost nothing — a move of everything above, not a
re-layout. That is exactly the O(1) move chain in LAYOUT.md; the
benchmark is what proves it. Done when the numbers are in this file with
the command, and the input-box case reports draws re-run, not just frame
time.
**Built as two rigs**, chosen per scenario by whether a real `wgpu`
device is needed (`UiRenderState`/`Widgets` touch no GPU or window, so
most of this runs as an ordinary binary — the same property
`layout_tests.rs` relies on):
- `iris/benches/message_list.rs` — a plain `Instant`-timed binary
(`[[bench]] harness = false` in `iris/Cargo.toml`), not criterion: see
the file's own header for why (short version — every scenario here
reduces to a *count* `UiRenderState::take_counters` already produces,
which criterion's statistical machinery adds nothing to and which a
new dependency is not worth pulling in for). Covers (a) first-frame
cost of a message list of N wrapped-text rows (one in 20 also carrying
a small in-memory image) for N = 100/1,000/10,000; (b) per-frame cost
of scrolling that list, 200 ticks; (c) the input-box case — a
fixed-height field at the bottom of the screen growing by a line 40
times, with the message list above it filling the rest of the screen.
Run: `cd iris && cargo bench --bench message_list` (always release —
`cargo bench` builds the `bench` profile, which is optimized).
- `iris/examples/bench_images.rs` — needs a real device, so it runs
through `iris/run-headless.sh bench_images`, printing
`UiRenderNode::take_image_bind_group_creates()` (a new counter, added
in `core/src/render/texture.rs` and `core/src/render/mod.rs`,
mirroring `UiRenderState::take_counters`) each frame. Covers (d): 1,000
image rows, checked both cold (does bind-group creation reach zero
once loaded) and after appending one more image once settled (does
*that* stay cheap) — the second question is what actually matters for
a live transcript and is what turned up the two Fix items above.
- `iris/run-bench.sh [list|images]` runs either or both and is what to
run before/after touching `Scroll`, `Span`, `Sized`, the move-offset
chain, or `GpuTextures`.
**Numbers (2026-09-05, release, `cargo bench`/`run-headless.sh`, this
VM: AMD Ryzen 7 3800X, 8 cores, rustc 1.98.0 nightly-2026-09-03):**
cd iris && cargo bench --bench message_list
(a) first frame, N=100: 30.30ms draws=227 rewrites=15 moves=0
(a) first frame, N=1000: 186.04ms draws=2252 rewrites=150 moves=0
(a) first frame, N=10000:1770.36ms draws=22502 rewrites=1500 moves=0
(b) scroll, N=100/1000/10000, 200 ticks each:
draws=200 rewrites=0 moves=200 (identical at every N)
per-tick average: 0.0002ms (identical at every N)
(c) input grows 40 lines, N=100/1000/10000 rows above it:
draws=320 rewrites=40 moves=160 (identical at every N)
per-line average: 0.0012-0.0013ms (identical at every N)
cd iris && ./run-bench.sh images
frame=1 bind_group_creates=1000 (cold load)
frame=2 bind_group_creates=1000 (see Fix item above)
frame=3 bind_group_creates=0
frame=4 bind_group_creates=0
(append one image here)
frame=5 bind_group_creates=1001 (see Fix item above)
frame=6 bind_group_creates=0
**Reading it**: (a) is real, necessary work — shaping and laying out N
never-before-seen text rows — and scales with N as it must, ~10x cost
per 10x N. (b) and (c) are the pass conditions that matter: both are
**exactly flat across N = 100 to 10,000**, confirming LAYOUT.md's O(1)
move chain holds for both scrolling and for a growing input box pushing
the message list — draws/moves per tick or per line do not grow with
list size, and the per-operation cost (a fraction of a microsecond) is
nowhere near a frame budget. (d)'s cold-load and steady-state halves
behave as designed; its *append* half did not, which is the two Fix
items above.
- [ ] **Masks defined relative to each other.** Wanted: mask A multiplies
by something *and also* applies mask B — a mask can reference a parent
mask, the way the move chain references a parent offset. Today masks
+8
View File
@@ -20,6 +20,14 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
[dev-dependencies]
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
# Plain Instant-timed binaries, not criterion -- see benches/message_list.rs's
# header for why. `harness = false` opts out of the unstable `#[bench]`
# test-crate harness cargo would otherwise want, in favour of an ordinary
# `fn main()`.
[[bench]]
name = "message_list"
harness = false
[workspace]
members = ["core", "macro"]
+258
View File
@@ -0,0 +1,258 @@
//! On-demand benchmarks for iris's message-list scenario -- IRIS_TODO.md's
//! "Benchmarks" item. Never run by `cargo test`; run explicitly with
//! `cargo bench --bench message_list --release` or `./run-bench.sh`.
//!
//! **Why a plain `Instant`-timed binary, not criterion.** Every scenario
//! here is really "how many `Widget::draw` calls and primitive rewrites did
//! this frame cost," which `UiRenderState::take_counters` already answers
//! exactly (see `iris/src/layout_tests.rs`, which this file's harness
//! mirrors). A short loop that times itself and prints the counters
//! alongside the wall time says everything criterion's warm-up/sampling/
//! outlier-removal machinery would add on top, for scenarios that are
//! fundamentally about a *count*, not a noisy microbenchmark distribution
//! -- 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.
//!
//! Scenarios (LAYOUT.md's O(1) move chain, and IRIS_TODO.md's "Benchmarks"
//! wording):
//!
//! - (a) first-frame cost of a message list of N wrapped-text rows, some
//! with an image, for N = 100 / 1,000 / 10,000.
//! - (b) per-frame cost of scrolling that list -- must be O(1) moves, not
//! re-layout.
//! - (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.
//! Reports frame time *and* the draw/rewrite/move counters LAYOUT.md
//! section 8 defines.
//!
//! (d), many images with zero steady-state bind-group creation, needs a
//! real `wgpu` device and lives in `iris/examples/bench_images.rs` instead,
//! driven through `run-headless.sh` -- see that file's header.
//!
//! `UiRenderState`/`Widgets` touch no GPU or window (as `layout_tests.rs`
//! notes), so everything here runs as an ordinary `--release` binary with
//! no compositor. Numbers are recorded in IRIS_TODO.md, not here -- this
//! file is the rig, not the result.
use iris::prelude::*;
use std::time::Instant;
/// The minimal `UiRsc` a benchmark needs -- identical in shape to
/// `layout_tests.rs`'s `TestRsc`.
struct BenchRsc {
ui: UiData,
}
impl UiRsc for BenchRsc {
fn ui(&self) -> &UiData {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
&mut self.ui
}
}
/// Long enough to force real wrapping at a phone-plausible column width, and
/// varied enough (no two rows byte-identical) that nothing can special-case
/// on repeated content.
const BODY: &str = "The quick brown fox jumps over the lazy dog. Iris lays \
out wrapped text by shaping once per width and caching the result, so a \
row that is offered the same width twice does not reshape. This sentence \
exists only to give a row enough text to wrap across several lines at a \
typical phone column width.";
/// One message row: a wrapped `Text`, and every `image_every`th row also an
/// `Image` beneath it -- a small in-memory RGBA square rather than a file,
/// so N=10,000 rows costs no disk I/O.
fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
let mut text = Text::new(format!("Message {i}: {BODY}"));
text.wrap = true;
let text = rsc.ui.widgets.add_strong(text).any();
if image_every > 0 && i.is_multiple_of(image_every) {
let img = image::DynamicImage::new_rgba8(64, 64);
let image_widget = image::<BenchRsc>(img)(rsc);
let image_widget = rsc.ui.widgets.add_strong(image_widget).any();
let mut row = Span::empty(Dir::DOWN);
row.push(text);
row.push(image_widget);
rsc.ui.widgets.add_strong(row).any()
} else {
text
}
}
/// A `Scroll` over `n` message rows, one in `image_every` of them carrying
/// an image (0 disables images entirely). Returns the scroll widget (weak,
/// so the caller can drive it) and the erased root to render.
fn build_list(
rsc: &mut BenchRsc,
n: usize,
image_every: usize,
) -> (WeakWidget<Scroll>, StrongWidget) {
let mut span = Span::empty(Dir::DOWN);
for i in 0..n {
span.push(build_row(rsc, i, image_every));
}
let span = rsc.ui.widgets.add_strong(span);
let scroll = rsc.ui.widgets.add_strong(Scroll::new(span.any(), Axis::Y));
(scroll.weak(), scroll.any())
}
fn report(label: &str, elapsed: std::time::Duration, draws: u64, rewrites: u64, moves: u64) {
println!(
"{label}: {:.2}ms draws={draws} rewrites={rewrites} moves={moves}",
elapsed.as_secs_f64() * 1000.0
);
}
/// (a) First-frame cost of a message list of N rows.
fn bench_first_frame(n: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let (_scroll, root) = build_list(&mut rsc, n, 20);
let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0));
let start = Instant::now();
render.update(&root, &mut rsc);
let elapsed = start.elapsed();
let (draws, rewrites, moves) = render.take_counters();
report(
&format!("(a) first frame, N={n}"),
elapsed,
draws,
rewrites,
moves,
);
}
/// (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`
/// needs one no-op tick before a real scroll becomes a same-size move
/// rather than a resize), then times a run of individual scroll ticks.
fn bench_scroll(n: usize, ticks: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let (scroll, root) = build_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(&scroll).unwrap().scroll(0.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;
for _ in 0..ticks {
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-8.0);
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!("(b) scroll, N={n}, {ticks} ticks (totals; expect draws/moves independent of N)"),
total,
total_draws,
total_rewrites,
total_moves,
);
println!(
" per-tick average: {:.4}ms",
total.as_secs_f64() * 1000.0 / ticks as f64
);
}
/// (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
/// rest of the screen above it. Growing the input shrinks the *offered*
/// height of the scroll container (a single widget, from the outer
/// `Span`'s point of view) without changing the width it offers its
/// content -- so the rows underneath, which only care about width, must
/// not redraw; the scroll's own re-registration of where its content sits
/// is the one O(1) move this is checking for. See LAYOUT.md's `Scroll`
/// 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) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let (scroll, list_root) = build_list(&mut rsc, n, 20);
let list_area = rsc.ui.widgets.add_strong(Sized {
inner: list_root,
x: None,
y: Some(rest(1.0)),
});
let line_height = 24.0;
let input_rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
let input_area = rsc.ui.widgets.add_strong(Sized {
inner: input_rect.any(),
x: None,
y: Some(abs(line_height)),
});
let input_area_weak = input_area.weak();
let mut root_span = Span::empty(Dir::DOWN);
root_span.push(list_area.any());
root_span.push(input_area.any());
let root = rsc.ui.widgets.add_strong(root_span).any();
let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0));
render.update(&root, &mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.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;
for line in 1..=lines {
rsc.ui.widgets.get_mut(&input_area_weak).unwrap().y =
Some(abs(line_height * (line + 1) as f32));
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!(
"(c) input grows by {lines} lines above N={n} rows (totals; \
draws/rewrites must not scale with N)"
),
total,
total_draws,
total_rewrites,
total_moves,
);
println!(
" per-line average: {:.4}ms",
total.as_secs_f64() * 1000.0 / lines as f64
);
}
fn main() {
println!("iris message-list benchmark -- release build, this machine's CPU");
for &n in &[100usize, 1_000, 10_000] {
bench_first_frame(n);
}
for &n in &[100usize, 1_000, 10_000] {
bench_scroll(n, 200);
}
for &n in &[100usize, 1_000, 10_000] {
bench_input_grows(n, 40);
}
}
+7
View File
@@ -441,4 +441,11 @@ impl UiRenderNode {
pub fn view_count(&self) -> usize {
self.textures.view_count()
}
/// Standalone-image bind groups built since the last call -- see
/// `GpuTextures::take_bind_group_creates`. Call once per frame before
/// `update()` to measure exactly that frame.
pub fn take_image_bind_group_creates(&mut self) -> u64 {
self.textures.take_bind_group_creates()
}
}
+19 -1
View File
@@ -61,6 +61,14 @@ pub struct GpuTextures {
/// nothing of its own to put there: rects and glyphs never sample it,
/// but the layout requires something bound regardless.
null_view: TextureView,
/// Standalone-image bind groups actually built (`create_image`'s own
/// build, or one per slot touched by `rebuild_image_bind_groups`) since
/// the last `take_bind_group_creates`. IRIS_TODO.md's "many images"
/// benchmark reads this to prove the steady-state cost of an
/// unchanging image list is zero, the same way `UiRenderState`'s
/// `draw_count`/`region_mut_count` prove the layout side.
bind_group_creates: u64,
}
impl GpuTextures {
@@ -297,12 +305,13 @@ impl GpuTextures {
masks,
move_offsets,
);
self.bind_group_creates += 1;
}
}
}
fn create_image(
&self,
&mut self,
image: &DynamicImage,
rsc_layout: &BindGroupLayout,
masks: &ArrBuf<Mask>,
@@ -339,6 +348,7 @@ impl GpuTextures {
masks,
move_offsets,
);
self.bind_group_creates += 1;
ImageGpu {
texture,
view,
@@ -424,9 +434,17 @@ impl GpuTextures {
page_count: 0,
sampler,
null_view,
bind_group_creates: 0,
}
}
/// Reads and zeroes the standalone-image bind-group creation counter --
/// call once per frame before `update()`, mirroring
/// `UiRenderState::take_counters`.
pub fn take_bind_group_creates(&mut self) -> u64 {
std::mem::take(&mut self.bind_group_creates)
}
pub fn array_view(&self) -> &TextureView {
&self.array_view
}
+101
View File
@@ -0,0 +1,101 @@
//! (d) of IRIS_TODO.md's "Benchmarks" item: 1,000 image rows, checking that
//! standalone-image bind-group *creation* -- a real `wgpu` resource, unlike
//! the counters in `benches/message_list.rs` -- goes to zero once every
//! image has loaded. This needs an actual `wgpu` device (`GpuTextures`,
//! `UiRenderNode`), so unlike the rest of the suite it cannot run as a
//! plain binary; run it through `iris/run-headless.sh bench_images`, which
//! gives it a real (headless, GPU-accelerated) compositor and surface. See
//! `run-bench.sh` for the wrapper that greps its output into one line.
//!
//! Each `RedrawRequested` prints the frame number and
//! `UiRenderNode::take_image_bind_group_creates()` for that frame, then
//! requests another redraw (nothing else marks the scene dirty, so without
//! this the app would only ever draw once). The first frame is expected to
//! report 1,000 (one create per image, on first load); the steady state
//! IRIS_TODO.md asks this scenario to prove is every frame after settling
//! down to 0.
//!
//! After `SETTLE_FRAMES` it appends one *new* image row (a transcript
//! receiving one more message) and keeps counting -- a chat transcript's
//! real access pattern is "one more image arrives," not "reload the whole
//! list," so the steady-state question that actually matters is the
//! *incremental* cost of that one append, not just whether an untouched
//! scene costs zero. It exits after `FRAMES`.
use iris::prelude::*;
const ROWS: usize = 1000;
const SETTLE_FRAMES: usize = 4;
const FRAMES: usize = 6;
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
span: WeakWidget<Span>,
frame: usize,
appended: bool,
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let mut span = Span::empty(Dir::DOWN);
for _ in 0..ROWS {
let img = image::DynamicImage::new_rgba8(32, 32);
let widget = image::<DefaultRsc<Self>>(img)(rsc);
let widget = rsc.ui.widgets.add_strong(widget);
span.push(widget.any());
}
let span = rsc.ui.widgets.add_strong(span);
let span_weak = span.weak();
let root = rsc.ui.widgets.add_strong(Scroll::new(span.any(), Axis::Y));
ui_state.set_root(root.any());
Self {
ui_state,
span: span_weak,
frame: 0,
appended: false,
}
}
fn window_event(
&mut self,
event: winit::event::WindowEvent,
rsc: &mut DefaultRsc<Self>,
_render: &mut UiRenderState,
) {
if !matches!(event, winit::event::WindowEvent::RedrawRequested) {
return;
}
self.frame += 1;
let creates = self.ui_state.renderer.ui.take_image_bind_group_creates();
println!(
"BENCH_IMAGES frame={} bind_group_creates={creates}",
self.frame
);
if self.frame == SETTLE_FRAMES && !self.appended {
self.appended = true;
let img = image::DynamicImage::new_rgba8(32, 32);
let widget = image::<DefaultRsc<Self>>(img)(rsc);
let widget = rsc.ui.widgets.add_strong(widget);
rsc.ui
.widgets
.get_mut(&self.span)
.unwrap()
.push(widget.any());
println!("BENCH_IMAGES appended one image after settling");
}
if self.frame < FRAMES {
self.ui_state.window.request_redraw();
} else {
std::process::exit(0);
}
}
}
fn main() {
DefaultApp::<State>::run();
}
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Runs iris's on-demand benchmark suite (IRIS_TODO.md's "Benchmarks" item).
# Never run by `cargo test`; run this by hand or before/after a layout
# change. Always release -- see AGENTS.md's own rule against reading a
# frame time from a debug build.
#
# ./run-bench.sh # everything
# ./run-bench.sh list # just the CPU-only message-list scenarios
# ./run-bench.sh images # just the GPU bind-group-creation scenario
set -eu
here=$(cd "$(dirname "$0")" && pwd)
cd "$here"
what="${1:-all}"
if [ "$what" = "all" ] || [ "$what" = "list" ]; then
echo "=== message_list (CPU-only, no window) ==="
cargo bench --bench message_list
fi
if [ "$what" = "all" ] || [ "$what" = "images" ]; then
echo "=== bench_images (real wgpu device, via run-headless.sh) ==="
timeout 60 ./run-headless.sh bench_images --seconds 4 2>&1 | grep "^BENCH_IMAGES"
fi