|
| 1 | +//! `CAP_GPUI_AUTO_HOVER_BENCH=1` (with `CAP_GPUI_AUTO_EDITOR`): the editor's |
| 2 | +//! per-move cost while the pointer glides over a paused timeline. The pointer |
| 3 | +//! sweeps the timeline at a fixed speed and moves arrive at |
| 4 | +//! `CAP_GPUI_AUTO_HOVER_BENCH_HZ` (default 120); a move that lands while the |
| 5 | +//! previous one is still being handled is coalesced into the next, as the OS |
| 6 | +//! does. Each move runs the timeline's own hover handler, then one synchronous |
| 7 | +//! `Window::draw` (view renders + layout + prepaint + paint) is timed with it. |
| 8 | +//! `CAP_GPUI_AUTO_SIDEBAR` picks the sidebar tab that stays open meanwhile. |
| 9 | +//! |
| 10 | +//! Run: |
| 11 | +//! CAP_GPUI_AUTO_EDITOR=<project.cap> CAP_GPUI_AUTO_HOVER_BENCH=1 \ |
| 12 | +//! CAP_GPUI_AUTO_SIDEBAR=captions CAP_GPUI_NO_MENUS=1 CAP_GPUI_NO_TRAY=1 \ |
| 13 | +//! cargo run --release |
| 14 | +
|
| 15 | +use std::time::{Duration, Instant}; |
| 16 | + |
| 17 | +use gpui::{App, WindowHandle}; |
| 18 | + |
| 19 | +use crate::editor_window::EditorWindow; |
| 20 | + |
| 21 | +const DEFAULT_MOUSE_HZ: u64 = 120; |
| 22 | +const DEFAULT_SWEEP_SECS: f32 = 3.; |
| 23 | +const WARMUP: Duration = Duration::from_secs(1); |
| 24 | +const MEASURED: Duration = Duration::from_secs(6); |
| 25 | + |
| 26 | +pub async fn run(handle: WindowHandle<EditorWindow>, cx: &mut gpui::AsyncApp) { |
| 27 | + if !std::env::var("CAP_GPUI_AUTO_HOVER_BENCH").is_ok_and(|value| value == "1") { |
| 28 | + return; |
| 29 | + } |
| 30 | + cx.background_executor() |
| 31 | + .timer(Duration::from_millis(2500)) |
| 32 | + .await; |
| 33 | + cx.update(|cx| { |
| 34 | + cx.activate(true); |
| 35 | + handle |
| 36 | + .update(cx, |_, window, _| window.activate_window()) |
| 37 | + .ok(); |
| 38 | + }); |
| 39 | + cx.background_executor() |
| 40 | + .timer(Duration::from_millis(500)) |
| 41 | + .await; |
| 42 | + |
| 43 | + let mouse_hz = std::env::var("CAP_GPUI_AUTO_HOVER_BENCH_HZ") |
| 44 | + .ok() |
| 45 | + .and_then(|value| value.parse::<u64>().ok()) |
| 46 | + .filter(|hz| *hz > 0) |
| 47 | + .unwrap_or(DEFAULT_MOUSE_HZ); |
| 48 | + let interval = Duration::from_micros(1_000_000 / mouse_hz); |
| 49 | + let sweep_secs = std::env::var("CAP_GPUI_AUTO_HOVER_BENCH_SWEEP_SECS") |
| 50 | + .ok() |
| 51 | + .and_then(|value| value.parse::<f32>().ok()) |
| 52 | + .filter(|secs| *secs > 0.) |
| 53 | + .unwrap_or(DEFAULT_SWEEP_SECS); |
| 54 | + let started = Instant::now(); |
| 55 | + let mut next_move = started; |
| 56 | + let mut move_us = Vec::new(); |
| 57 | + let mut frames_at_start = None; |
| 58 | + loop { |
| 59 | + next_move += interval; |
| 60 | + let now = Instant::now(); |
| 61 | + if next_move > now { |
| 62 | + cx.background_executor() |
| 63 | + .timer(next_move.saturating_duration_since(now)) |
| 64 | + .await; |
| 65 | + } else { |
| 66 | + next_move = now; |
| 67 | + // An overloaded loop must still give the preview pump and the run |
| 68 | + // loop a turn between moves, as real event dispatch does. |
| 69 | + tokio::task::yield_now().await; |
| 70 | + } |
| 71 | + let elapsed = started.elapsed(); |
| 72 | + if elapsed >= WARMUP + MEASURED { |
| 73 | + break; |
| 74 | + } |
| 75 | + let measuring = elapsed >= WARMUP; |
| 76 | + if measuring && frames_at_start.is_none() { |
| 77 | + frames_at_start = cx.update(|cx| frames_presented(handle, cx)); |
| 78 | + } |
| 79 | + let phase = (elapsed.as_secs_f32() / sweep_secs).fract(); |
| 80 | + // Two separate updates: observers of the editor (its section views) |
| 81 | + // only hear about the move once the first update flushes its effects, |
| 82 | + // and nothing yields to the run loop in between, so the platform |
| 83 | + // cannot draw the frame before it is timed here. |
| 84 | + let move_started = Instant::now(); |
| 85 | + let hovered = cx.update(|cx| { |
| 86 | + handle |
| 87 | + .update(cx, |view, window, cx| { |
| 88 | + let width = f32::from(window.viewport_size().width); |
| 89 | + let x = 140. + (width - 180.).max(1.) * phase; |
| 90 | + view.timeline_hover(x, window, cx); |
| 91 | + }) |
| 92 | + .is_ok() |
| 93 | + }); |
| 94 | + let drawn = cx.update(|cx| draw_editor_window(handle, cx)); |
| 95 | + if !hovered || drawn.is_none() { |
| 96 | + eprintln!("hover-bench: the editor window went away"); |
| 97 | + std::process::exit(1); |
| 98 | + } |
| 99 | + if measuring { |
| 100 | + move_us.push(move_started.elapsed().as_secs_f64() * 1e6); |
| 101 | + } |
| 102 | + } |
| 103 | + let frames = cx |
| 104 | + .update(|cx| frames_presented(handle, cx)) |
| 105 | + .zip(frames_at_start) |
| 106 | + .map(|(end, start)| end.saturating_sub(start)); |
| 107 | + |
| 108 | + println!( |
| 109 | + "hover-bench: sidebar tab {}, pointer at {mouse_hz}Hz crossing the timeline in {sweep_secs}s, {} moves handled and {frames:?} preview frames presented in {:.1}s", |
| 110 | + std::env::var("CAP_GPUI_AUTO_SIDEBAR").unwrap_or_else(|_| "default".into()), |
| 111 | + move_us.len(), |
| 112 | + MEASURED.as_secs_f64(), |
| 113 | + ); |
| 114 | + report("move", &mut move_us); |
| 115 | + std::process::exit(0); |
| 116 | +} |
| 117 | + |
| 118 | +fn frames_presented(handle: WindowHandle<EditorWindow>, cx: &mut App) -> Option<u64> { |
| 119 | + handle |
| 120 | + .update(cx, |view, _, _| view.preview_frames_presented()) |
| 121 | + .ok() |
| 122 | + .flatten() |
| 123 | +} |
| 124 | + |
| 125 | +fn report(label: &str, samples: &mut [f64]) { |
| 126 | + if samples.is_empty() { |
| 127 | + println!("hover-bench: no {label} samples"); |
| 128 | + return; |
| 129 | + } |
| 130 | + samples.sort_by(|a, b| a.partial_cmp(b).expect("no NaN draw times")); |
| 131 | + let mean = samples.iter().sum::<f64>() / samples.len() as f64; |
| 132 | + let p50 = samples[samples.len() / 2]; |
| 133 | + let p95 = samples[(samples.len() as f64 * 0.95) as usize]; |
| 134 | + let max = samples[samples.len() - 1]; |
| 135 | + println!("hover-bench: {label} {mean:.1} mean / {p50:.1} p50 / {p95:.1} p95 / {max:.1} max us"); |
| 136 | +} |
| 137 | + |
| 138 | +/// `AnyWindowHandle::update` hands over the root as an unleased `AnyView`; |
| 139 | +/// the typed handle would lease the root entity that `draw()` re-renders. |
| 140 | +fn draw_editor_window(handle: WindowHandle<EditorWindow>, cx: &mut App) -> Option<()> { |
| 141 | + gpui::AnyWindowHandle::from(handle) |
| 142 | + .update(cx, |_, window, cx| window.draw(cx).clear()) |
| 143 | + .ok() |
| 144 | +} |
0 commit comments