use iris::prelude::*; use std::time::Instant; struct BenchRsc { ui: Ui, } impl UiRsc for BenchRsc { fn ui(&self) -> &Ui { &self.ui } fn ui_mut(&mut self) -> &mut Ui { &mut self.ui } } 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."; fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget { let text = wtext(format!("Message {i}: {BODY}")) .wrap(true) .add_strong(rsc) .any(); if image_every > 0 && i.is_multiple_of(image_every) { let img = image::DynamicImage::new_rgba8(64, 64); let image_widget = image::(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 } } fn build_message_list( rsc: &mut BenchRsc, n: usize, image_every: usize, ) -> (WeakWidget, StrongWidget) { let mut list = LazySpan::new(Dir::DOWN, Pin::End); for i in 0..n { let row = build_row(rsc, i, image_every); list.push_back(LazyItem::new(i as u64, row)); } let list = rsc.ui.widgets.add_strong(list); (list.weak(), list.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 ); } fn bench_first_frame(n: usize) { let mut rsc = BenchRsc { ui: Ui::default() }; let (_list, root) = build_message_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 RenderCounters { draws, region_rewrites: rewrites, moves, .. } = render.take_counters(); report( &format!("(a) first frame, N={n}"), elapsed, draws, rewrites, moves, ); } fn bench_scroll(n: usize, ticks: usize) { let mut rsc = BenchRsc { ui: Ui::default() }; let (scroll, 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(&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 RenderCounters { draws, region_rewrites: 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 ); } fn bench_input_grows(n: usize, lines: usize) { let mut rsc = BenchRsc { ui: Ui::default() }; let (scroll, list_root) = build_message_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(PaintId::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 RenderCounters { draws, region_rewrites: 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 bench_insert_above_anchor(n: usize, inserts: usize) { let mut rsc = BenchRsc { ui: Ui::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 { let row = build_row(&mut rsc, usize::MAX - i, 20); rsc.ui .widgets .get_mut(&list) .unwrap() .push_front(LazyItem::new(i as u64, row)); let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); let RenderCounters { draws, region_rewrites: 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 ); } fn bench_expand_holds_edge(n: usize, growths: usize) { let mut rsc = BenchRsc { ui: Ui::default() }; let mut list = LazySpan::new(Dir::DOWN, Pin::End); 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(PaintId::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(LazyItem::new(i as u64, sized.any())); } else { let row = build_row(&mut rsc, i, 20); list.push_back(LazyItem::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 RenderCounters { draws, region_rewrites: 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 bench_redraw_big_text(chars: usize, redraws: usize) { let mut rsc = BenchRsc { ui: Ui::default() }; let content: String = (0..chars) .map(|i| char::from(b'a' + (i % 26) as u8)) .collect(); let text = wtext(content).wrap(true).add_strong(&mut rsc); let handle = text.weak(); let root = text.any(); 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; for _ in 0..redraws { rsc.ui.widgets.get_mut(&handle).unwrap(); let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); } let RenderCounters { draws, region_rewrites: rewrites, moves, .. } = render.take_counters(); report( &format!("(g) redraw one {chars}-glyph text, {redraws}x (totals)"), total, draws, rewrites, moves, ); println!( " per redraw: {:.3}ms, per glyph: {:.4}us", total.as_secs_f64() * 1000.0 / redraws as f64, total.as_secs_f64() * 1_000_000.0 / (redraws * chars) 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); } 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); } for &chars in &[1_000usize, 10_000, 50_000] { bench_redraw_big_text(chars, 10); } }