151 lines
4.6 KiB
Rust
151 lines
4.6 KiB
Rust
//! Per-frame dispatch hot path benches.
|
||
//!
|
||
//! The renderer rebuilds the widget tree on every frame, then hit-tests
|
||
//! pointer events against the resulting flat `Vec<LaidOutWidget>` and
|
||
//! looks up handlers by `flat_idx`. Both calls are O(N) in the slice
|
||
//! length. Run with `cargo bench --bench lookup`.
|
||
|
||
use criterion::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion };
|
||
|
||
use ltk::test_support::{ find_handlers, find_widget, find_widget_at, LaidOutWidget, WidgetHandlers };
|
||
use ltk::{ Point, Rect };
|
||
|
||
// ── Fixture builders ──────────────────────────────────────────────────────────
|
||
|
||
/// Build N widgets laid out in a vertical strip. Each widget is 100×30 with a
|
||
/// 1px gap, so `find_widget_at(x=50, y=k*31+15)` lands inside widget `k`. Half
|
||
/// of them carry a Button handler so `find_handlers` exercises the
|
||
/// match-and-clone path on roughly every other lookup.
|
||
fn build_widgets( n: usize ) -> Vec<LaidOutWidget<()>>
|
||
{
|
||
( 0 .. n ).map( |i|
|
||
{
|
||
let rect = Rect
|
||
{
|
||
x: 0.0,
|
||
y: ( i as f32 ) * 31.0,
|
||
width: 100.0,
|
||
height: 30.0,
|
||
};
|
||
let handlers = if i % 2 == 0
|
||
{
|
||
WidgetHandlers::Button { on_press: Some( () ), on_long_press: None, on_drag_start: None, on_escape: None, repeating: false }
|
||
} else {
|
||
WidgetHandlers::None
|
||
};
|
||
LaidOutWidget
|
||
{
|
||
rect,
|
||
flat_idx: i,
|
||
id: None,
|
||
paint_rect: rect,
|
||
handlers,
|
||
keyboard_focusable: true,
|
||
}
|
||
} ).collect()
|
||
}
|
||
|
||
// ── Benches ───────────────────────────────────────────────────────────────────
|
||
|
||
/// Pointer hit testing on a hot path. Three sub-benches:
|
||
/// - last: hit the last widget (worst case under reverse iteration is the
|
||
/// *first* widget; the helper iterates from the end to honour
|
||
/// "topmost wins", so the last widget in slice order is the cheap
|
||
/// one and the first is the expensive one).
|
||
/// - first: hit the first widget — full-slice walk.
|
||
/// - miss: point that lies outside every rect — also a full-slice walk
|
||
/// but never returns early.
|
||
fn bench_find_widget_at( c: &mut Criterion )
|
||
{
|
||
let mut group = c.benchmark_group( "find_widget_at" );
|
||
|
||
for &n in &[ 10usize, 100, 1000 ]
|
||
{
|
||
let widgets = build_widgets( n );
|
||
|
||
// Hit on the last widget in slice order — `find_widget_at` walks in
|
||
// reverse, so this returns on the first iteration.
|
||
let last_y = ( n as f32 - 1.0 ) * 31.0 + 15.0;
|
||
group.bench_with_input( BenchmarkId::new( "hit_last", n ), &n, |b, _|
|
||
{
|
||
b.iter( ||
|
||
{
|
||
let p = Point { x: 50.0, y: last_y };
|
||
black_box( find_widget_at( black_box( &widgets ), black_box( p ) ) )
|
||
} );
|
||
} );
|
||
|
||
// Hit on the first widget — full reverse walk.
|
||
group.bench_with_input( BenchmarkId::new( "hit_first", n ), &n, |b, _|
|
||
{
|
||
b.iter( ||
|
||
{
|
||
let p = Point { x: 50.0, y: 15.0 };
|
||
black_box( find_widget_at( black_box( &widgets ), black_box( p ) ) )
|
||
} );
|
||
} );
|
||
|
||
// Miss — point sits to the right of every rect.
|
||
group.bench_with_input( BenchmarkId::new( "miss", n ), &n, |b, _|
|
||
{
|
||
b.iter( ||
|
||
{
|
||
let p = Point { x: 9_999.0, y: 9_999.0 };
|
||
black_box( find_widget_at( black_box( &widgets ), black_box( p ) ) )
|
||
} );
|
||
} );
|
||
}
|
||
|
||
group.finish();
|
||
}
|
||
|
||
/// `flat_idx` lookup. Linear scan; we measure the worst case (last entry)
|
||
/// since it's what bounds latency on real frames.
|
||
fn bench_find_widget( c: &mut Criterion )
|
||
{
|
||
let mut group = c.benchmark_group( "find_widget" );
|
||
|
||
for &n in &[ 10usize, 100, 1000 ]
|
||
{
|
||
let widgets = build_widgets( n );
|
||
let target = n - 1;
|
||
|
||
group.bench_with_input( BenchmarkId::new( "last", n ), &n, |b, _|
|
||
{
|
||
b.iter( ||
|
||
{
|
||
black_box( find_widget( black_box( &widgets ), black_box( target ) ) )
|
||
} );
|
||
} );
|
||
}
|
||
|
||
group.finish();
|
||
}
|
||
|
||
/// Same shape as `find_widget` but exercises the handler-cloning path on hit.
|
||
/// Useful as a separate measurement because the clone cost grows with the
|
||
/// handler payload, even though the scan itself does not.
|
||
fn bench_find_handlers( c: &mut Criterion )
|
||
{
|
||
let mut group = c.benchmark_group( "find_handlers" );
|
||
|
||
for &n in &[ 10usize, 100, 1000 ]
|
||
{
|
||
let widgets = build_widgets( n );
|
||
let target = n - 1;
|
||
|
||
group.bench_with_input( BenchmarkId::new( "last", n ), &n, |b, _|
|
||
{
|
||
b.iter( ||
|
||
{
|
||
black_box( find_handlers( black_box( &widgets ), black_box( target ) ) )
|
||
} );
|
||
} );
|
||
}
|
||
|
||
group.finish();
|
||
}
|
||
|
||
criterion_group!( benches, bench_find_widget_at, bench_find_widget, bench_find_handlers );
|
||
criterion_main!( benches );
|