diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 41aa9b5..8c73e23 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -42,8 +42,6 @@ mod web_surface_worker; #[cfg(test)] mod gpui_harness_tests; -use std::time::Duration; - use ely_browser_core::{BrowserCore, InitialBrowserConfig}; use ely_domain::{DEFAULT_TRANSLUCENCY_PCT, ProfileId, SpaceId, TabId}; use gpui::{AppContext, Context, Entity, FocusHandle, Subscription, Timer, Window}; @@ -459,14 +457,12 @@ impl ElyShell { fn start_external_web_surface_timer(cx: &mut Context) { cx.spawn(async move |shell, cx| { - // 8 ms ≈ 125 Hz, matched to a 120 Hz display's frame budget. - // The Servo worker thread does the blocking IPC, so this tick - // only enqueues Poll requests and drains the response channel - // — cheap enough to run twice as often as the previous 60 Hz - // schedule and lets active pages feed the renderer a fresh - // frame between every display refresh. loop { - Timer::after(Duration::from_millis(8)).await; + let delay = match shell.update(cx, |shell, _| shell.external_web_surface_tick_delay()) { + Ok(delay) => delay, + Err(_) => break, + }; + Timer::after(delay).await; let result = shell.update(cx, |shell, cx| { if shell.tick_external_web_surfaces() { cx.notify(); diff --git a/crates/ely_app/src/shell/web_surface.rs b/crates/ely_app/src/shell/web_surface.rs index e7cd41d..d8a08bd 100644 --- a/crates/ely_app/src/shell/web_surface.rs +++ b/crates/ely_app/src/shell/web_surface.rs @@ -1,4 +1,5 @@ -use std::{collections::BTreeMap, time::Instant}; +use std::collections::BTreeMap; +use std::time::{Duration, Instant}; use ely_domain::{BrowserTab, TabId}; use gpui::{Bounds, Pixels, Point}; @@ -7,6 +8,7 @@ use crate::services::ProfileDataMode; use super::web_surface_metadata::WebSurfacePageMetadata; use super::{ + web_surface_cadence::IDLE_POLL_INTERVAL, web_surface_frame::WebSurfaceFrame, web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceSize}, web_surface_permissions::WebSurfaceSitePermission, @@ -149,6 +151,13 @@ impl WebSurfaceStore { result } + pub(super) fn next_tick_delay(&self, visible_tab_ids: &[TabId]) -> Duration { + self.runtime + .next_poll_delay(visible_tab_ids, Instant::now()) + .unwrap_or(IDLE_POLL_INTERVAL) + .min(IDLE_POLL_INTERVAL) + } + pub(super) fn retain_tabs(&mut self, open_tab_ids: &[TabId]) { let stale_tab_ids = self .surfaces diff --git a/crates/ely_app/src/shell/web_surface_cadence.rs b/crates/ely_app/src/shell/web_surface_cadence.rs index 0ca8c91..e546984 100644 --- a/crates/ely_app/src/shell/web_surface_cadence.rs +++ b/crates/ely_app/src/shell/web_surface_cadence.rs @@ -1,7 +1,7 @@ use std::time::{Duration, Instant}; -const ACTIVE_POLL_INTERVAL: Duration = Duration::from_millis(8); -const IDLE_POLL_INTERVAL: Duration = Duration::from_millis(80); +pub(super) const ACTIVE_POLL_INTERVAL: Duration = Duration::from_millis(8); +pub(super) const IDLE_POLL_INTERVAL: Duration = Duration::from_millis(80); const LOAD_BOOST_WINDOW: Duration = Duration::from_secs(5); const INPUT_BOOST_WINDOW: Duration = Duration::from_millis(600); const HOVER_BOOST_WINDOW: Duration = Duration::from_millis(120); @@ -57,6 +57,10 @@ impl WebSurfacePollCadence { self.next_poll_at.is_none_or(|next| now >= next) } + pub(super) fn next_poll_delay(&self, now: Instant) -> Duration { + self.next_poll_at.map_or(Duration::ZERO, |next| next.saturating_duration_since(now)) + } + pub(super) fn note_poll_submitted(&mut self, now: Instant) { self.next_poll_at = Some(now + self.current_interval(now)); } @@ -120,7 +124,9 @@ impl WebSurfaceInputKind { mod tests { use std::time::{Duration, Instant}; - use super::{WebSurfaceInputKind, WebSurfacePollCadence}; + use super::{ + ACTIVE_POLL_INTERVAL, IDLE_POLL_INTERVAL, WebSurfaceInputKind, WebSurfacePollCadence, + }; #[test] fn idle_poll_uses_low_frequency_after_submission() { @@ -129,6 +135,7 @@ mod tests { cadence.note_poll_submitted(start); + assert_eq!(cadence.next_poll_delay(start), IDLE_POLL_INTERVAL); assert!(!cadence.should_poll(start + Duration::from_millis(79))); assert!(cadence.should_poll(start + Duration::from_millis(80))); } @@ -141,6 +148,7 @@ mod tests { cadence.note_ensure(WebSurfaceInputKind::Scroll, false, start); cadence.note_poll_submitted(start); + assert_eq!(cadence.next_poll_delay(start), ACTIVE_POLL_INTERVAL); assert!(!cadence.should_poll(start + Duration::from_millis(7))); assert!(cadence.should_poll(start + Duration::from_millis(8))); } diff --git a/crates/ely_app/src/shell/web_surface_controller.rs b/crates/ely_app/src/shell/web_surface_controller.rs index 14292ff..1afd1c5 100644 --- a/crates/ely_app/src/shell/web_surface_controller.rs +++ b/crates/ely_app/src/shell/web_surface_controller.rs @@ -69,14 +69,34 @@ impl ElyShell { result.changed || url_changed || metadata_changed || sync_changed } + pub(super) fn external_web_surface_tick_delay(&self) -> std::time::Duration { + let visible_tab_ids = match &self.state { + super::ShellState::Ready(core) => { + core.visible_content_tab_ids().unwrap_or_else(|_| Vec::new()) + } + super::ShellState::StartupError(_) => Vec::new(), + }; + self.web_surfaces.next_tick_delay(&visible_tab_ids) + } + + fn flush_external_web_surface_tick(&mut self, cx: &mut Context) { + if self.tick_external_web_surfaces() { + cx.notify(); + } + } + pub(super) fn record_external_web_viewport( &mut self, tab_id: TabId, bounds: Bounds, scale_factor: f32, - _cx: &mut Context, + cx: &mut Context, ) { - let _ = self.web_surfaces.record_viewport_size(&tab_id, bounds, scale_factor); + if self.web_surfaces.record_viewport_size(&tab_id, bounds, scale_factor) + == WebSurfaceInputOutcome::Applied + { + self.flush_external_web_surface_tick(cx); + } } pub(super) fn scroll_external_web_viewport( @@ -86,15 +106,18 @@ impl ElyShell { delta: Point, position: Point, scale_factor: f32, - _cx: &mut Context, + cx: &mut Context, ) { - let _ = self.web_surfaces.record_scroll_delta( + if self.web_surfaces.record_scroll_delta( &tab_id, requested_url.as_str(), delta, position, scale_factor, - ); + ) == WebSurfaceInputOutcome::Applied + { + self.flush_external_web_surface_tick(cx); + } } pub(super) fn hover_external_web_viewport( @@ -102,9 +125,13 @@ impl ElyShell { tab_id: TabId, position: Point, scale_factor: f32, - _cx: &mut Context, + cx: &mut Context, ) { - let _ = self.web_surfaces.record_hover_point(&tab_id, position, scale_factor); + if self.web_surfaces.record_hover_point(&tab_id, position, scale_factor) + == WebSurfaceInputOutcome::Applied + { + self.flush_external_web_surface_tick(cx); + } } pub(super) fn click_external_web_viewport( @@ -113,16 +140,19 @@ impl ElyShell { requested_url: String, position: Point, window: &mut gpui::Window, - _cx: &mut Context, + cx: &mut Context, ) { self.focus_handle.focus(window); let scale_factor = window.scale_factor(); - let _ = self.web_surfaces.record_click_point( + if self.web_surfaces.record_click_point( &tab_id, requested_url.as_str(), position, scale_factor, - ); + ) == WebSurfaceInputOutcome::Applied + { + self.flush_external_web_surface_tick(cx); + } } /// Hand focus to the shell's root focus handle so subsequent @@ -138,11 +168,12 @@ impl ElyShell { tab_id: TabId, requested_url: String, text: &str, - _cx: &mut Context, + cx: &mut Context, ) -> bool { if self.web_surfaces.record_typed_text(&tab_id, requested_url.as_str(), text) == WebSurfaceInputOutcome::Applied { + self.flush_external_web_surface_tick(cx); return true; } diff --git a/crates/ely_app/src/shell/web_surface_runtime.rs b/crates/ely_app/src/shell/web_surface_runtime.rs index 8cd2d6f..cdd3885 100644 --- a/crates/ely_app/src/shell/web_surface_runtime.rs +++ b/crates/ely_app/src/shell/web_surface_runtime.rs @@ -1,4 +1,9 @@ -use std::{collections::BTreeMap, fs, path::PathBuf, time::Instant}; +use std::{ + collections::BTreeMap, + fs, + path::PathBuf, + time::{Duration, Instant}, +}; use ely_domain::{BrowserTab, ProfileId, TabId}; @@ -177,14 +182,26 @@ impl WebSurfaceRuntime { let Some(scoped) = self.workers.get(&session.scope) else { continue; }; - if scoped.worker.submit_poll(tab_id.as_str().to_string()) { - session.cadence.note_poll_submitted(poll_now); - } + let _ = scoped.worker.submit_poll(tab_id.as_str().to_string()); + session.cadence.note_poll_submitted(poll_now); } frames } + pub(super) fn next_poll_delay( + &self, + visible_tab_ids: &[TabId], + now: Instant, + ) -> Option { + visible_tab_ids + .iter() + .filter_map(|tab_id| self.sessions.get(tab_id)) + .filter(|session| self.workers.contains_key(&session.scope)) + .map(|session| session.cadence.next_poll_delay(now)) + .min() + } + pub(super) fn close_tab(&mut self, tab_id: &TabId) { let Some(session) = self.sessions.remove(tab_id) else { return; diff --git a/crates/ely_app/src/shell/web_surface_runtime_tests.rs b/crates/ely_app/src/shell/web_surface_runtime_tests.rs index cec60ee..2abd79f 100644 --- a/crates/ely_app/src/shell/web_surface_runtime_tests.rs +++ b/crates/ely_app/src/shell/web_surface_runtime_tests.rs @@ -1,6 +1,7 @@ use std::{ collections::BTreeMap, sync::atomic::{AtomicUsize, Ordering}, + time::Duration, }; use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText}; @@ -9,6 +10,7 @@ use crate::{ services::ProfileDataMode, shell::{ WebSurfaceStore, + web_surface_cadence::{ACTIVE_POLL_INTERVAL, IDLE_POLL_INTERVAL}, web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize}, web_surface_state::{WebSurfaceInputOutcome, WebSurfacePendingInput}, web_surface_worker::{LiveRuntimeClient, LiveRuntimeClientError}, @@ -111,6 +113,30 @@ fn unchanged_surface_without_input_skips_runtime_ensure() -> Result<(), String> Ok(()) } +#[test] +fn store_tick_delay_tracks_runtime_cadence() -> Result<(), String> { + let mut store = WebSurfaceStore::new_with_runtime(WebSurfaceRuntime::new_with_client_factory( + fake_client_factory, + )); + let tab = web_tab(TabId::new(), ProfileId::new(), "https://example.com/cadence")?; + let visible = vec![tab.id().clone()]; + + assert_eq!(store.next_tick_delay(&visible), IDLE_POLL_INTERVAL); + assert_eq!( + store.record_viewport_size(tab.id(), viewport_bounds(), 1.0), + WebSurfaceInputOutcome::Applied, + ); + assert!(store.ensure_surface(&tab, ProfileDataMode::Transient, &[]).changed); + assert_eq!(store.next_tick_delay(&visible), Duration::ZERO); + + let _ = store.tick(&visible); + let delay = store.next_tick_delay(&visible); + + assert!(delay <= ACTIVE_POLL_INTERVAL); + assert!(delay > Duration::ZERO); + Ok(()) +} + #[test] fn sidecar_exit_removes_dead_runtime_client() -> Result<(), String> { RECOVERY_FACTORY_COUNT.store(0, Ordering::SeqCst);