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, FrameEvent, FrameTimingCollector,
journal::{ForegroundEvent, ForegroundJournalCollector, ForegroundJournalEntry},
},
};
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, Copy, Debug)]
pub struct ForegroundWorkSummary {
pub count: u64,
pub total: Duration,
pub max: Duration,
pub p50: Duration,
pub p90: Duration,
pub p95: Duration,
pub p99: Duration,
pub frame_budget_overruns_total: u64,
pub frame_budget_overruns_max: u64,
}
#[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, events: impl IntoIterator<Item = &'i FrameEvent>) {
let mut snapshot = self.frame_snapshot.borrow_mut();
for event in events {
match event {
FrameEvent::Draw(timing) => {
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();
}
}
FrameEvent::Present(timing) => {
if let Some(animation_interval) = timing.animation_interval {
snapshot
.present_interval
.record(animation_interval.as_nanos() as u64)
.ok();
}
}
}
}
}
fn record_foreground_events<'i>(&self, events: impl IntoIterator<Item = &'i ForegroundEvent>) {
let mut snapshot = self.frame_snapshot.borrow_mut();
for event in events {
let duration = match event {
ForegroundEvent::Draw(_) | ForegroundEvent::Present(_) => continue,
ForegroundEvent::SmallPolls(flush) => flush.summary.total,
_ => event.duration(),
};
snapshot.foreground_work.record(duration);
}
}
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 foreground_work(&self) -> Option<ForegroundWorkSummary> {
let frame_snapshot = self.frame_snapshot.borrow();
let foreground_work = &frame_snapshot.foreground_work;
if foreground_work.histogram.is_empty() {
return None;
}
let max = Duration::from_nanos(foreground_work.histogram.max());
Some(ForegroundWorkSummary {
count: foreground_work.histogram.len(),
total: Duration::from_nanos(foreground_work.total_nanos),
max,
p50: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.50)),
p90: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.90)),
p95: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.95)),
p99: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.99)),
frame_budget_overruns_total: self.total_budget_overruns(&foreground_work.histogram),
frame_budget_overruns_max: self.budget_overruns(max),
})
}
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);
self.print_histogram("window present interval", &frame_snapshot.present_interval);
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()
);
}
self.print_foreground_work(&frame_snapshot.foreground_work);
}
fn print_histogram(&self, name: &str, histogram: &Histogram<u64>) {
if histogram.is_empty() {
return;
}
eprintln!(" {name}:");
self.print_histogram_body(histogram);
}
fn print_foreground_work(&self, foreground_work: &DurationHistogram) {
if foreground_work.histogram.is_empty() {
return;
}
eprintln!(" foreground executor work (task polls, actions, input dispatch):");
eprintln!(" note: excludes window draw/present, reported separately above");
eprintln!(
" total: {}",
format_duration(Duration::from_nanos(foreground_work.total_nanos))
);
self.print_histogram_body(&foreground_work.histogram);
}
fn print_histogram_body(&self, histogram: &Histogram<u64>) {
let max_foreground_time = Duration::from_nanos(histogram.max());
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>,
present_interval: Histogram<u64>,
invalidations_per_frame: Histogram<u64>,
foreground_work: DurationHistogram,
}
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"),
present_interval: Histogram::new(3).expect("3 significant digits is valid"),
invalidations_per_frame: Histogram::new(3).expect("3 significant digits is valid"),
foreground_work: DurationHistogram::new(),
}
}
fn is_empty(&self) -> bool {
self.dirty_to_draw.is_empty()
&& self.draw.is_empty()
&& self.present_interval.is_empty()
&& self.foreground_work.histogram.is_empty()
}
}
struct DurationHistogram {
histogram: Histogram<u64>,
total_nanos: u64,
}
impl DurationHistogram {
fn new() -> Self {
Self {
histogram: Histogram::new(3).expect("3 significant digits is valid"),
total_nanos: 0,
}
}
fn record(&mut self, duration: Duration) {
let nanos = duration.as_nanos() as u64;
self.histogram.record(nanos).ok();
self.total_nanos += nanos;
}
}
fn format_duration(duration: Duration) -> String {
format!("{:.3}ms", duration.as_secs_f64() * 1000.)
}
struct TraceScope {
collector: FrameTimingCollector,
journal_collector: ForegroundJournalCollector,
_trace_guard: profiler::TraceGuard,
}
impl TraceScope {
fn start(journal_collector: ForegroundJournalCollector) -> Self {
let trace_guard = profiler::trace_scope();
Self {
collector: FrameTimingCollector::new(),
journal_collector,
_trace_guard: trace_guard,
}
}
fn finish(mut self) -> TracedEvents {
TracedEvents {
frame_events: self.collector.collect_unseen(),
journal_entries: self.journal_collector.collect_unseen().entries,
}
}
}
struct TracedEvents {
frame_events: Vec<FrameEvent>,
journal_entries: Vec<ForegroundJournalEntry>,
}
impl TracedEvents {
fn foreground_events(&self) -> impl Iterator<Item = &ForegroundEvent> {
self.journal_entries.iter().filter_map(|entry| match entry {
ForegroundJournalEntry::Event(event) => Some(event),
_ => None,
})
}
}
struct MeasuredTaskInput<Input> {
input: Input,
trace_scope: Option<TraceScope>,
}
struct MeasuredTaskOutput<Output> {
trace_scope: Option<TraceScope>,
report: BenchReport,
_output: Output,
}
impl<Output> Drop for MeasuredTaskOutput<Output> {
fn drop(&mut self) {
let trace_scope = self
.trace_scope
.take()
.expect("measured task output should retain its trace scope");
let events = trace_scope.finish();
self.report.record_frame_timings(events.frame_events.iter());
self.report
.record_foreground_events(events.foreground_events());
}
}
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: Arc<dyn http_client::HttpClient> =
Arc::new(http_client::BlockedHttpClient::new());
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 settle(&mut self) {
let dispatcher = self.background_executor.dispatcher().clone();
let dispatcher = dispatcher
.as_threaded()
.expect("validated in BenchAppContext::build");
loop {
self.run_until_idle();
self.update(|_| ());
if dispatcher.is_idle() {
return;
}
}
}
pub fn run_until<R>(&self, ready: impl FnMut() -> Option<R>) -> R {
self.background_executor
.dispatcher()
.as_threaded()
.expect("validated in BenchAppContext::build")
.run_until(ready)
}
fn foreground_journal_collector(&self) -> ForegroundJournalCollector {
self.read(|app| app.foreground_journal().collector())
}
pub fn bench_iter(&mut self, mut benchmark: impl FnMut(&mut Self)) {
let bencher = self.take_bencher("bench_iter");
let collector = TraceScope::start(self.foreground_journal_collector());
let mut benchmark = || benchmark(self);
bencher.iter(&mut benchmark);
let events = collector.finish();
self.report.record_frame_timings(events.frame_events.iter());
self.report
.record_foreground_events(events.foreground_events());
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(
|| {
setup_context.settle();
MeasuredTaskInput {
input: setup(&mut setup_context),
trace_scope: Some(TraceScope::start(
setup_context.foreground_journal_collector(),
)),
}
},
|measured_input| {
let task = benchmark(&mut measured_input.input, &mut benchmark_context);
let output = run_task_to_completion(&foreground_executor, task);
MeasuredTaskOutput {
trace_scope: measured_input.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 = TraceScope::start(self.foreground_journal_collector());
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 events = collector.finish();
self.report
.record_frame_timings(events.frame_events.iter().filter(|event| match event {
FrameEvent::Draw(timing) => timing.window_id == window_id,
FrameEvent::Present(timing) => timing.window_id == window_id,
}));
self.report
.record_foreground_events(events.foreground_events());
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::*;
use crate::profiler::journal::install_test_foreground_journal;
#[test]
fn foreground_work_reports_long_task_without_window_draw() {
let (journal, _journal_guard) = install_test_foreground_journal(1024, 64);
let dispatcher = Arc::new(ThreadedDispatcher::new());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let trace_scope = TraceScope::start(journal.collector());
let task = foreground_executor.spawn(async move {
std::thread::sleep(Duration::from_millis(60));
});
run_task_to_completion(&foreground_executor, task);
let events = trace_scope.finish();
assert!(
events.frame_events.is_empty(),
"no window was involved, so no frame events should be recorded"
);
let report = BenchReport::default();
report.record_foreground_events(events.foreground_events());
let summary = report
.foreground_work()
.expect("a long task poll should be reported even without a window draw");
assert!(summary.count >= 1, "expected at least one recorded item");
assert!(
summary.max >= Duration::from_millis(55),
"expected the long poll's duration to be recorded, got {:?}",
summary.max
);
assert!(
summary.total >= Duration::from_millis(55),
"expected the long poll's duration to be included in the total, got {:?}",
summary.total
);
}
#[test]
fn foreground_work_excludes_setup_before_trace_scope_starts() {
let (journal, _journal_guard) = install_test_foreground_journal(1024, 64);
let dispatcher = Arc::new(ThreadedDispatcher::new());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let setup_task = foreground_executor.spawn(async move {
std::thread::sleep(Duration::from_millis(80));
});
run_task_to_completion(&foreground_executor, setup_task);
let trace_scope = TraceScope::start(journal.collector());
let measured_task = foreground_executor.spawn(async move {
std::thread::sleep(Duration::from_millis(10));
});
run_task_to_completion(&foreground_executor, measured_task);
let events = trace_scope.finish();
let report = BenchReport::default();
report.record_foreground_events(events.foreground_events());
let summary = report
.foreground_work()
.expect("the measured task's poll should be reported");
assert!(
summary.max < Duration::from_millis(40),
"setup work's 80ms poll must not leak into the measured summary, got {:?}",
summary.max
);
assert!(
summary.total < Duration::from_millis(40),
"setup work's 80ms poll must not leak into the measured total, got {:?}",
summary.total
);
}
#[test]
fn bench_task_reports_long_task_without_window() {
let platform = bench_platform(None, Arc::new(crate::NoopTextSystem::new()));
let report = BenchReport::default();
let name = "bench_task_reports_long_task_without_window";
let mut criterion = criterion::Criterion::default()
.without_plots()
.sample_size(10)
.warm_up_time(Duration::from_millis(1))
.measurement_time(Duration::from_millis(1));
criterion.bench_function(name, |bencher| {
let mut cx = BenchAppContext::new_with_platform_and_report(
platform.clone(),
Some(name),
bencher,
report.clone(),
);
cx.bench_task(|cx| {
cx.foreground_executor().spawn(async move {
std::thread::sleep(Duration::from_millis(20));
})
});
cx.teardown();
});
let summary = report
.foreground_work()
.expect("bench_task should report foreground work with no window involved");
assert!(
summary.max >= Duration::from_millis(15),
"expected a ~20ms task poll to be recorded, got {:?}",
summary.max
);
}
#[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"
);
}
}