perf(web-surface): adapt shell tick cadence

This commit is contained in:
2026-05-15 23:42:48 -04:00
parent 225a998642
commit 06e947f43e
6 changed files with 115 additions and 28 deletions
+5 -9
View File
@@ -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<ElyShell>) {
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();
+10 -1
View File
@@ -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
@@ -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)));
}
@@ -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<Self>) {
if self.tick_external_web_surfaces() {
cx.notify();
}
}
pub(super) fn record_external_web_viewport(
&mut self,
tab_id: TabId,
bounds: Bounds<Pixels>,
scale_factor: f32,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) {
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<Pixels>,
position: Point<Pixels>,
scale_factor: f32,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) {
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<Pixels>,
scale_factor: f32,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) {
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<Pixels>,
window: &mut gpui::Window,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) {
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<Self>,
cx: &mut Context<Self>,
) -> 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;
}
@@ -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()) {
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<Duration> {
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;
@@ -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);