perf(web-surface): adapt shell tick cadence
This commit is contained in:
@@ -42,8 +42,6 @@ mod web_surface_worker;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod gpui_harness_tests;
|
mod gpui_harness_tests;
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||||
use ely_domain::{DEFAULT_TRANSLUCENCY_PCT, ProfileId, SpaceId, TabId};
|
use ely_domain::{DEFAULT_TRANSLUCENCY_PCT, ProfileId, SpaceId, TabId};
|
||||||
use gpui::{AppContext, Context, Entity, FocusHandle, Subscription, Timer, Window};
|
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>) {
|
fn start_external_web_surface_timer(cx: &mut Context<ElyShell>) {
|
||||||
cx.spawn(async move |shell, cx| {
|
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 {
|
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| {
|
let result = shell.update(cx, |shell, cx| {
|
||||||
if shell.tick_external_web_surfaces() {
|
if shell.tick_external_web_surfaces() {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|||||||
@@ -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 ely_domain::{BrowserTab, TabId};
|
||||||
use gpui::{Bounds, Pixels, Point};
|
use gpui::{Bounds, Pixels, Point};
|
||||||
@@ -7,6 +8,7 @@ use crate::services::ProfileDataMode;
|
|||||||
|
|
||||||
use super::web_surface_metadata::WebSurfacePageMetadata;
|
use super::web_surface_metadata::WebSurfacePageMetadata;
|
||||||
use super::{
|
use super::{
|
||||||
|
web_surface_cadence::IDLE_POLL_INTERVAL,
|
||||||
web_surface_frame::WebSurfaceFrame,
|
web_surface_frame::WebSurfaceFrame,
|
||||||
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceSize},
|
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceSize},
|
||||||
web_surface_permissions::WebSurfaceSitePermission,
|
web_surface_permissions::WebSurfaceSitePermission,
|
||||||
@@ -149,6 +151,13 @@ impl WebSurfaceStore {
|
|||||||
result
|
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]) {
|
pub(super) fn retain_tabs(&mut self, open_tab_ids: &[TabId]) {
|
||||||
let stale_tab_ids = self
|
let stale_tab_ids = self
|
||||||
.surfaces
|
.surfaces
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const ACTIVE_POLL_INTERVAL: Duration = Duration::from_millis(8);
|
pub(super) const ACTIVE_POLL_INTERVAL: Duration = Duration::from_millis(8);
|
||||||
const IDLE_POLL_INTERVAL: Duration = Duration::from_millis(80);
|
pub(super) const IDLE_POLL_INTERVAL: Duration = Duration::from_millis(80);
|
||||||
const LOAD_BOOST_WINDOW: Duration = Duration::from_secs(5);
|
const LOAD_BOOST_WINDOW: Duration = Duration::from_secs(5);
|
||||||
const INPUT_BOOST_WINDOW: Duration = Duration::from_millis(600);
|
const INPUT_BOOST_WINDOW: Duration = Duration::from_millis(600);
|
||||||
const HOVER_BOOST_WINDOW: Duration = Duration::from_millis(120);
|
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)
|
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) {
|
pub(super) fn note_poll_submitted(&mut self, now: Instant) {
|
||||||
self.next_poll_at = Some(now + self.current_interval(now));
|
self.next_poll_at = Some(now + self.current_interval(now));
|
||||||
}
|
}
|
||||||
@@ -120,7 +124,9 @@ impl WebSurfaceInputKind {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use super::{WebSurfaceInputKind, WebSurfacePollCadence};
|
use super::{
|
||||||
|
ACTIVE_POLL_INTERVAL, IDLE_POLL_INTERVAL, WebSurfaceInputKind, WebSurfacePollCadence,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn idle_poll_uses_low_frequency_after_submission() {
|
fn idle_poll_uses_low_frequency_after_submission() {
|
||||||
@@ -129,6 +135,7 @@ mod tests {
|
|||||||
|
|
||||||
cadence.note_poll_submitted(start);
|
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(79)));
|
||||||
assert!(cadence.should_poll(start + Duration::from_millis(80)));
|
assert!(cadence.should_poll(start + Duration::from_millis(80)));
|
||||||
}
|
}
|
||||||
@@ -141,6 +148,7 @@ mod tests {
|
|||||||
cadence.note_ensure(WebSurfaceInputKind::Scroll, false, start);
|
cadence.note_ensure(WebSurfaceInputKind::Scroll, false, start);
|
||||||
cadence.note_poll_submitted(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(7)));
|
||||||
assert!(cadence.should_poll(start + Duration::from_millis(8)));
|
assert!(cadence.should_poll(start + Duration::from_millis(8)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,14 +69,34 @@ impl ElyShell {
|
|||||||
result.changed || url_changed || metadata_changed || sync_changed
|
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(
|
pub(super) fn record_external_web_viewport(
|
||||||
&mut self,
|
&mut self,
|
||||||
tab_id: TabId,
|
tab_id: TabId,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
scale_factor: f32,
|
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(
|
pub(super) fn scroll_external_web_viewport(
|
||||||
@@ -86,15 +106,18 @@ impl ElyShell {
|
|||||||
delta: Point<Pixels>,
|
delta: Point<Pixels>,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
scale_factor: f32,
|
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,
|
&tab_id,
|
||||||
requested_url.as_str(),
|
requested_url.as_str(),
|
||||||
delta,
|
delta,
|
||||||
position,
|
position,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
);
|
) == WebSurfaceInputOutcome::Applied
|
||||||
|
{
|
||||||
|
self.flush_external_web_surface_tick(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn hover_external_web_viewport(
|
pub(super) fn hover_external_web_viewport(
|
||||||
@@ -102,9 +125,13 @@ impl ElyShell {
|
|||||||
tab_id: TabId,
|
tab_id: TabId,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
scale_factor: f32,
|
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(
|
pub(super) fn click_external_web_viewport(
|
||||||
@@ -113,16 +140,19 @@ impl ElyShell {
|
|||||||
requested_url: String,
|
requested_url: String,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
window: &mut gpui::Window,
|
window: &mut gpui::Window,
|
||||||
_cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.focus_handle.focus(window);
|
self.focus_handle.focus(window);
|
||||||
let scale_factor = window.scale_factor();
|
let scale_factor = window.scale_factor();
|
||||||
let _ = self.web_surfaces.record_click_point(
|
if self.web_surfaces.record_click_point(
|
||||||
&tab_id,
|
&tab_id,
|
||||||
requested_url.as_str(),
|
requested_url.as_str(),
|
||||||
position,
|
position,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
);
|
) == WebSurfaceInputOutcome::Applied
|
||||||
|
{
|
||||||
|
self.flush_external_web_surface_tick(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hand focus to the shell's root focus handle so subsequent
|
/// Hand focus to the shell's root focus handle so subsequent
|
||||||
@@ -138,11 +168,12 @@ impl ElyShell {
|
|||||||
tab_id: TabId,
|
tab_id: TabId,
|
||||||
requested_url: String,
|
requested_url: String,
|
||||||
text: &str,
|
text: &str,
|
||||||
_cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.web_surfaces.record_typed_text(&tab_id, requested_url.as_str(), text)
|
if self.web_surfaces.record_typed_text(&tab_id, requested_url.as_str(), text)
|
||||||
== WebSurfaceInputOutcome::Applied
|
== WebSurfaceInputOutcome::Applied
|
||||||
{
|
{
|
||||||
|
self.flush_external_web_surface_tick(cx);
|
||||||
return true;
|
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};
|
use ely_domain::{BrowserTab, ProfileId, TabId};
|
||||||
|
|
||||||
@@ -177,14 +182,26 @@ impl WebSurfaceRuntime {
|
|||||||
let Some(scoped) = self.workers.get(&session.scope) else {
|
let Some(scoped) = self.workers.get(&session.scope) else {
|
||||||
continue;
|
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);
|
session.cadence.note_poll_submitted(poll_now);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
frames
|
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) {
|
pub(super) fn close_tab(&mut self, tab_id: &TabId) {
|
||||||
let Some(session) = self.sessions.remove(tab_id) else {
|
let Some(session) = self.sessions.remove(tab_id) else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
sync::atomic::{AtomicUsize, Ordering},
|
sync::atomic::{AtomicUsize, Ordering},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
|
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
|
||||||
@@ -9,6 +10,7 @@ use crate::{
|
|||||||
services::ProfileDataMode,
|
services::ProfileDataMode,
|
||||||
shell::{
|
shell::{
|
||||||
WebSurfaceStore,
|
WebSurfaceStore,
|
||||||
|
web_surface_cadence::{ACTIVE_POLL_INTERVAL, IDLE_POLL_INTERVAL},
|
||||||
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
|
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
|
||||||
web_surface_state::{WebSurfaceInputOutcome, WebSurfacePendingInput},
|
web_surface_state::{WebSurfaceInputOutcome, WebSurfacePendingInput},
|
||||||
web_surface_worker::{LiveRuntimeClient, LiveRuntimeClientError},
|
web_surface_worker::{LiveRuntimeClient, LiveRuntimeClientError},
|
||||||
@@ -111,6 +113,30 @@ fn unchanged_surface_without_input_skips_runtime_ensure() -> Result<(), String>
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn sidecar_exit_removes_dead_runtime_client() -> Result<(), String> {
|
fn sidecar_exit_removes_dead_runtime_client() -> Result<(), String> {
|
||||||
RECOVERY_FACTORY_COUNT.store(0, Ordering::SeqCst);
|
RECOVERY_FACTORY_COUNT.store(0, Ordering::SeqCst);
|
||||||
|
|||||||
Reference in New Issue
Block a user