Tests: frame budgets take turns and keep the best of three rounds

The two budget tests ran in parallel, each with its own render workers, and
a single preempted stretch on a shared CI runner pushed the average over the
40 ms budget (41-52 ms on Ubuntu and Windows). They now hold a lock while
measuring, and a round over budget is measured again (up to three rounds):
noise only adds time, so the best round is the UI's own cost and a real
regression still fails every round. The budget is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-04 14:44:50 -04:00
parent f5e3255b80
commit 9ecfd0afbc
1 changed files with 39 additions and 22 deletions

View File

@ -28,6 +28,36 @@ fn overloaded() -> bool {
}
}
/// The two budget tests take turns: each starts render workers, and measuring one while the other
/// runs would charge it for the other's work on a small CI machine.
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// Generous for debug builds and shared CI machines; release builds are ~10× faster.
const BUDGET: std::time::Duration = std::time::Duration::from_millis(40);
/// The average frame over `frames` steps, best of up to three rounds: another process taking the
/// CPU only ever adds time, so the fastest round is the closest to the UI's own cost, and a real
/// regression is over budget in every round. Later rounds run only when a round is over budget.
fn best_average(label: &str, frames: usize, mut step: impl FnMut(usize)) -> std::time::Duration {
let mut best = std::time::Duration::MAX;
for round in 1..=3 {
let start = std::time::Instant::now();
let mut worst = std::time::Duration::ZERO;
for f in 0..frames {
let t = std::time::Instant::now();
step(f);
worst = worst.max(t.elapsed());
}
let avg = start.elapsed() / frames as u32;
eprintln!("PERF {label} round {round}: avg frame {avg:?}, worst {worst:?}");
best = best.min(avg);
if best < BUDGET {
break;
}
}
best
}
/// `n` text pages with a few comments each, so panels and overlays have work to do.
fn big(n: usize) -> Vec<u8> {
let mut objs: Vec<String> = vec!["<< /Type /Catalog /Pages 2 0 R >>".into()];
@ -65,6 +95,7 @@ fn scrolling_a_500_page_document_stays_within_the_frame_budget() {
if overloaded() {
return;
}
let _turn = SERIAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let bytes = big(500);
let t0 = std::time::Instant::now();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
@ -73,21 +104,13 @@ fn scrolling_a_500_page_document_stays_within_the_frame_budget() {
app
});
h.run_steps(4);
let open = t0.elapsed();
eprintln!("PERF open {:?}", t0.elapsed());
// Scroll through the document: 200 frames, jumping ~2 pages per frame.
let frames = 200;
let mut worst = std::time::Duration::ZERO;
let start = std::time::Instant::now();
for f in 0..frames {
let avg = best_average("scroll", 200, |f| {
h.state_mut().views[0].goto = Some(((f * 5) / 2 % 500, 0.3));
let t = std::time::Instant::now();
h.step();
worst = worst.max(t.elapsed());
}
let avg = start.elapsed() / frames as u32;
eprintln!("PERF open {open:?}, avg frame {avg:?}, worst {worst:?}");
// Generous for debug builds and shared CI machines; release builds are ~10× faster.
assert!(avg < std::time::Duration::from_millis(40), "average frame {avg:?}");
});
assert!(avg < BUDGET, "average frame {avg:?}");
}
#[test]
@ -95,6 +118,7 @@ fn panels_with_hundreds_of_items_stay_within_the_frame_budget() {
if overloaded() {
return;
}
let _turn = SERIAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let bytes = big(500);
for panel in ["comments", "pages", "fields"] {
let b = bytes.clone();
@ -105,17 +129,10 @@ fn panels_with_hundreds_of_items_stay_within_the_frame_budget() {
app
});
h.run_steps(4);
let frames = 100;
let start = std::time::Instant::now();
let mut worst = std::time::Duration::ZERO;
for f in 0..frames {
let avg = best_average(panel, 100, |f| {
h.state_mut().views[0].goto = Some(((f * 5) % 500, 0.3));
let t = std::time::Instant::now();
h.step();
worst = worst.max(t.elapsed());
}
let avg = start.elapsed() / frames as u32;
eprintln!("PERF {panel}: avg frame {avg:?}, worst {worst:?}");
assert!(avg < std::time::Duration::from_millis(40), "{panel}: average frame {avg:?}");
});
assert!(avg < BUDGET, "{panel}: average frame {avg:?}");
}
}