781 lines · 26.1 KB · Rust
RawDownload
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<Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>>>,    text_system: Arc<dyn PlatformTextSystem>,) -> Rc<dyn Platform> {    thread_local! {        static DISPATCHER: OnceCell<Arc<BenchDispatcher>> = 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<dyn Platform>} /// 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<RefCell<WindowFrameSnapshot>>,    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<Item = &'i FrameTiming>) {        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>) -> 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<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.)} /// 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<FrameTiming> {        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<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> {    /// 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<dyn Platform>,        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<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();        // 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<R>(&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<R>(&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<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_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");

Lines 1–500 of 781.