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, BenchDispatcher, Bounds, Context, Empty, Entity, EntityId, Focusable, ForegroundExecutor, Global, Platform, PlatformHeadlessRenderer, PlatformTextSystem, Render, Reservation, Task, TestPlatform, VisualContext, Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiBorrow, profiler::{self, FrameTiming, FrameTimingCollector}, }; /// Returns a benchmark platform backed by this thread's shared dispatcher. /// /// The platform uses this thread's shared multithreaded [`BenchDispatcher`], so /// background work runs with production concurrency in real time. The dispatcher /// is cached per thread and reused across benchmark invocations so worker and /// timer threads persist for the whole process instead of being recreated for /// every Criterion calibration pass. /// /// Text is shaped with the provided platform text system. Benchmarks generated /// by `#[gpui::bench]` use the current platform's text system, so text-heavy /// benchmark measurements include production shaping and glyph rasterization. /// /// `headless_renderer_factory` supplies a renderer for benchmark windows, e.g. /// `gpui_platform::current_headless_renderer`. When present, scenes drawn by /// benchmarks are rasterized through the real sprite atlas and submitted to /// the GPU on present, so quad/sprite regressions show up in measurements. /// When `None`, presenting discards the scene. Currently only macOS provides /// a headless renderer (Metal), so GPU submission is excluded from benchmark /// measurements on other platforms. pub fn bench_platform( headless_renderer_factory: Option Option>>>, text_system: Arc, ) -> Rc { thread_local! { static DISPATCHER: OnceCell> = const { OnceCell::new() }; } let dispatcher = DISPATCHER.with(|cell| { cell.get_or_init(|| Arc::new(BenchDispatcher::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 } /// Default target frame rate when a benchmark doesn't specify `fps = N`. const DEFAULT_FPS: u64 = 120; const NANOS_PER_SECOND: u128 = 1_000_000_000; /// A small report produced by GPUI benchmarks. #[derive(Clone)] pub struct BenchReport { frame_snapshot: Rc>, frame_budget_nanos: u128, } impl Default for BenchReport { fn default() -> Self { Self::with_fps(DEFAULT_FPS) } } impl BenchReport { /// Creates a report whose per-frame budget is one frame at `fps` when /// counting frame budget overruns. 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) } /// Creates a report that treats `frame_budget_nanos` as the per-frame budget /// when counting frame budget overruns. 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) { let mut snapshot = self.frame_snapshot.borrow_mut(); // `.ok()` on `record`: this operation is infallible (the histograms auto-resize). 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 { histogram .iter_recorded() .map(|value| { self.budget_overruns(Duration::from_nanos(value.value_iterated_to())) * value.count_at_value() }) .sum() } /// Returns how many whole frame budgets `foreground_time` exceeded the /// per frame budget by. This is a synthetic proxy for missed frames: the /// benchmark harness has no vsync, so it counts how many frame deadlines /// would have elapsed while the foreground thread was busy. 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 } /// Prints this report to stderr. 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) { 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, draw: Histogram, invalidations_per_frame: Histogram, } 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.) } /// Enables frame tracing for the duration of a measurement and collects the /// frames recorded within it. The previous tracing state is restored on drop, /// so a panicking measurement doesn't leave tracing enabled for unrelated code /// (e.g. a later benchmark in the same process). 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 { self.collector.collect_unseen() // Dropping `self` restores the previous tracing state. } } impl Drop for FrameTraceScope { fn drop(&mut self) { if !self.was_already_enabled { profiler::set_frame_trace_enabled(false); } } } /// A GPUI app context for Criterion benchmarks. /// /// `BenchAppContext` is intentionally separate from `TestAppContext`: it owns a /// benchmark app instance and exposes only the app/window operations needed by /// benchmark setup. Criterion remains responsible for the measured loop via its /// `Bencher` API. #[derive(Clone)] pub struct BenchAppContext<'a, 'measurement> { app: Rc, background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, benchmark_name: Option<&'static str>, bencher: Rc>>>, report: BenchReport, } impl<'a, 'measurement> BenchAppContext<'a, 'measurement> { /// Creates a new benchmark app context backed by the provided platform. /// /// The platform's executors must be backed by a [`BenchDispatcher`] /// (see [`bench_platform`]) so the context can drain foreground work via /// [`Self::run_until_idle`]; panics otherwise. pub fn new( platform: Rc, benchmark_name: Option<&'static str>, bencher: &'a mut criterion::Bencher<'measurement>, ) -> Self { Self::build(platform, benchmark_name, bencher, BenchReport::default()) } /// Creates a new benchmark app context backed by the provided platform. /// /// The platform's executors must be backed by a [`BenchDispatcher`] /// (see [`bench_platform`]) so the context can drain foreground work via /// [`Self::run_until_idle`]; panics otherwise. #[doc(hidden)] pub fn new_with_platform_and_report( platform: Rc, 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, benchmark_name: Option<&'static str>, bencher: &'a mut criterion::Bencher<'measurement>, report: BenchReport, ) -> Self { let background_executor = platform.background_executor(); // Validate up front so misconfiguration fails at construction with a // clear message instead of deep inside `run_until_idle`. assert!( background_executor.dispatcher().as_bench().is_some(), "BenchAppContext requires a platform whose executors are backed by a \ BenchDispatcher; 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, } } /// The benchmark function name that created this context. pub fn benchmark_name(&self) -> Option<&'static str> { self.benchmark_name } /// Returns the background executor used by this benchmark app. pub fn background_executor(&self) -> &BackgroundExecutor { &self.background_executor } /// Returns the foreground executor used by this benchmark app. pub fn foreground_executor(&self) -> &ForegroundExecutor { &self.foreground_executor } /// Updates the app and flushes synchronous GPUI effects afterward. pub fn update(&mut self, update: impl FnOnce(&mut App) -> R) -> R { let mut app = self.app.borrow_mut(); app.update(update) } /// Reads app state. pub fn read(&self, read: impl FnOnce(&App) -> R) -> R { let app = self.app.borrow(); read(&app) } /// Runs queued foreground tasks on this thread and waits for in flight /// background work to finish. Timers that aren't due yet are not waited /// for (see [`BenchDispatcher::run_until_idle`]). pub fn run_until_idle(&self) { self.background_executor .dispatcher() .as_bench() .expect("validated in BenchAppContext::build") .run_until_idle(); } /// Measures a generic benchmark workload using Criterion's iteration loop. /// /// The closure is invoked once per Criterion iteration with this /// benchmark app context so it can update GPUI state. /// /// Any window draws triggered by the workload are recorded into the /// benchmark's frame report through the GPUI frame profiler. 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); } /// Measures frame latency after updating a GPUI entity in its current window. /// /// Each iteration runs `update` against the entity in its current window. In /// bench builds, flushing the update's effects synchronously draws dirty /// windows. The entity should be part of the window's render tree, such as the /// root view or a child of it. /// /// Frame timings are collected through the GPUI frame profiler /// ([`crate::profiler::record_frame_timing`]), which is enabled for the /// duration of the measurement. pub fn bench_renderer( &mut self, view: Entity, mut update: impl FnMut(&mut V, &mut Window, &mut Context), ) 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_bench() .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"); // Submit the frame drawn by the update's effect flush, mirroring // production where every drawn frame is presented. With a headless // renderer this includes scene submission to the GPU. 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); } /// Adds a window with an empty root view for benchmark setup. 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" ); } /// Runs GPUI benchmark teardown. /// /// Cancels any timers still armed on the shared dispatcher and drains the /// work that cancellation unblocks so they can't fire during a later /// benchmark; assumes no other `BenchAppContext` is live on this thread. 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_bench() .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(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { let mut app = self.app.borrow_mut(); app.new(build_entity) } fn reserve_entity(&mut self) -> Reservation { let mut app = self.app.borrow_mut(); app.reserve_entity() } fn insert_entity( &mut self, reservation: Reservation, build_entity: impl FnOnce(&mut Context) -> T, ) -> Entity { let mut app = self.app.borrow_mut(); app.insert_entity(reservation, build_entity) } fn update_entity( &mut self, handle: &Entity, update: impl FnOnce(&mut T, &mut Context) -> R, ) -> R { let mut app = self.app.borrow_mut(); app.update_entity(handle, update) } fn as_mut<'b, T>(&'b mut self, _: &Entity) -> GpuiBorrow<'b, T> where T: 'static, { panic!("Cannot use as_mut with BenchAppContext. Call update() instead.") } fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R where T: 'static, { let app = self.app.borrow(); app.read_entity(handle, read) } fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result where F: FnOnce(AnyView, &mut Window, &mut App) -> T, { let mut app = self.app.borrow_mut(); app.update_window(window, update) } fn with_window( &mut self, entity_id: EntityId, update: impl FnOnce(&mut Window, &mut App) -> R, ) -> Option { let mut app = self.app.borrow_mut(); app.with_window(entity_id, update) } fn read_window( &self, window: &WindowHandle, read: impl FnOnce(Entity, &App) -> R, ) -> Result where T: 'static, { let app = self.app.borrow(); app.read_window(window, read) } fn background_spawn(&self, future: impl Future + Send + 'static) -> Task where R: Send + 'static, { self.background_executor.spawn(future) } fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R where G: Global, { let app = self.app.borrow(); app.read_global(callback) } } /// A window-specific context for GPUI benchmarks. /// /// This is separate from `VisualTestContext`; it provides access to a benchmark /// window without exposing test-only helpers such as input simulation. #[derive(Clone)] pub struct BenchWindowContext<'a, 'measurement> { cx: BenchAppContext<'a, 'measurement>, window: AnyWindowHandle, } impl<'a, 'measurement> BenchWindowContext<'a, 'measurement> { /// Returns the underlying benchmark app context. pub fn app_context(&mut self) -> &mut BenchAppContext<'a, 'measurement> { &mut self.cx } /// Returns the window associated with this context. pub fn window_handle(&self) -> AnyWindowHandle { self.window } /// Runs queued foreground tasks on this thread and waits for in-flight /// background work to finish. Pending timers are not waited for. pub fn run_until_idle(&self) { self.cx.run_until_idle(); } /// Updates the benchmark window. pub fn update(&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(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { self.window .update(&mut self.cx, |_, _, cx| cx.new(build_entity)) .expect("benchmark window was unexpectedly closed") } fn reserve_entity(&mut self) -> Reservation { self.cx.reserve_entity() } fn insert_entity( &mut self, reservation: Reservation, build_entity: impl FnOnce(&mut Context) -> T, ) -> Entity { self.window .update(&mut self.cx, |_, _, cx| { cx.insert_entity(reservation, build_entity) }) .expect("benchmark window was unexpectedly closed") } fn update_entity( &mut self, handle: &Entity, update: impl FnOnce(&mut T, &mut Context) -> R, ) -> R { self.cx.update_entity(handle, update) } fn as_mut<'b, T>(&'b mut self, handle: &Entity) -> GpuiBorrow<'b, T> where T: 'static, { self.cx.as_mut(handle) } fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R where T: 'static, { self.cx.read_entity(handle, read) } fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result where F: FnOnce(AnyView, &mut Window, &mut App) -> T, { self.cx.update_window(window, update) } fn with_window( &mut self, entity_id: EntityId, update: impl FnOnce(&mut Window, &mut App) -> R, ) -> Option { self.cx.with_window(entity_id, update) } fn read_window( &self, window: &WindowHandle, read: impl FnOnce(Entity, &App) -> R, ) -> Result where T: 'static, { self.cx.read_window(window, read) } fn background_spawn(&self, future: impl Future + Send + 'static) -> Task where R: Send + 'static, { self.cx.background_spawn(future) } fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R where G: Global, { self.cx.read_global(callback) } } impl VisualContext for BenchWindowContext<'_, '_> { type Result = Result; fn window_handle(&self) -> AnyWindowHandle { self.window } fn update_window_entity( &mut self, entity: &Entity, update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, ) -> Result { 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( &mut self, build_entity: impl FnOnce(&mut Window, &mut Context) -> T, ) -> Result> { self.window.update(&mut self.cx, |_, window, cx| { cx.new(|cx| build_entity(window, cx)) }) } fn replace_root_view( &mut self, build_view: impl FnOnce(&mut Window, &mut Context) -> V, ) -> Result> where V: 'static + Render, { self.window.update(&mut self.cx, |_, window, cx| { window.replace_root(cx, build_view) }) } fn focus(&mut self, entity: &Entity) -> Result<()> where V: Focusable, { self.window.update(&mut self.cx, |_, window, cx| { entity.read(cx).focus_handle(cx).focus(window, cx) }) } }