use std::{
cell::{OnceCell, RefCell},
future::Future,
rc::Rc,
sync::Arc,
time::Duration,
};
use anyhow::{Result, anyhow};
use hdrhistogram::Histogram;
use crate::{
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, Bounds, Context, Empty,
Entity, EntityId, Focusable, ForegroundExecutor, Global, Platform, PlatformHeadlessRenderer,
PlatformTextSystem, Render, Reservation, Task, TestPlatform, ThreadedDispatcher, VisualContext,
Window, WindowBounds, WindowHandle, WindowOptions,
app::GpuiBorrow,
profiler::{self, FrameTiming, FrameTimingCollector},
};
pub fn bench_platform(
headless_renderer_factory: Option<Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>>>,
text_system: Arc<dyn PlatformTextSystem>,
) -> Rc<dyn Platform> {
thread_local! {
static DISPATCHER: OnceCell<Arc<ThreadedDispatcher>> = const { OnceCell::new() };
}
let dispatcher = DISPATCHER.with(|cell| {
cell.get_or_init(|| Arc::new(ThreadedDispatcher::new()))
.clone()
});
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
TestPlatform::with_platform(
background_executor,
foreground_executor,
text_system,
headless_renderer_factory,
) as Rc<dyn Platform>
}
const DEFAULT_FPS: u64 = 120;
const NANOS_PER_SECOND: u128 = 1_000_000_000;
#[derive(Clone)]
pub struct BenchReport {
frame_snapshot: Rc<RefCell<WindowFrameSnapshot>>,
frame_budget_nanos: u128,
}
impl Default for BenchReport {
fn default() -> Self {
Self::with_fps(DEFAULT_FPS)
}
}
impl BenchReport {
pub fn with_fps(fps: u64) -> Self {
assert!(fps > 0, "frame rate must be greater than zero");
Self::with_frame_budget_nanos(NANOS_PER_SECOND / fps as u128)
}
pub fn with_frame_budget_nanos(frame_budget_nanos: u128) -> Self {
Self {
frame_snapshot: Rc::new(RefCell::new(WindowFrameSnapshot::new())),
frame_budget_nanos,
}
}
fn record_frame_timings<'i>(&self, timings: impl IntoIterator<Item = &'i FrameTiming>) {
let mut snapshot = self.frame_snapshot.borrow_mut();
for timing in timings {
snapshot
.draw
.record(timing.draw_duration().as_nanos() as u64)
.ok();
if let Some(dirty_to_draw) = timing.dirty_to_draw_duration() {
snapshot
.dirty_to_draw
.record(dirty_to_draw.as_nanos() as u64)
.ok();
}
if timing.invalidations > 0 {
snapshot
.invalidations_per_frame
.record(timing.invalidations)
.ok();
}
}
}
fn total_budget_overruns(&self, histogram: &Histogram<u64>) -> u64 {
histogram
.iter_recorded()
.map(|value| {
self.budget_overruns(Duration::from_nanos(value.value_iterated_to()))
* value.count_at_value()
})
.sum()
}
fn budget_overruns(&self, foreground_time: Duration) -> u64 {
let foreground_nanos = foreground_time.as_nanos();
if foreground_nanos <= self.frame_budget_nanos {
return 0;
}
let over_budget_nanos = foreground_nanos - self.frame_budget_nanos;
over_budget_nanos.div_ceil(self.frame_budget_nanos) as u64
}
pub fn print(&self, benchmark_name: Option<&'static str>) {
let frame_snapshot = self.frame_snapshot.borrow();
if frame_snapshot.is_empty() {
return;
}
let benchmark_name = benchmark_name.unwrap_or("unknown benchmark");
eprintln!("GPUI bench report (all observed iterations): {benchmark_name}");
eprintln!(" note: includes Criterion warmup/calibration");
self.print_histogram("window dirty-to-draw", &frame_snapshot.dirty_to_draw);
self.print_histogram("window draw", &frame_snapshot.draw);
if !frame_snapshot.invalidations_per_frame.is_empty() {
eprintln!(
" invalidations per frame: mean {:.2}, max {}",
frame_snapshot.invalidations_per_frame.mean(),
frame_snapshot.invalidations_per_frame.max()
);
}
}
fn print_histogram(&self, name: &str, histogram: &Histogram<u64>) {
if histogram.is_empty() {
return;
}
let max_foreground_time = Duration::from_nanos(histogram.max());
eprintln!(" {name}:");
eprintln!(" samples: {}", histogram.len());
eprintln!(
" mean: {}",
format_duration(Duration::from_nanos(histogram.mean() as u64))
);
eprintln!(
" p50: {}",
format_duration(Duration::from_nanos(histogram.value_at_quantile(0.50)))
);
eprintln!(
" p90: {}",
format_duration(Duration::from_nanos(histogram.value_at_quantile(0.90)))
);
eprintln!(
" p95: {}",
format_duration(Duration::from_nanos(histogram.value_at_quantile(0.95)))
);
eprintln!(
" p99: {}",
format_duration(Duration::from_nanos(histogram.value_at_quantile(0.99)))
);
eprintln!(" max: {}", format_duration(max_foreground_time));
eprintln!(
" frame budget overruns total: {}",
self.total_budget_overruns(histogram)
);
eprintln!(
" frame budget overruns max: {}",
self.budget_overruns(max_foreground_time)
);
}
}
struct WindowFrameSnapshot {
dirty_to_draw: Histogram<u64>,
draw: Histogram<u64>,
invalidations_per_frame: Histogram<u64>,
}
impl WindowFrameSnapshot {
fn new() -> Self {
Self {
dirty_to_draw: Histogram::new(3).expect("3 significant digits is valid"),
draw: Histogram::new(3).expect("3 significant digits is valid"),
invalidations_per_frame: Histogram::new(3).expect("3 significant digits is valid"),
}
}
fn is_empty(&self) -> bool {
self.dirty_to_draw.is_empty() && self.draw.is_empty()
}
}
fn format_duration(duration: Duration) -> String {
format!("{:.3}ms", duration.as_secs_f64() * 1000.)
}
struct FrameTraceScope {
collector: FrameTimingCollector,
was_already_enabled: bool,
}
impl FrameTraceScope {
fn start() -> Self {
let was_already_enabled = !profiler::set_frame_trace_enabled(true);
Self {
collector: FrameTimingCollector::new(),
was_already_enabled,
}
}
fn finish(mut self) -> Vec<FrameTiming> {
self.collector.collect_unseen()
}
}
impl Drop for FrameTraceScope {
fn drop(&mut self) {
if !self.was_already_enabled {
profiler::set_frame_trace_enabled(false);
}
}
}
struct MeasuredTaskInput<Input> {
input: Input,
frame_trace_scope: Option<FrameTraceScope>,
}
struct MeasuredTaskOutput<Output> {
frame_trace_scope: Option<FrameTraceScope>,
report: BenchReport,
_output: Output,
}
impl<Output> Drop for MeasuredTaskOutput<Output> {
fn drop(&mut self) {
let frame_trace_scope = self
.frame_trace_scope
.take()
.expect("measured task output should retain its frame trace scope");
self.report
.record_frame_timings(frame_trace_scope.finish().iter());
}
}
fn run_task_to_completion<Output>(
foreground_executor: &ForegroundExecutor,
task: Task<Output>,
) -> Output
where
Output: 'static,
{
let output = Rc::new(RefCell::new(None));
foreground_executor
.spawn({
let output = output.clone();
async move {
*output.borrow_mut() = Some(task.await);
}
})
.detach();
foreground_executor
.dispatcher()
.as_threaded()
.expect("BenchAppContext requires a ThreadedDispatcher")
.run_until(|| output.borrow_mut().take())
}
#[derive(Clone)]
pub struct BenchAppContext<'a, 'measurement> {
app: Rc<AppCell>,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
benchmark_name: Option<&'static str>,
bencher: Rc<RefCell<Option<&'a mut criterion::Bencher<'measurement>>>>,
report: BenchReport,
}
impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
pub fn new(
platform: Rc<dyn Platform>,
benchmark_name: Option<&'static str>,
bencher: &'a mut criterion::Bencher<'measurement>,
) -> Self {
Self::build(platform, benchmark_name, bencher, BenchReport::default())
}
#[doc(hidden)]
pub fn new_with_platform_and_report(
platform: Rc<dyn Platform>,
benchmark_name: Option<&'static str>,
bencher: &'a mut criterion::Bencher<'measurement>,
report: BenchReport,
) -> Self {
Self::build(platform, benchmark_name, bencher, report)
}
fn build(
platform: Rc<dyn Platform>,
benchmark_name: Option<&'static str>,
bencher: &'a mut criterion::Bencher<'measurement>,
report: BenchReport,
) -> Self {
let background_executor = platform.background_executor();
assert!(
background_executor.dispatcher().as_threaded().is_some(),
"BenchAppContext requires a platform whose executors are backed by a \
ThreadedDispatcher; construct one with gpui::bench_platform"
);
let foreground_executor = platform.foreground_executor();
let asset_source = Arc::new(());
let http_client = http_client::FakeHttpClient::with_404_response();
let app = App::new_app(platform, asset_source, http_client);
Self {
app,
background_executor,
foreground_executor,
benchmark_name,
bencher: Rc::new(RefCell::new(Some(bencher))),
report,
}
}
pub fn benchmark_name(&self) -> Option<&'static str> {
self.benchmark_name
}
pub fn background_executor(&self) -> &BackgroundExecutor {
&self.background_executor
}
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
pub fn update<R>(&mut self, update: impl FnOnce(&mut App) -> R) -> R {
let mut app = self.app.borrow_mut();
app.update(update)
}
pub fn read<R>(&self, read: impl FnOnce(&App) -> R) -> R {
let app = self.app.borrow();
read(&app)
}
pub fn run_until_idle(&self) {
self.background_executor
.dispatcher()
.as_threaded()
.expect("validated in BenchAppContext::build")
.run_until_idle();
}
pub fn bench_iter(&mut self, mut benchmark: impl FnMut(&mut Self)) {
let bencher = self.take_bencher("bench_iter");
let collector = FrameTraceScope::start();
let mut benchmark = || benchmark(self);
bencher.iter(&mut benchmark);
self.report.record_frame_timings(collector.finish().iter());
self.replace_bencher(bencher);
}
pub fn bench_task<Output>(&mut self, mut benchmark: impl FnMut(&mut Self) -> Task<Output>)
where
Output: 'static,
{
self.bench_batched_task_internal("bench_task", |_| (), |_, cx| benchmark(cx));
}
pub fn bench_batched_task<Input, Output>(
&mut self,
setup: impl FnMut(&mut Self) -> Input,
benchmark: impl FnMut(&mut Input, &mut Self) -> Task<Output>,
) where
Output: 'static,
{
self.bench_batched_task_internal("bench_batched_task", setup, benchmark);
}
fn bench_batched_task_internal<Input, Output>(
&mut self,
benchmark_kind: &str,
mut setup: impl FnMut(&mut Self) -> Input,
mut benchmark: impl FnMut(&mut Input, &mut Self) -> Task<Output>,
) where
Output: 'static,
{
let bencher = self.take_bencher(benchmark_kind);
let mut setup_context = self.clone();
let mut benchmark_context = self.clone();
let foreground_executor = self.foreground_executor.clone();
let report = self.report.clone();
bencher.iter_batched_ref(
|| MeasuredTaskInput {
input: setup(&mut setup_context),
frame_trace_scope: Some(FrameTraceScope::start()),
},
|measured_input| {
let task = benchmark(&mut measured_input.input, &mut benchmark_context);
let output = run_task_to_completion(&foreground_executor, task);
MeasuredTaskOutput {
frame_trace_scope: measured_input.frame_trace_scope.take(),
report: report.clone(),
_output: output,
}
},
criterion::BatchSize::PerIteration,
);
self.replace_bencher(bencher);
}
pub fn bench_renderer<V>(
&mut self,
view: Entity<V>,
mut update: impl FnMut(&mut V, &mut Window, &mut Context<V>),
) where
V: 'static + Render,
{
let bencher = self.take_bencher("bench_renderer");
let window_id = self
.with_window(view.entity_id(), |window, _| {
window.window_handle().window_id()
})
.expect("cannot benchmark renderer for entity without a current window");
let dispatcher = self.background_executor.dispatcher().clone();
let collector = FrameTraceScope::start();
let mut benchmark = || {
dispatcher
.as_threaded()
.expect("validated in BenchAppContext::build")
.run_ready_main_tasks();
self.with_window(view.entity_id(), |window, cx| {
view.update(cx, |view, cx| update(view, window, cx));
})
.expect("cannot benchmark renderer for entity without a current window");
self.with_window(view.entity_id(), |window, _| {
window.present_if_needed();
})
.expect("cannot benchmark renderer for entity without a current window");
};
bencher.iter(&mut benchmark);
let timings = collector.finish();
self.report.record_frame_timings(
timings
.iter()
.filter(|timing| timing.window_id == window_id),
);
self.replace_bencher(bencher);
}
pub fn add_empty_window(&mut self) -> BenchWindowContext<'a, 'measurement> {
let bounds = {
let app = self.app.borrow();
Bounds::maximized(None, &app)
};
let window = {
let mut app = self.app.borrow_mut();
let window: AnyWindowHandle = app
.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| Empty),
)
.expect("failed to open benchmark window")
.into();
window
};
self.run_until_idle();
BenchWindowContext {
cx: self.clone(),
window,
}
}
fn take_bencher(&self, benchmark_kind: &str) -> &'a mut criterion::Bencher<'measurement> {
self.bencher.borrow_mut().take().unwrap_or_else(|| {
panic!("cannot start {benchmark_kind}: benchmark measurement is already running")
})
}
fn replace_bencher(&self, bencher: &'a mut criterion::Bencher<'measurement>) {
let previous = self.bencher.borrow_mut().replace(bencher);
assert!(
previous.is_none(),
"benchmark bencher was unexpectedly present after measurement"
);
}
pub fn teardown(mut self) {
self.run_until_idle();
self.update(|cx| {
cx.quit();
});
self.run_until_idle();
let dispatcher = self.background_executor.dispatcher();
let dispatcher = dispatcher
.as_threaded()
.expect("validated in BenchAppContext::build");
drop(self.app);
drop(self.foreground_executor);
for _ in 0..100 {
if dispatcher.cancel_pending_timers() == 0 {
return;
}
dispatcher.run_until_idle();
}
panic!(
"benchmark teardown kept scheduling timers: {}",
dispatcher.debug_state()
);
}
}
impl AppContext for BenchAppContext<'_, '_> {
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
let mut app = self.app.borrow_mut();
app.new(build_entity)
}
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
let mut app = self.app.borrow_mut();
app.reserve_entity()
}
fn insert_entity<T: 'static>(
&mut self,
reservation: Reservation<T>,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Entity<T> {
let mut app = self.app.borrow_mut();
app.insert_entity(reservation, build_entity)
}
fn update_entity<T: 'static, R>(
&mut self,
handle: &Entity<T>,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> R {
let mut app = self.app.borrow_mut();
app.update_entity(handle, update)
}
fn as_mut<'b, T>(&'b mut self, _: &Entity<T>) -> GpuiBorrow<'b, T>
where
T: 'static,
{
panic!("Cannot use as_mut with BenchAppContext. Call update() instead.")
}
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
where
T: 'static,
{
let app = self.app.borrow();
app.read_entity(handle, read)
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<T>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
let mut app = self.app.borrow_mut();
app.update_window(window, update)
}
fn with_window<R>(
&mut self,
entity_id: EntityId,
update: impl FnOnce(&mut Window, &mut App) -> R,
) -> Option<R> {
let mut app = self.app.borrow_mut();
app.with_window(entity_id, update)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(Entity<T>, &App) -> R,
) -> Result<R>
where
T: 'static,
{
let app = self.app.borrow();
app.read_window(window, read)
}
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.background_executor.spawn(future)
}
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
where
G: Global,
{
let app = self.app.borrow();
app.read_global(callback)
}
}
#[derive(Clone)]
pub struct BenchWindowContext<'a, 'measurement> {
cx: BenchAppContext<'a, 'measurement>,
window: AnyWindowHandle,
}
impl<'a, 'measurement> BenchWindowContext<'a, 'measurement> {
pub fn app_context(&mut self) -> &mut BenchAppContext<'a, 'measurement> {
&mut self.cx
}
pub fn window_handle(&self) -> AnyWindowHandle {
self.window
}
pub fn run_until_idle(&self) {
self.cx.run_until_idle();
}
pub fn update<R>(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> R {
self.cx
.update_window(self.window, |_, window, cx| update(window, cx))
.expect("benchmark window was unexpectedly closed")
}
}
impl AppContext for BenchWindowContext<'_, '_> {
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
self.window
.update(&mut self.cx, |_, _, cx| cx.new(build_entity))
.expect("benchmark window was unexpectedly closed")
}
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
self.cx.reserve_entity()
}
fn insert_entity<T: 'static>(
&mut self,
reservation: Reservation<T>,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Entity<T> {
self.window
.update(&mut self.cx, |_, _, cx| {
cx.insert_entity(reservation, build_entity)
})
.expect("benchmark window was unexpectedly closed")
}
fn update_entity<T: 'static, R>(
&mut self,
handle: &Entity<T>,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> R {
self.cx.update_entity(handle, update)
}
fn as_mut<'b, T>(&'b mut self, handle: &Entity<T>) -> GpuiBorrow<'b, T>
where
T: 'static,
{
self.cx.as_mut(handle)
}
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
where
T: 'static,
{
self.cx.read_entity(handle, read)
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<T>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
self.cx.update_window(window, update)
}
fn with_window<R>(
&mut self,
entity_id: EntityId,
update: impl FnOnce(&mut Window, &mut App) -> R,
) -> Option<R> {
self.cx.with_window(entity_id, update)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(Entity<T>, &App) -> R,
) -> Result<R>
where
T: 'static,
{
self.cx.read_window(window, read)
}
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.cx.background_spawn(future)
}
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
where
G: Global,
{
self.cx.read_global(callback)
}
}
impl VisualContext for BenchWindowContext<'_, '_> {
type Result<T> = Result<T>;
fn window_handle(&self) -> AnyWindowHandle {
self.window
}
fn update_window_entity<T: 'static, R>(
&mut self,
entity: &Entity<T>,
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
) -> Result<R> {
let entity = entity.clone();
self.cx
.app
.borrow_mut()
.with_window(entity.entity_id(), |window, app| {
entity.update(app, |entity, cx| update(entity, window, cx))
})
.ok_or_else(|| {
anyhow!("entity has no current window; use `update` instead of `update_in`")
})
}
fn new_window_entity<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
) -> Result<Entity<T>> {
self.window.update(&mut self.cx, |_, window, cx| {
cx.new(|cx| build_entity(window, cx))
})
}
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
) -> Result<Entity<V>>
where
V: 'static + Render,
{
self.window.update(&mut self.cx, |_, window, cx| {
window.replace_root(cx, build_view)
})
}
fn focus<V>(&mut self, entity: &Entity<V>) -> Result<()>
where
V: Focusable,
{
self.window.update(&mut self.cx, |_, window, cx| {
entity.read(cx).focus_handle(cx).focus(window, cx)
})
}
}
#[cfg(test)]
mod tests {
use std::{rc::Rc, sync::Arc};
use super::*;
#[test]
fn task_completion_supports_non_send_foreground_output() {
let dispatcher = Arc::new(ThreadedDispatcher::new());
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let (sender, receiver) = futures::channel::oneshot::channel();
background_executor
.spawn(async move {
sender
.send(())
.expect("foreground receiver should remain alive");
})
.detach();
let expected_output = Rc::new(42);
let task_output = expected_output.clone();
let task = foreground_executor.spawn(async move {
receiver.await.expect("background task should send a value");
task_output
});
let output = run_task_to_completion(&foreground_executor, task);
assert!(
Rc::ptr_eq(&output, &expected_output),
"task runner should preserve non-Send foreground output"
);
}
}