perf(app): batch scroll surface flushes

This commit is contained in:
2026-05-16 04:23:49 -04:00
parent 0c733fa006
commit 91df575cf8
4 changed files with 55 additions and 34 deletions
+21 -20
View File
@@ -48,20 +48,19 @@ impl WebSurfaceStore {
tab: &BrowserTab, tab: &BrowserTab,
profile_data_mode: ProfileDataMode, profile_data_mode: ProfileDataMode,
permissions: &[WebSurfaceSitePermission], permissions: &[WebSurfaceSitePermission],
) -> WebSurfaceEnsureOutcome { ) -> bool {
if !is_external_web_url(tab.url().as_str()) { if !is_external_web_url(tab.url().as_str()) {
return WebSurfaceEnsureOutcome::default(); return false;
} }
let requested_url = tab.url().as_str().to_string(); let requested_url = tab.url().as_str().to_string();
let Some(size) = self.surfaces.get(tab.id()).and_then(|surface| surface.viewport_size) let Some(size) = self.surfaces.get(tab.id()).and_then(|surface| surface.viewport_size)
else { else {
return WebSurfaceEnsureOutcome::default(); return false;
}; };
let ensure_key = let ensure_key =
WebSurfaceEnsureKey::new(requested_url.clone(), size, tab.zoom_percent(), permissions); WebSurfaceEnsureKey::new(requested_url.clone(), size, tab.zoom_percent(), permissions);
if self.surfaces.get(tab.id()).is_some_and(|surface| !surface.should_ensure(&ensure_key)) { if self.surfaces.get(tab.id()).is_some_and(|surface| !surface.should_ensure(&ensure_key)) {
return WebSurfaceEnsureOutcome::default(); return false;
} }
let input = self.take_pending_input(tab.id(), requested_url.as_str()); let input = self.take_pending_input(tab.id(), requested_url.as_str());
let previous_frame = let previous_frame =
@@ -75,14 +74,14 @@ impl WebSurfaceStore {
requested_url: result.requested_url, requested_url: result.requested_url,
previous_frame, previous_frame,
}); });
return WebSurfaceEnsureOutcome { changed: true, url_change: None }; return true;
} }
WebSurfaceEnsureOutcome::default() false
} }
Err(message) => { Err(message) => {
self.surface_mut(tab.id()).mark_ensured(ensure_key); self.surface_mut(tab.id()).mark_ensured(ensure_key);
self.surface_mut(tab.id()).state = Some(WebSurfaceState::Failed { message }); self.surface_mut(tab.id()).state = Some(WebSurfaceState::Failed { message });
WebSurfaceEnsureOutcome { changed: true, url_change: None } true
} }
} }
} }
@@ -162,7 +161,7 @@ impl WebSurfaceStore {
let stale_tab_ids = self let stale_tab_ids = self
.surfaces .surfaces
.keys() .keys()
.filter(|tab_id| !open_tab_ids.iter().any(|open_tab_id| open_tab_id == *tab_id)) .filter(|tab_id| !open_tab_ids.contains(*tab_id))
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for tab_id in stale_tab_ids { for tab_id in stale_tab_ids {
@@ -170,10 +169,6 @@ impl WebSurfaceStore {
} }
} }
pub(super) fn close_surface(&mut self, tab_id: &TabId) {
self.close_surface_for_tab(tab_id);
}
pub(super) fn record_scroll_delta( pub(super) fn record_scroll_delta(
&mut self, &mut self,
tab_id: &TabId, tab_id: &TabId,
@@ -196,6 +191,7 @@ impl WebSurfaceStore {
}; };
let surface = self.surface_mut(tab_id); let surface = self.surface_mut(tab_id);
let flush_throttled = surface.input_flush_is_throttled(Instant::now());
let scroll = surface let scroll = surface
.scroll_offset .scroll_offset
.get_or_insert_with(|| WebSurfaceScrollState::new(requested_url.to_string())); .get_or_insert_with(|| WebSurfaceScrollState::new(requested_url.to_string()));
@@ -211,8 +207,12 @@ impl WebSurfaceStore {
surface.pending_scroll_point = Some(point); surface.pending_scroll_point = Some(point);
surface.mark_pending_input_started(); surface.mark_pending_input_started();
surface.click_point = None; surface.click_point = None;
if flush_throttled {
WebSurfaceInputOutcome::Buffered
} else {
WebSurfaceInputOutcome::Applied WebSurfaceInputOutcome::Applied
} }
}
pub(super) fn record_viewport_size( pub(super) fn record_viewport_size(
&mut self, &mut self,
@@ -381,6 +381,13 @@ impl WebSurfaceStore {
.map(|state| state.text); .map(|state| state.text);
let hover_point = surface.hover_point.take(); let hover_point = surface.hover_point.take();
let enqueued_at = surface.pending_input_started_at.take(); let enqueued_at = surface.pending_input_started_at.take();
if scroll_delta.is_some()
|| click_point.is_some()
|| hover_point.is_some()
|| typed_text.is_some()
{
surface.mark_input_flushed(Instant::now());
}
WebSurfacePendingInput { WebSurfacePendingInput {
enqueued_at, enqueued_at,
@@ -453,7 +460,7 @@ impl WebSurfaceStore {
self.surfaces.entry(tab_id.clone()).or_insert_with(PerTabSurface::new) self.surfaces.entry(tab_id.clone()).or_insert_with(PerTabSurface::new)
} }
fn close_surface_for_tab(&mut self, tab_id: &TabId) { pub(super) fn close_surface(&mut self, tab_id: &TabId) {
self.runtime.close_tab(tab_id); self.runtime.close_tab(tab_id);
self.surfaces.remove(tab_id); self.surfaces.remove(tab_id);
if self.keyboard_focus.as_ref().is_some_and(|focus| focus.tab_id == *tab_id) { if self.keyboard_focus.as_ref().is_some_and(|focus| focus.tab_id == *tab_id) {
@@ -476,12 +483,6 @@ pub(super) fn is_external_web_url(url: &str) -> bool {
url.starts_with("https://") || url.starts_with("http://") url.starts_with("https://") || url.starts_with("http://")
} }
#[derive(Default)]
pub(super) struct WebSurfaceEnsureOutcome {
pub(super) changed: bool,
pub(super) url_change: Option<WebSurfaceUrlChange>,
}
#[derive(Default)] #[derive(Default)]
pub(super) struct WebSurfaceTickResult { pub(super) struct WebSurfaceTickResult {
pub(super) changed: bool, pub(super) changed: bool,
@@ -182,20 +182,15 @@ impl ElyShell {
impl ElyShell { impl ElyShell {
fn ensure_visible_web_surfaces(&mut self, visible_tabs: Vec<VisibleWebSurfaceTab>) -> bool { fn ensure_visible_web_surfaces(&mut self, visible_tabs: Vec<VisibleWebSurfaceTab>) -> bool {
let mut url_changed = false;
let mut changed = false; let mut changed = false;
for visible in visible_tabs { for visible in visible_tabs {
let outcome = self.web_surfaces.ensure_surface( changed |= self.web_surfaces.ensure_surface(
&visible.tab, &visible.tab,
visible.profile_data_mode, visible.profile_data_mode,
&visible.permissions, &visible.permissions,
); );
changed |= outcome.changed;
if let Some(url_change) = outcome.url_change {
url_changed |= self.apply_web_surface_url_change(url_change);
} }
} changed
changed || url_changed
} }
fn apply_web_surface_url_change(&mut self, change: WebSurfaceUrlChange) -> bool { fn apply_web_surface_url_change(&mut self, change: WebSurfaceUrlChange) -> bool {
@@ -103,11 +103,11 @@ fn unchanged_surface_without_input_skips_runtime_ensure() -> Result<(), String>
store.record_viewport_size(tab.id(), viewport_bounds(), 1.0), store.record_viewport_size(tab.id(), viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied, WebSurfaceInputOutcome::Applied,
); );
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed); assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]));
store.flush_runtime_for_test(); store.flush_runtime_for_test();
assert_eq!(IDLE_SKIP_ENSURE_COUNT.load(Ordering::SeqCst), 1); assert_eq!(IDLE_SKIP_ENSURE_COUNT.load(Ordering::SeqCst), 1);
assert!(!store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed); assert!(!store.ensure_surface(&tab, ProfileDataMode::Transient, &[]));
store.flush_runtime_for_test(); store.flush_runtime_for_test();
assert_eq!(IDLE_SKIP_ENSURE_COUNT.load(Ordering::SeqCst), 1); assert_eq!(IDLE_SKIP_ENSURE_COUNT.load(Ordering::SeqCst), 1);
Ok(()) Ok(())
@@ -126,7 +126,7 @@ fn store_tick_delay_tracks_runtime_cadence() -> Result<(), String> {
store.record_viewport_size(tab.id(), viewport_bounds(), 1.0), store.record_viewport_size(tab.id(), viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied, WebSurfaceInputOutcome::Applied,
); );
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed); assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]));
assert_eq!(store.next_tick_delay(&visible), Duration::ZERO); assert_eq!(store.next_tick_delay(&visible), Duration::ZERO);
let _ = store.tick(&visible); let _ = store.tick(&visible);
@@ -188,13 +188,13 @@ fn failed_surface_ensure_waits_for_a_new_key_before_retrying() -> Result<(), Str
store.record_viewport_size(tab.id(), viewport_bounds(), 1.0), store.record_viewport_size(tab.id(), viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied, WebSurfaceInputOutcome::Applied,
); );
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed); assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]));
store.flush_runtime_for_test(); store.flush_runtime_for_test();
let tick = store.tick(&[tab.id().clone()]); let tick = store.tick(&[tab.id().clone()]);
assert!(tick.changed, "the failing client must surface a state change via tick"); assert!(tick.changed, "the failing client must surface a state change via tick");
assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 1); assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 1);
assert!(!store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed); assert!(!store.ensure_surface(&tab, ProfileDataMode::Transient, &[]));
store.flush_runtime_for_test(); store.flush_runtime_for_test();
let _ = store.tick(&[tab.id().clone()]); let _ = store.tick(&[tab.id().clone()]);
assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 1); assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 1);
@@ -203,7 +203,7 @@ fn failed_surface_ensure_waits_for_a_new_key_before_retrying() -> Result<(), Str
store.record_viewport_size(tab.id(), resized_viewport_bounds(), 1.0), store.record_viewport_size(tab.id(), resized_viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied, WebSurfaceInputOutcome::Applied,
); );
assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed); assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]));
store.flush_runtime_for_test(); store.flush_runtime_for_test();
let _ = store.tick(&[tab.id().clone()]); let _ = store.tick(&[tab.id().clone()]);
assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 2); assert_eq!(FAILING_ENSURE_COUNT.load(Ordering::SeqCst), 2);
@@ -4,6 +4,7 @@ use ely_domain::TabId;
use gpui::{Bounds, Pixels}; use gpui::{Bounds, Pixels};
use super::{ use super::{
web_surface_cadence::ACTIVE_POLL_INTERVAL,
web_surface_frame::WebSurfaceFrame, web_surface_frame::WebSurfaceFrame,
web_surface_geometry::{ web_surface_geometry::{
WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceScrollOffset, WebSurfaceSize, WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceScrollOffset, WebSurfaceSize,
@@ -70,6 +71,8 @@ pub(super) struct WebSurfacePendingInput {
pub(super) enum WebSurfaceInputOutcome { pub(super) enum WebSurfaceInputOutcome {
/// State changed and the renderer should re-notify. /// State changed and the renderer should re-notify.
Applied, Applied,
/// State changed and the active cadence timer will flush it.
Buffered,
/// Same value as currently recorded — nothing to flush downstream. /// Same value as currently recorded — nothing to flush downstream.
NoChange, NoChange,
/// Geometry constructor rejected the input (zero/NaN/negative /// Geometry constructor rejected the input (zero/NaN/negative
@@ -124,6 +127,7 @@ pub(super) struct PerTabSurface {
pub(super) scroll_offset: Option<WebSurfaceScrollState>, pub(super) scroll_offset: Option<WebSurfaceScrollState>,
pub(super) typed_text: Option<WebSurfaceTextInputState>, pub(super) typed_text: Option<WebSurfaceTextInputState>,
pub(super) state: Option<WebSurfaceState>, pub(super) state: Option<WebSurfaceState>,
last_input_flushed_at: Option<Instant>,
} }
impl PerTabSurface { impl PerTabSurface {
@@ -141,6 +145,7 @@ impl PerTabSurface {
scroll_offset: None, scroll_offset: None,
typed_text: None, typed_text: None,
state: None, state: None,
last_input_flushed_at: None,
} }
} }
@@ -157,6 +162,15 @@ impl PerTabSurface {
self.last_hover_enqueued_at = Some(now); self.last_hover_enqueued_at = Some(now);
} }
pub(super) fn input_flush_is_throttled(&self, now: Instant) -> bool {
self.last_input_flushed_at
.is_some_and(|last| now.duration_since(last) < ACTIVE_POLL_INTERVAL)
}
pub(super) fn mark_input_flushed(&mut self, now: Instant) {
self.last_input_flushed_at = Some(now);
}
pub(super) fn should_ensure(&self, key: &WebSurfaceEnsureKey) -> bool { pub(super) fn should_ensure(&self, key: &WebSurfaceEnsureKey) -> bool {
self.last_ensure_key.as_ref() != Some(key) || self.has_pending_input() self.last_ensure_key.as_ref() != Some(key) || self.has_pending_input()
} }
@@ -242,6 +256,17 @@ mod tests {
assert!(surface.should_ensure(&new_key)); assert!(surface.should_ensure(&new_key));
} }
#[test]
fn recent_input_flush_throttles_immediate_flush() {
let start = Instant::now();
let mut surface = PerTabSurface::new();
surface.mark_input_flushed(start);
assert!(surface.input_flush_is_throttled(start + Duration::from_millis(7)));
assert!(!surface.input_flush_is_throttled(start + Duration::from_millis(8)));
}
fn ensure_key(url: &str, width: u32, height: u32) -> WebSurfaceEnsureKey { fn ensure_key(url: &str, width: u32, height: u32) -> WebSurfaceEnsureKey {
WebSurfaceEnsureKey::new( WebSurfaceEnsureKey::new(
url.to_string(), url.to_string(),