use std::time::{Duration, Instant};
use slate_framework::{AnyElement, Color, Div, HeadlessApp, IntoAny, Text};
use slate_reactive::{Runtime, Signal};
const WARMUP_FRAMES: usize = 5;
const MEASURE_FRAMES: usize = 20;
macro_rules! headless_app_or_skip {
($width:expr, $height:expr) => {
match HeadlessApp::new($width, $height) {
Ok(app) => app,
Err(e) => {
eprintln!("Skipping benchmark (no GPU): {}", e);
return;
}
}
};
}
fn measure_frame_times<F>(
app: &mut HeadlessApp,
build_element: F,
warmup: usize,
measure: usize,
) -> Vec<Duration>
where
F: Fn() -> AnyElement,
{
for _ in 0..warmup {
let root = build_element();
let _ = app.render(root);
}
let mut times = Vec::with_capacity(measure);
for _ in 0..measure {
let root = build_element();
let start = Instant::now();
let _ = app.render(root);
times.push(start.elapsed());
}
times
}
fn avg_duration(durations: &[Duration]) -> Duration {
let total: Duration = durations.iter().sum();
total / durations.len() as u32
}
fn median_duration(durations: &[Duration]) -> Duration {
let mut sorted = durations.to_vec();
sorted.sort();
sorted[sorted.len() / 2]
}
fn p99_duration(durations: &[Duration]) -> Duration {
let mut sorted = durations.to_vec();
sorted.sort();
sorted[(sorted.len() * 99) / 100]
}
fn counter_app_tree() -> AnyElement {
Div::new()
.background(Color::from_hex("#1e1e2e").unwrap_or(Color::BLACK))
.style(|s| s.flex_grow(1.0).padding_all(32.0).gap(16.0))
.child(
Text::new("Reactive Counter")
.font_size(24.0)
.color(Color::WHITE.into()),
)
.child(
Div::new()
.background(Color::from_hex("#3b82f6").unwrap_or(Color::BLUE))
.corner_radius(8.0)
.style(|s| s.padding_all(16.0))
.child(
Text::new("Count: 42")
.font_size(32.0)
.color(Color::WHITE.into()),
),
)
.child(
Text::new("Increments every second via background task")
.font_size(14.0)
.color(Color::from_hex("#94a3b8").unwrap_or(Color::WHITE).into()),
)
.into_any()
}
fn large_tree_1000() -> AnyElement {
let mut div = Div::new()
.background(Color::BLACK)
.style(|s| s.flex_grow(1.0));
for i in 0..1000 {
div = div.child(
Text::new(format!("Item {}", i))
.font_size(10.0)
.color(Color::WHITE.into()),
);
}
div.into_any()
}
#[test]
fn bench_idle_frame_time() {
let mut app = headless_app_or_skip!(400, 300);
let times = measure_frame_times(&mut app, counter_app_tree, WARMUP_FRAMES, MEASURE_FRAMES);
let avg = avg_duration(×);
let median = median_duration(×);
let p99 = p99_duration(×);
println!("\n=== Idle Frame Time (counter app, no signal changes) ===");
println!(" Avg: {:?}", avg);
println!(" Median: {:?}", median);
println!(" P99: {:?}", p99);
println!(" Target: ≤ 1.5 ms");
if median > Duration::from_micros(1500) {
println!(" Note: Above target — headless overhead, file follow-up if persistent");
}
}
#[test]
fn bench_cache_hit_rate() {
let mut app = headless_app_or_skip!(400, 300);
for _ in 0..100 {
let _ = app.render(counter_app_tree());
}
let hits = app.text_shaping_cache_hits();
let misses = app.text_shaping_cache_misses();
let total = hits + misses;
let hit_rate = (hits as f64 / total as f64) * 100.0;
println!("\n=== TextShapingCache Hit Rate ===");
println!(" Hits: {}", hits);
println!(" Misses: {}", misses);
println!(" Rate: {:.1}%", hit_rate);
println!(" Target: ≥ 80%");
assert!(
hit_rate >= 80.0,
"cache hit rate {:.1}% below 80% target",
hit_rate
);
}
#[test]
#[cfg_attr(target_os = "windows", ignore)] fn bench_1000_element_tree() {
let mut app = headless_app_or_skip!(800, 600);
let times = measure_frame_times(&mut app, large_tree_1000, 3, 10);
let avg = avg_duration(×);
let median = median_duration(×);
println!("\n=== 1000-Element Tree Frame Time ===");
println!(" Avg: {:?}", avg);
println!(" Median: {:?}", median);
println!(" Note: Includes layout + paint + GPU submit");
}
#[test]
#[cfg_attr(target_os = "windows", ignore)] fn bench_1000_element_cached() {
let mut app = headless_app_or_skip!(800, 600);
let _ = app.render(large_tree_1000());
let initial_hits = app.text_shaping_cache_hits();
let initial_misses = app.text_shaping_cache_misses();
let times = measure_frame_times(&mut app, large_tree_1000, 0, 10);
let hits = app.text_shaping_cache_hits() - initial_hits;
let misses = app.text_shaping_cache_misses() - initial_misses;
let hit_rate = if hits + misses > 0 {
(hits as f64 / (hits + misses) as f64) * 100.0
} else {
0.0
};
let avg = avg_duration(×);
let median = median_duration(×);
println!("\n=== 1000-Element Tree (Cached) ===");
println!(" Avg frame: {:?}", avg);
println!(" Median: {:?}", median);
println!(" Cache hits: {} ({:.1}%)", hits, hit_rate);
}
#[test]
fn bench_set_driven_frame() {
let mut app = headless_app_or_skip!(400, 300);
let rt = Runtime::new();
let signal = Signal::new(rt.clone(), 0u32);
for _ in 0..WARMUP_FRAMES {
let _ = app.render(counter_app_tree());
}
let mut times = Vec::with_capacity(MEASURE_FRAMES);
for i in 0..MEASURE_FRAMES {
signal.set(i as u32);
let start = Instant::now();
let _ = app.render(counter_app_tree());
times.push(start.elapsed());
}
let avg = avg_duration(×);
let median = median_duration(×);
println!("\n=== Set-Driven Frame Time ===");
println!(" Avg: {:?}", avg);
println!(" Median: {:?}", median);
println!(" Target: ≤ 1.5 ms");
if median > Duration::from_micros(1500) {
println!(" Note: Above target — headless overhead, file follow-up if persistent");
}
}