Skip to content

Commit 23bd622

Browse files
Merge pull request #2542 from notkainoa/perf/editor-section-invalidation
perf(desktop-gpui): repaint only the timeline and transport on hover
2 parents 94a99fc + 4a7f1ea commit 23bd622

4 files changed

Lines changed: 173 additions & 8 deletions

File tree

‎apps/desktop-gpui/src/app_windows.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5386,6 +5386,7 @@ fn load_editor_project_candidate(
53865386
drive_auto_sidebar(handle, cx).await;
53875387
drive_auto_playback(path, handle, cx).await;
53885388
drive_auto_export(handle, cx).await;
5389+
crate::editor_hover_bench::run(handle, cx).await;
53895390
})
53905391
.detach();
53915392
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
}

‎apps/desktop-gpui/src/editor_window.rs‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3769,9 +3769,11 @@ impl EditorWindow {
37693769
// scene-mode pane gates on it too. Notifying the parent here would
37703770
// rebuild every cached section -- header, toolbar, sidebar included --
37713771
// at the 60Hz playhead rate, which is the same class of rebuild that
3772-
// measured 40ms/frame on the text-panel sidebar. Paused paths (hover,
3773-
// seeks) keep the full invalidation: the overlay's element boxes
3774-
// appear and disappear as the playhead crosses segments.
3772+
// measured 40ms/frame on the text-panel sidebar. A paused playhead
3773+
// change keeps the full invalidation: the overlay's element boxes
3774+
// appear and disappear as the playhead crosses segments. Pointer hover
3775+
// goes through `invalidate_hover_chrome` instead, and an arriving
3776+
// paused frame whose layout changed still notifies the whole editor.
37753777
if !self.playing {
37763778
cx.notify();
37773779
}
@@ -3792,10 +3794,16 @@ impl EditorWindow {
37923794
);
37933795
}
37943796

3797+
pub(crate) fn preview_frames_presented(&self) -> Option<u64> {
3798+
self.stats
3799+
.as_ref()
3800+
.map(|stats| stats.presented.load(Ordering::Relaxed))
3801+
}
3802+
37953803
/// `onMouseMove` on the timeline container (`TL/index.tsx:1170-1188`):
37963804
/// while paused, the pointer's time becomes `previewTime`; outside the
37973805
/// content column, and at all times while playing, it is cleared.
3798-
fn timeline_hover(&mut self, x: f32, window: &mut Window, cx: &mut Context<Self>) {
3806+
pub(crate) fn timeline_hover(&mut self, x: f32, window: &mut Window, cx: &mut Context<Self>) {
37993807
// `if (editorState.playing) return;` -- the handler bails *before* it
38003808
// writes, so a preview time set while paused survives a play rather
38013809
// than being cleared. The ghost is hidden by the render's own
@@ -3808,17 +3816,28 @@ impl EditorWindow {
38083816
if next != self.view.preview_time {
38093817
self.view.preview_time = next;
38103818
self.emit_preview_frame();
3811-
self.invalidate_playback_chrome(window, cx);
3819+
self.invalidate_hover_chrome(cx);
38123820
}
38133821
}
38143822

3815-
fn timeline_hover_leave(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3823+
fn timeline_hover_leave(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
38163824
if self.playing || self.view.preview_time.is_none() {
38173825
return;
38183826
}
38193827
self.view.preview_time = None;
38203828
self.emit_preview_frame();
3821-
self.invalidate_playback_chrome(window, cx);
3829+
self.invalidate_hover_chrome(cx);
3830+
}
3831+
3832+
/// Pointer-hover state that only the timeline, the transport and the
3833+
/// canvas overlay read. Notifying `self` would rebuild the header, toolbar
3834+
/// and sidebar on every pointer move, because each section observes the
3835+
/// editor. Notifying a child view marks its ancestors dirty instead, so
3836+
/// this window and its canvas overlay still re-render while the untouched
3837+
/// sections are reused from their cache.
3838+
fn invalidate_hover_chrome(&mut self, cx: &mut Context<Self>) {
3839+
self.timeline_view.update(cx, |_, cx| cx.notify());
3840+
self.transport_controls.update(cx, |_, cx| cx.notify());
38223841
}
38233842

38243843
fn set_hovered_track(
@@ -5922,7 +5941,7 @@ impl EditorWindow {
59225941
changed = true;
59235942
}
59245943
if changed {
5925-
cx.notify();
5944+
self.invalidate_hover_chrome(cx);
59265945
}
59275946
}
59285947

‎apps/desktop-gpui/src/main.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod editor_color;
2828
mod editor_crop;
2929
mod editor_edits;
3030
mod editor_export;
31+
mod editor_hover_bench;
3132
#[cfg(target_os = "linux")]
3233
mod editor_modal;
3334
mod editor_panels;

0 commit comments

Comments
 (0)