perf(web-surface): throttle idle Servo polling
This commit is contained in:
@@ -15,7 +15,8 @@ use gpui::Context;
|
|||||||
|
|
||||||
use crate::services::servo_profile_data::{default_profile_data_root, profile_data_dir};
|
use crate::services::servo_profile_data::{default_profile_data_root, profile_data_dir};
|
||||||
|
|
||||||
use super::{ElyShell, ShellState, SyncStateUpdate};
|
use super::sync_state::{SyncStateUpdate, sync_platform_label};
|
||||||
|
use super::{ElyShell, ShellState};
|
||||||
|
|
||||||
/// Where the user is in the email OTP form. Tracked on `ElyShell` so
|
/// Where the user is in the email OTP form. Tracked on `ElyShell` so
|
||||||
/// the Sync settings page can pick the right widget cluster (only the
|
/// the Sync settings page can pick the right widget cluster (only the
|
||||||
@@ -131,7 +132,7 @@ impl ElyShell {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let profile_dir = profile_data_dir(&profile_root, &active_profile_id);
|
let profile_dir = profile_data_dir(&profile_root, &active_profile_id);
|
||||||
match SyncEngine::for_profile_dir(&profile_dir, "ELY", super::sync_platform_label()) {
|
match SyncEngine::for_profile_dir(&profile_dir, "ELY", sync_platform_label()) {
|
||||||
Ok(mut engine) => {
|
Ok(mut engine) => {
|
||||||
let _ = engine.install_bearer("");
|
let _ = engine.install_bearer("");
|
||||||
}
|
}
|
||||||
@@ -210,8 +211,7 @@ fn spawn_verify_otp(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut engine =
|
let mut engine =
|
||||||
match SyncEngine::for_profile_dir(&profile_dir, "ELY", super::sync_platform_label())
|
match SyncEngine::for_profile_dir(&profile_dir, "ELY", sync_platform_label()) {
|
||||||
{
|
|
||||||
Ok(engine) => engine,
|
Ok(engine) => engine,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let _ = tx.send(SyncStateUpdate::AuthError {
|
let _ = tx.send(SyncStateUpdate::AuthError {
|
||||||
|
|||||||
@@ -16,19 +16,23 @@ mod plugins;
|
|||||||
mod reading_list;
|
mod reading_list;
|
||||||
mod render;
|
mod render;
|
||||||
mod settings_actions;
|
mod settings_actions;
|
||||||
|
mod shell_actions;
|
||||||
mod shortcut_files;
|
mod shortcut_files;
|
||||||
mod sidebar;
|
mod sidebar;
|
||||||
mod site_permissions;
|
mod site_permissions;
|
||||||
mod space_files;
|
mod space_files;
|
||||||
mod spaces;
|
mod spaces;
|
||||||
mod splits;
|
mod splits;
|
||||||
|
mod sync_state;
|
||||||
mod tab_groups;
|
mod tab_groups;
|
||||||
mod tab_lifecycle;
|
mod tab_lifecycle;
|
||||||
mod web_surface;
|
mod web_surface;
|
||||||
|
mod web_surface_cadence;
|
||||||
mod web_surface_controller;
|
mod web_surface_controller;
|
||||||
mod web_surface_frame;
|
mod web_surface_frame;
|
||||||
mod web_surface_geometry;
|
mod web_surface_geometry;
|
||||||
mod web_surface_keyboard;
|
mod web_surface_keyboard;
|
||||||
|
mod web_surface_metadata;
|
||||||
mod web_surface_permissions;
|
mod web_surface_permissions;
|
||||||
mod web_surface_runtime;
|
mod web_surface_runtime;
|
||||||
mod web_surface_state;
|
mod web_surface_state;
|
||||||
@@ -51,14 +55,9 @@ use bookmarks::PendingBookmarkEdit;
|
|||||||
use downloads::PendingDownloadFileAction;
|
use downloads::PendingDownloadFileAction;
|
||||||
use history::{PendingHistoryDomainClear, PendingHistoryTimeClear};
|
use history::{PendingHistoryDomainClear, PendingHistoryTimeClear};
|
||||||
use plugins::{PendingPluginInstall, PendingPluginUninstall};
|
use plugins::{PendingPluginInstall, PendingPluginUninstall};
|
||||||
|
use sync_state::SyncStateUpdate;
|
||||||
use web_surface::WebSurfaceStore;
|
use web_surface::WebSurfaceStore;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
CloseCurrentTab, DownloadCurrentPage, FocusAddressBar, FocusCommandMode, OpenDownloads,
|
|
||||||
OpenHistory, OpenNewTab, OpenSettings, OpenTaskManager, ResetZoom, RestoreClosedTab,
|
|
||||||
SelectNextTab, SelectPreviousTab, ToggleFavoriteTab, TogglePinnedTab, ZoomIn, ZoomOut,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum ShellState {
|
enum ShellState {
|
||||||
Ready(Box<BrowserCore>),
|
Ready(Box<BrowserCore>),
|
||||||
StartupError(String),
|
StartupError(String),
|
||||||
@@ -113,36 +112,6 @@ pub struct ElyShell {
|
|||||||
_translucency_subscription: Subscription,
|
_translucency_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Messages the off-thread sync workers push back to the shell so
|
|
||||||
/// `SyncConnectionState` on `BrowserCore` and the in-flight auth
|
|
||||||
/// form reflect live state without the UI thread ever touching the
|
|
||||||
/// network. `SignedIn` is the initial-probe state set synchronously
|
|
||||||
/// on shell startup and does not flow through this channel.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) enum SyncStateUpdate {
|
|
||||||
SignedOut,
|
|
||||||
AwaitingDeviceApproval,
|
|
||||||
SyncReady { last_synced_at_secs: u64 },
|
|
||||||
SyncError { message: String },
|
|
||||||
AuthOtpSent { email: String },
|
|
||||||
AuthSucceeded { email: String },
|
|
||||||
AuthError { email: String, message: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stable label for the current OS used by the device registration
|
|
||||||
/// payload. Defined once here so every off-thread call site agrees.
|
|
||||||
pub(crate) const fn sync_platform_label() -> &'static str {
|
|
||||||
if cfg!(target_os = "macos") {
|
|
||||||
"macos"
|
|
||||||
} else if cfg!(target_os = "windows") {
|
|
||||||
"windows"
|
|
||||||
} else if cfg!(target_os = "linux") {
|
|
||||||
"linux"
|
|
||||||
} else {
|
|
||||||
"other"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
Self::new_with_config(InitialBrowserConfig::ely_defaults(), window, cx)
|
Self::new_with_config(InitialBrowserConfig::ely_defaults(), window, cx)
|
||||||
@@ -486,129 +455,6 @@ impl ElyShell {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_close_current_tab(
|
|
||||||
&mut self,
|
|
||||||
_: &CloseCurrentTab,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.close_active_tab(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_focus_address_bar(
|
|
||||||
&mut self,
|
|
||||||
_: &FocusAddressBar,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.focus_address_bar(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_focus_command_mode(
|
|
||||||
&mut self,
|
|
||||||
_: &FocusCommandMode,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.focus_command_mode(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_open_new_tab(&mut self, _: &OpenNewTab, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open_new_tab(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_open_downloads(
|
|
||||||
&mut self,
|
|
||||||
_: &OpenDownloads,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.open_downloads(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_download_current_page(
|
|
||||||
&mut self,
|
|
||||||
_: &DownloadCurrentPage,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.download_active_tab(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_open_history(&mut self, _: &OpenHistory, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open_history(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_open_settings(&mut self, _: &OpenSettings, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.open_settings(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_open_task_manager(
|
|
||||||
&mut self,
|
|
||||||
_: &OpenTaskManager,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.open_task_manager(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_restore_closed_tab(
|
|
||||||
&mut self,
|
|
||||||
_: &RestoreClosedTab,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.restore_closed_tab(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_reset_zoom(&mut self, _: &ResetZoom, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.reset_active_tab_zoom(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_select_next_tab(
|
|
||||||
&mut self,
|
|
||||||
_: &SelectNextTab,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.select_next_tab(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_select_previous_tab(
|
|
||||||
&mut self,
|
|
||||||
_: &SelectPreviousTab,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.select_previous_tab(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_toggle_favorite_tab(
|
|
||||||
&mut self,
|
|
||||||
_: &ToggleFavoriteTab,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.toggle_active_tab_favorite(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_toggle_pinned_tab(
|
|
||||||
&mut self,
|
|
||||||
_: &TogglePinnedTab,
|
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.toggle_active_tab_pinned(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_zoom_in(&mut self, _: &ZoomIn, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.zoom_active_tab_in(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_zoom_out(&mut self, _: &ZoomOut, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.zoom_active_tab_out(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_external_web_surface_timer(cx: &mut Context<ElyShell>) {
|
fn start_external_web_surface_timer(cx: &mut Context<ElyShell>) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use gpui_component::slider::SliderValue;
|
|||||||
|
|
||||||
use crate::services::servo_profile_data::{default_profile_data_root, profile_data_dir};
|
use crate::services::servo_profile_data::{default_profile_data_root, profile_data_dir};
|
||||||
|
|
||||||
|
use super::sync_state::{SyncStateUpdate, sync_platform_label};
|
||||||
use super::{ElyShell, ShellState};
|
use super::{ElyShell, ShellState};
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
@@ -316,25 +317,25 @@ fn run_sync_upload(
|
|||||||
profile_dir: std::path::PathBuf,
|
profile_dir: std::path::PathBuf,
|
||||||
device_name: String,
|
device_name: String,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
inbox: std::sync::mpsc::Sender<super::SyncStateUpdate>,
|
inbox: std::sync::mpsc::Sender<SyncStateUpdate>,
|
||||||
) {
|
) {
|
||||||
let mut engine = match SyncEngine::for_profile_dir(
|
let mut engine = match SyncEngine::for_profile_dir(
|
||||||
&profile_dir,
|
&profile_dir,
|
||||||
device_name,
|
device_name,
|
||||||
super::sync_platform_label(),
|
sync_platform_label(),
|
||||||
) {
|
) {
|
||||||
Ok(engine) => engine,
|
Ok(engine) => engine,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let message = error.to_string();
|
let message = error.to_string();
|
||||||
tracing::warn!(target: "ely::sync", error = %message, "could not initialise sync engine");
|
tracing::warn!(target: "ely::sync", error = %message, "could not initialise sync engine");
|
||||||
let _ = inbox.send(super::SyncStateUpdate::SyncError { message });
|
let _ = inbox.send(SyncStateUpdate::SyncError { message });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match engine.upload_bytes(bytes) {
|
match engine.upload_bytes(bytes) {
|
||||||
Ok(ely_browser_core::SyncOutcome::SignedOut) => {
|
Ok(ely_browser_core::SyncOutcome::SignedOut) => {
|
||||||
tracing::info!(target: "ely::sync", "no bearer token on disk; sync skipped");
|
tracing::info!(target: "ely::sync", "no bearer token on disk; sync skipped");
|
||||||
let _ = inbox.send(super::SyncStateUpdate::SignedOut);
|
let _ = inbox.send(SyncStateUpdate::SignedOut);
|
||||||
}
|
}
|
||||||
Ok(ely_browser_core::SyncOutcome::Uploaded {
|
Ok(ely_browser_core::SyncOutcome::Uploaded {
|
||||||
snapshot_id,
|
snapshot_id,
|
||||||
@@ -354,15 +355,15 @@ fn run_sync_upload(
|
|||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let _ = inbox.send(super::SyncStateUpdate::SyncReady { last_synced_at_secs });
|
let _ = inbox.send(SyncStateUpdate::SyncReady { last_synced_at_secs });
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let message = error.to_string();
|
let message = error.to_string();
|
||||||
tracing::warn!(target: "ely::sync", error = %message, "snapshot upload failed");
|
tracing::warn!(target: "ely::sync", error = %message, "snapshot upload failed");
|
||||||
let update = if message.contains("device_not_approved") {
|
let update = if message.contains("device_not_approved") {
|
||||||
super::SyncStateUpdate::AwaitingDeviceApproval
|
SyncStateUpdate::AwaitingDeviceApproval
|
||||||
} else {
|
} else {
|
||||||
super::SyncStateUpdate::SyncError { message }
|
SyncStateUpdate::SyncError { message }
|
||||||
};
|
};
|
||||||
let _ = inbox.send(update);
|
let _ = inbox.send(update);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
use gpui::{Context, Window};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
CloseCurrentTab, DownloadCurrentPage, FocusAddressBar, FocusCommandMode, OpenDownloads,
|
||||||
|
OpenHistory, OpenNewTab, OpenSettings, OpenTaskManager, ResetZoom, RestoreClosedTab,
|
||||||
|
SelectNextTab, SelectPreviousTab, ToggleFavoriteTab, TogglePinnedTab, ZoomIn, ZoomOut,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ElyShell;
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn on_close_current_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &CloseCurrentTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.close_active_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_focus_address_bar(
|
||||||
|
&mut self,
|
||||||
|
_: &FocusAddressBar,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.focus_address_bar(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_focus_command_mode(
|
||||||
|
&mut self,
|
||||||
|
_: &FocusCommandMode,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.focus_command_mode(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_open_new_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &OpenNewTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.open_new_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_open_downloads(
|
||||||
|
&mut self,
|
||||||
|
_: &OpenDownloads,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.open_downloads(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_download_current_page(
|
||||||
|
&mut self,
|
||||||
|
_: &DownloadCurrentPage,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.download_active_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_open_history(
|
||||||
|
&mut self,
|
||||||
|
_: &OpenHistory,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.open_history(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_open_settings(
|
||||||
|
&mut self,
|
||||||
|
_: &OpenSettings,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.open_settings(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_open_task_manager(
|
||||||
|
&mut self,
|
||||||
|
_: &OpenTaskManager,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.open_task_manager(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_restore_closed_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &RestoreClosedTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.restore_closed_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_reset_zoom(&mut self, _: &ResetZoom, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.reset_active_tab_zoom(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_select_next_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &SelectNextTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.select_next_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_select_previous_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &SelectPreviousTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.select_previous_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_toggle_favorite_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &ToggleFavoriteTab,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.toggle_active_tab_favorite(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_toggle_pinned_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &TogglePinnedTab,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.toggle_active_tab_pinned(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_zoom_in(&mut self, _: &ZoomIn, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.zoom_active_tab_in(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_zoom_out(&mut self, _: &ZoomOut, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.zoom_active_tab_out(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/// Messages the off-thread sync workers push back to the shell so
|
||||||
|
/// `SyncConnectionState` on `BrowserCore` and the in-flight auth
|
||||||
|
/// form reflect live state without the UI thread ever touching the
|
||||||
|
/// network. `SignedIn` is the initial-probe state set synchronously
|
||||||
|
/// on shell startup and does not flow through this channel.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum SyncStateUpdate {
|
||||||
|
SignedOut,
|
||||||
|
AwaitingDeviceApproval,
|
||||||
|
SyncReady { last_synced_at_secs: u64 },
|
||||||
|
SyncError { message: String },
|
||||||
|
AuthOtpSent { email: String },
|
||||||
|
AuthSucceeded { email: String },
|
||||||
|
AuthError { email: String, message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stable label for the current OS used by the device registration
|
||||||
|
/// payload. Defined once here so every off-thread call site agrees.
|
||||||
|
pub(crate) const fn sync_platform_label() -> &'static str {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
"macos"
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"windows"
|
||||||
|
} else if cfg!(target_os = "linux") {
|
||||||
|
"linux"
|
||||||
|
} else {
|
||||||
|
"other"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::{collections::BTreeMap, time::Instant};
|
||||||
|
|
||||||
use ely_domain::{BrowserTab, TabId};
|
use ely_domain::{BrowserTab, TabId};
|
||||||
use gpui::{Bounds, Pixels, Point};
|
use gpui::{Bounds, Pixels, Point};
|
||||||
|
|
||||||
use crate::services::ProfileDataMode;
|
use crate::services::ProfileDataMode;
|
||||||
|
|
||||||
|
use super::web_surface_metadata::WebSurfacePageMetadata;
|
||||||
use super::{
|
use super::{
|
||||||
web_surface_frame::WebSurfaceFrame,
|
web_surface_frame::WebSurfaceFrame,
|
||||||
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceSize},
|
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceSize},
|
||||||
@@ -17,33 +18,6 @@ use super::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One page's worth of metadata observed in a Ready frame. The
|
|
||||||
/// controller applies these to the `BrowserTab` (title / favicon_key)
|
|
||||||
/// after the frame has been swapped into the surface state. Title and
|
|
||||||
/// favicon are independent — a navigation typically settles the URL
|
|
||||||
/// first, then Servo emits a title change a frame or two later, and
|
|
||||||
/// the favicon URL is derived from the loaded URL.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub(super) struct WebSurfacePageMetadata {
|
|
||||||
pub(super) tab_id: TabId,
|
|
||||||
pub(super) title: Option<String>,
|
|
||||||
pub(super) favicon_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebSurfacePageMetadata {
|
|
||||||
fn from_frame(tab_id: &TabId, frame: &WebSurfaceFrame) -> Option<Self> {
|
|
||||||
let title = frame.title().map(str::to_string);
|
|
||||||
let favicon_url = frame
|
|
||||||
.loaded_url()
|
|
||||||
.and_then(|loaded| ely_domain::UrlText::parse(loaded).ok())
|
|
||||||
.and_then(|url| url.favicon_url());
|
|
||||||
if title.is_none() && favicon_url.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Self { tab_id: tab_id.clone(), title, favicon_url })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) struct WebSurfaceStore {
|
pub(super) struct WebSurfaceStore {
|
||||||
runtime: WebSurfaceRuntime,
|
runtime: WebSurfaceRuntime,
|
||||||
/// Single owner of every per-tab invariant. See [`PerTabSurface`].
|
/// Single owner of every per-tab invariant. See [`PerTabSurface`].
|
||||||
@@ -125,11 +99,6 @@ impl WebSurfaceStore {
|
|||||||
match self.initial_display_gate_message(&tab_id, &frame, had_ready) {
|
match self.initial_display_gate_message(&tab_id, &frame, had_ready) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
// Only transition to Failed when there is
|
|
||||||
// nothing on screen yet — once a real frame
|
|
||||||
// has rendered, transient gate failures
|
|
||||||
// (e.g. a stray empty-paint pass) must not
|
|
||||||
// wipe it out.
|
|
||||||
if !had_ready {
|
if !had_ready {
|
||||||
self.surface_mut(&tab_id).state =
|
self.surface_mut(&tab_id).state =
|
||||||
Some(WebSurfaceState::Failed { message });
|
Some(WebSurfaceState::Failed { message });
|
||||||
@@ -163,12 +132,6 @@ impl WebSurfaceStore {
|
|||||||
Some(WebSurfaceState::Ready(_))
|
Some(WebSurfaceState::Ready(_))
|
||||||
);
|
);
|
||||||
if had_ready {
|
if had_ready {
|
||||||
// Keep the last good frame on screen — the
|
|
||||||
// worker emits Failed for any transient ensure
|
|
||||||
// / poll error (parse glitch, momentary IPC
|
|
||||||
// hiccup) and downgrading every one of them
|
|
||||||
// strobes the page. The error still surfaces
|
|
||||||
// through `tracing` for diagnostics.
|
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "ely::web_surface",
|
target: "ely::web_surface",
|
||||||
tab_id = %tab_id,
|
tab_id = %tab_id,
|
||||||
@@ -238,12 +201,6 @@ impl WebSurfaceStore {
|
|||||||
});
|
});
|
||||||
surface.pending_scroll_point = Some(point);
|
surface.pending_scroll_point = Some(point);
|
||||||
surface.mark_pending_input_started();
|
surface.mark_pending_input_started();
|
||||||
// Drop any buffered click — its viewport coordinates were
|
|
||||||
// captured against the pre-scroll page, so applying it after
|
|
||||||
// the scroll would land on the wrong DOM element. Keep
|
|
||||||
// `keyboard_focus` and `typed_text` though: Servo maintains
|
|
||||||
// its own DOM focus across scrolls, so a focused input keeps
|
|
||||||
// accepting the user's keystrokes after they wheel-scroll.
|
|
||||||
surface.click_point = None;
|
surface.click_point = None;
|
||||||
WebSurfaceInputOutcome::Applied
|
WebSurfaceInputOutcome::Applied
|
||||||
}
|
}
|
||||||
@@ -278,6 +235,16 @@ impl WebSurfaceStore {
|
|||||||
tab_id: &TabId,
|
tab_id: &TabId,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
scale_factor: f32,
|
scale_factor: f32,
|
||||||
|
) -> WebSurfaceInputOutcome {
|
||||||
|
self.record_hover_point_at(tab_id, position, scale_factor, Instant::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_hover_point_at(
|
||||||
|
&mut self,
|
||||||
|
tab_id: &TabId,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
scale_factor: f32,
|
||||||
|
now: Instant,
|
||||||
) -> WebSurfaceInputOutcome {
|
) -> WebSurfaceInputOutcome {
|
||||||
let Some(surface) = self.surfaces.get_mut(tab_id) else {
|
let Some(surface) = self.surfaces.get_mut(tab_id) else {
|
||||||
return WebSurfaceInputOutcome::DroppedNoViewportBounds;
|
return WebSurfaceInputOutcome::DroppedNoViewportBounds;
|
||||||
@@ -290,7 +257,14 @@ impl WebSurfaceStore {
|
|||||||
else {
|
else {
|
||||||
return WebSurfaceInputOutcome::DroppedOutOfBounds;
|
return WebSurfaceInputOutcome::DroppedOutOfBounds;
|
||||||
};
|
};
|
||||||
|
if surface.hover_point == Some(point) {
|
||||||
|
return WebSurfaceInputOutcome::NoChange;
|
||||||
|
}
|
||||||
|
if surface.hover_is_throttled(now) {
|
||||||
|
return WebSurfaceInputOutcome::NoChange;
|
||||||
|
}
|
||||||
surface.hover_point = Some(point);
|
surface.hover_point = Some(point);
|
||||||
|
surface.mark_hover_enqueued(now);
|
||||||
WebSurfaceInputOutcome::Applied
|
WebSurfaceInputOutcome::Applied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
const ACTIVE_POLL_INTERVAL: Duration = Duration::from_millis(8);
|
||||||
|
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);
|
||||||
|
const FRAME_SETTLE_WINDOW: Duration = Duration::from_millis(250);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub(super) struct WebSurfacePollCadence {
|
||||||
|
next_poll_at: Option<Instant>,
|
||||||
|
active_until: Option<Instant>,
|
||||||
|
last_render_phase: Option<WebSurfaceRenderPhase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSurfacePollCadence {
|
||||||
|
pub(super) fn note_ensure(
|
||||||
|
&mut self,
|
||||||
|
input_kind: WebSurfaceInputKind,
|
||||||
|
started_loading: bool,
|
||||||
|
now: Instant,
|
||||||
|
) {
|
||||||
|
if started_loading {
|
||||||
|
self.last_render_phase = None;
|
||||||
|
self.boost_until(now + LOAD_BOOST_WINDOW);
|
||||||
|
}
|
||||||
|
match input_kind {
|
||||||
|
WebSurfaceInputKind::Idle => {}
|
||||||
|
WebSurfaceInputKind::Hover => self.boost_until(now + HOVER_BOOST_WINDOW),
|
||||||
|
WebSurfaceInputKind::Scroll
|
||||||
|
| WebSurfaceInputKind::Click
|
||||||
|
| WebSurfaceInputKind::Text => {
|
||||||
|
self.boost_until(now + INPUT_BOOST_WINDOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn note_frame(&mut self, render_state: &str, now: Instant) {
|
||||||
|
let phase = WebSurfaceRenderPhase::from_render_state(render_state);
|
||||||
|
match phase {
|
||||||
|
WebSurfaceRenderPhase::Created | WebSurfaceRenderPhase::Loading => {
|
||||||
|
self.boost_until(now + LOAD_BOOST_WINDOW);
|
||||||
|
}
|
||||||
|
WebSurfaceRenderPhase::Complete if self.last_render_phase != Some(phase) => {
|
||||||
|
self.boost_until(now + FRAME_SETTLE_WINDOW);
|
||||||
|
}
|
||||||
|
WebSurfaceRenderPhase::Complete => {}
|
||||||
|
WebSurfaceRenderPhase::Other => {
|
||||||
|
self.boost_until(now + INPUT_BOOST_WINDOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.last_render_phase = Some(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn should_poll(&self, now: Instant) -> bool {
|
||||||
|
self.next_poll_at.is_none_or(|next| now >= next)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn note_poll_submitted(&mut self, now: Instant) {
|
||||||
|
self.next_poll_at = Some(now + self.current_interval(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_interval(&self, now: Instant) -> Duration {
|
||||||
|
if self.active_until.is_some_and(|deadline| now < deadline) {
|
||||||
|
ACTIVE_POLL_INTERVAL
|
||||||
|
} else {
|
||||||
|
IDLE_POLL_INTERVAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boost_until(&mut self, deadline: Instant) {
|
||||||
|
if self.active_until.is_none_or(|current| deadline > current) {
|
||||||
|
self.active_until = Some(deadline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum WebSurfaceRenderPhase {
|
||||||
|
Created,
|
||||||
|
Loading,
|
||||||
|
Complete,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSurfaceRenderPhase {
|
||||||
|
fn from_render_state(render_state: &str) -> Self {
|
||||||
|
match render_state {
|
||||||
|
"created" => Self::Created,
|
||||||
|
"loading" => Self::Loading,
|
||||||
|
"complete" => Self::Complete,
|
||||||
|
_ => Self::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(super) enum WebSurfaceInputKind {
|
||||||
|
Idle,
|
||||||
|
Scroll,
|
||||||
|
Click,
|
||||||
|
Hover,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSurfaceInputKind {
|
||||||
|
pub(super) fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Idle => "idle",
|
||||||
|
Self::Scroll => "scroll",
|
||||||
|
Self::Click => "click",
|
||||||
|
Self::Hover => "hover",
|
||||||
|
Self::Text => "text",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use super::{WebSurfaceInputKind, WebSurfacePollCadence};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idle_poll_uses_low_frequency_after_submission() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut cadence = WebSurfacePollCadence::default();
|
||||||
|
|
||||||
|
cadence.note_poll_submitted(start);
|
||||||
|
|
||||||
|
assert!(!cadence.should_poll(start + Duration::from_millis(79)));
|
||||||
|
assert!(cadence.should_poll(start + Duration::from_millis(80)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_input_uses_active_frame_cadence() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut cadence = WebSurfacePollCadence::default();
|
||||||
|
|
||||||
|
cadence.note_ensure(WebSurfaceInputKind::Scroll, false, start);
|
||||||
|
cadence.note_poll_submitted(start);
|
||||||
|
|
||||||
|
assert!(!cadence.should_poll(start + Duration::from_millis(7)));
|
||||||
|
assert!(cadence.should_poll(start + Duration::from_millis(8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complete_frames_settle_then_return_to_idle_cadence() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut cadence = WebSurfacePollCadence::default();
|
||||||
|
|
||||||
|
cadence.note_frame("complete", start);
|
||||||
|
cadence.note_poll_submitted(start + Duration::from_millis(300));
|
||||||
|
|
||||||
|
assert!(!cadence.should_poll(start + Duration::from_millis(379)));
|
||||||
|
assert!(cadence.should_poll(start + Duration::from_millis(380)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_complete_frames_do_not_extend_settle_window() {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut cadence = WebSurfacePollCadence::default();
|
||||||
|
|
||||||
|
cadence.note_frame("complete", start);
|
||||||
|
cadence.note_frame("complete", start + Duration::from_millis(200));
|
||||||
|
cadence.note_poll_submitted(start + Duration::from_millis(260));
|
||||||
|
|
||||||
|
assert!(!cadence.should_poll(start + Duration::from_millis(339)));
|
||||||
|
assert!(cadence.should_poll(start + Duration::from_millis(340)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ use crate::services::ProfileDataMode;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ElyShell,
|
ElyShell,
|
||||||
web_surface::WebSurfacePageMetadata,
|
web_surface_metadata::WebSurfacePageMetadata,
|
||||||
web_surface_permissions::web_surface_site_permissions_for_tab,
|
web_surface_permissions::web_surface_site_permissions_for_tab,
|
||||||
web_surface_runtime::{WebSurfaceUrlChange, WebSurfaceUrlChangeKind},
|
web_surface_runtime::{WebSurfaceUrlChange, WebSurfaceUrlChangeKind},
|
||||||
web_surface_state::{WebSurfaceInputOutcome, WebSurfaceState},
|
web_surface_state::{WebSurfaceInputOutcome, WebSurfaceState},
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use ely_domain::TabId;
|
||||||
|
|
||||||
|
use super::web_surface_frame::WebSurfaceFrame;
|
||||||
|
|
||||||
|
/// One page's worth of metadata observed in a Ready frame. The
|
||||||
|
/// controller applies these to the `BrowserTab` after the frame has
|
||||||
|
/// been swapped into the surface state. Title and favicon are
|
||||||
|
/// independent: navigation often settles the URL first, then Servo
|
||||||
|
/// emits a title change a frame or two later.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(super) struct WebSurfacePageMetadata {
|
||||||
|
pub(super) tab_id: TabId,
|
||||||
|
pub(super) title: Option<String>,
|
||||||
|
pub(super) favicon_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSurfacePageMetadata {
|
||||||
|
pub(super) fn from_frame(tab_id: &TabId, frame: &WebSurfaceFrame) -> Option<Self> {
|
||||||
|
let title = frame.title().map(str::to_string);
|
||||||
|
let favicon_url = frame
|
||||||
|
.loaded_url()
|
||||||
|
.and_then(|loaded| ely_domain::UrlText::parse(loaded).ok())
|
||||||
|
.and_then(|url| url.favicon_url());
|
||||||
|
if title.is_none() && favicon_url.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Self { tab_id: tab_id.clone(), title, favicon_url })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use crate::services::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
web_surface_cadence::{WebSurfaceInputKind, WebSurfacePollCadence},
|
||||||
web_surface_frame::WebSurfaceFrame,
|
web_surface_frame::WebSurfaceFrame,
|
||||||
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
|
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
|
||||||
web_surface_permissions::WebSurfaceSitePermission,
|
web_surface_permissions::WebSurfaceSitePermission,
|
||||||
@@ -69,6 +70,7 @@ impl WebSurfaceRuntime {
|
|||||||
session.size = size;
|
session.size = size;
|
||||||
session.zoom_percent = zoom_percent;
|
session.zoom_percent = zoom_percent;
|
||||||
session.scroll_offset = next_scroll_offset;
|
session.scroll_offset = next_scroll_offset;
|
||||||
|
session.cadence.note_ensure(input_kind, started_loading, Instant::now());
|
||||||
started_loading
|
started_loading
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,29 +98,16 @@ impl WebSurfaceRuntime {
|
|||||||
return Err("Servo worker was created but is no longer registered".to_string());
|
return Err("Servo worker was created but is no longer registered".to_string());
|
||||||
};
|
};
|
||||||
scoped.worker.submit_ensure(request);
|
scoped.worker.submit_ensure(request);
|
||||||
log_ensure_submitted(tab, size, input_kind, enqueued_at, started_loading);
|
log_ensure_submitted(tab, size, input_kind.label(), enqueued_at, started_loading);
|
||||||
|
|
||||||
Ok(WebSurfaceEnsureResult { requested_url, started_loading })
|
Ok(WebSurfaceEnsureResult { requested_url, started_loading })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec<WebSurfaceRuntimeFrame> {
|
pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec<WebSurfaceRuntimeFrame> {
|
||||||
// Submit a Poll for every visible tab whose session is live so
|
|
||||||
// animations / JS-driven content keep advancing without user
|
|
||||||
// input. The worker coalesces — a Poll never overrides a
|
|
||||||
// pending Ensure — so this stays cheap even at 120 Hz.
|
|
||||||
for tab_id in visible_tab_ids {
|
|
||||||
let Some(session) = self.sessions.get(tab_id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(scoped) = self.workers.get(&session.scope) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
scoped.worker.submit_poll(tab_id.as_str().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
let mut dead_scopes = Vec::new();
|
let mut dead_scopes = Vec::new();
|
||||||
let scopes: Vec<WebSurfaceRuntimeScope> = self.workers.keys().cloned().collect();
|
let scopes: Vec<WebSurfaceRuntimeScope> = self.workers.keys().cloned().collect();
|
||||||
|
let now = Instant::now();
|
||||||
for scope in scopes {
|
for scope in scopes {
|
||||||
let responses = self
|
let responses = self
|
||||||
.workers
|
.workers
|
||||||
@@ -138,6 +127,7 @@ impl WebSurfaceRuntime {
|
|||||||
let requested_url = session.requested_url.clone();
|
let requested_url = session.requested_url.clone();
|
||||||
let scroll_offset = session.scroll_offset;
|
let scroll_offset = session.scroll_offset;
|
||||||
let zoom_percent = session.zoom_percent;
|
let zoom_percent = session.zoom_percent;
|
||||||
|
session.cadence.note_frame(frame.render_state(), now);
|
||||||
match WebSurfaceFrame::from_live_frame(
|
match WebSurfaceFrame::from_live_frame(
|
||||||
requested_url.clone(),
|
requested_url.clone(),
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
@@ -176,6 +166,22 @@ impl WebSurfaceRuntime {
|
|||||||
self.workers.remove(&scope);
|
self.workers.remove(&scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let poll_now = Instant::now();
|
||||||
|
for tab_id in visible_tab_ids {
|
||||||
|
let Some(session) = self.sessions.get_mut(tab_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !session.cadence.should_poll(poll_now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frames
|
frames
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +275,7 @@ pub(super) struct WebSurfaceSession {
|
|||||||
pub(super) zoom_percent: u16,
|
pub(super) zoom_percent: u16,
|
||||||
pub(super) scroll_offset: WebSurfaceScrollOffset,
|
pub(super) scroll_offset: WebSurfaceScrollOffset,
|
||||||
pub(super) pending_user_navigation: bool,
|
pub(super) pending_user_navigation: bool,
|
||||||
|
pub(super) cadence: WebSurfacePollCadence,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebSurfaceSession {
|
impl WebSurfaceSession {
|
||||||
@@ -280,6 +287,7 @@ impl WebSurfaceSession {
|
|||||||
zoom_percent: 0,
|
zoom_percent: 0,
|
||||||
scroll_offset: WebSurfaceScrollOffset::default(),
|
scroll_offset: WebSurfaceScrollOffset::default(),
|
||||||
pending_user_navigation: false,
|
pending_user_navigation: false,
|
||||||
|
cadence: WebSurfacePollCadence::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,17 +402,17 @@ fn input_requests_history_navigation(input: &WebSurfacePendingInput) -> bool {
|
|||||||
|| input.typed_text.as_deref().is_some_and(|text| text.contains('\n'))
|
|| input.typed_text.as_deref().is_some_and(|text| text.contains('\n'))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pending_input_kind(input: &WebSurfacePendingInput) -> &'static str {
|
fn pending_input_kind(input: &WebSurfacePendingInput) -> WebSurfaceInputKind {
|
||||||
if input.scroll_delta.is_some() {
|
if input.scroll_delta.is_some() {
|
||||||
"scroll"
|
WebSurfaceInputKind::Scroll
|
||||||
} else if input.click_point.is_some() {
|
} else if input.click_point.is_some() {
|
||||||
"click"
|
WebSurfaceInputKind::Click
|
||||||
} else if input.typed_text.is_some() {
|
} else if input.typed_text.is_some() {
|
||||||
"text"
|
WebSurfaceInputKind::Text
|
||||||
} else if input.hover_point.is_some() {
|
} else if input.hover_point.is_some() {
|
||||||
"hover"
|
WebSurfaceInputKind::Hover
|
||||||
} else {
|
} else {
|
||||||
"idle"
|
WebSurfaceInputKind::Idle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use ely_domain::TabId;
|
use ely_domain::TabId;
|
||||||
use gpui::{Bounds, Pixels};
|
use gpui::{Bounds, Pixels};
|
||||||
@@ -116,6 +116,7 @@ pub(super) struct PerTabSurface {
|
|||||||
pub(super) viewport_size: Option<WebSurfaceSize>,
|
pub(super) viewport_size: Option<WebSurfaceSize>,
|
||||||
pub(super) last_ensure_key: Option<WebSurfaceEnsureKey>,
|
pub(super) last_ensure_key: Option<WebSurfaceEnsureKey>,
|
||||||
pub(super) hover_point: Option<WebSurfaceClickPoint>,
|
pub(super) hover_point: Option<WebSurfaceClickPoint>,
|
||||||
|
last_hover_enqueued_at: Option<Instant>,
|
||||||
pub(super) click_point: Option<WebSurfaceClickState>,
|
pub(super) click_point: Option<WebSurfaceClickState>,
|
||||||
pub(super) pending_scroll_delta: Option<WebSurfaceScrollDelta>,
|
pub(super) pending_scroll_delta: Option<WebSurfaceScrollDelta>,
|
||||||
pub(super) pending_scroll_point: Option<WebSurfaceClickPoint>,
|
pub(super) pending_scroll_point: Option<WebSurfaceClickPoint>,
|
||||||
@@ -132,6 +133,7 @@ impl PerTabSurface {
|
|||||||
viewport_size: None,
|
viewport_size: None,
|
||||||
last_ensure_key: None,
|
last_ensure_key: None,
|
||||||
hover_point: None,
|
hover_point: None,
|
||||||
|
last_hover_enqueued_at: None,
|
||||||
click_point: None,
|
click_point: None,
|
||||||
pending_scroll_delta: None,
|
pending_scroll_delta: None,
|
||||||
pending_scroll_point: None,
|
pending_scroll_point: None,
|
||||||
@@ -146,6 +148,15 @@ impl PerTabSurface {
|
|||||||
self.pending_input_started_at.get_or_insert_with(Instant::now);
|
self.pending_input_started_at.get_or_insert_with(Instant::now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn hover_is_throttled(&self, now: Instant) -> bool {
|
||||||
|
self.last_hover_enqueued_at
|
||||||
|
.is_some_and(|last| now.duration_since(last) < HOVER_INPUT_MIN_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mark_hover_enqueued(&mut self, now: Instant) {
|
||||||
|
self.last_hover_enqueued_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()
|
||||||
}
|
}
|
||||||
@@ -171,6 +182,8 @@ impl PerTabSurface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HOVER_INPUT_MIN_INTERVAL: Duration = Duration::from_millis(32);
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub(super) struct WebSurfaceEnsureKey {
|
pub(super) struct WebSurfaceEnsureKey {
|
||||||
requested_url: String,
|
requested_url: String,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::error::Error;
|
use std::{error::Error, time::Duration};
|
||||||
|
|
||||||
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
|
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
|
||||||
use gpui::{Bounds, point, px, size};
|
use gpui::{Bounds, point, px, size};
|
||||||
@@ -218,6 +218,36 @@ fn zero_wheel_delta_reports_zero_delta() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_input_is_rate_limited() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut store = WebSurfaceStore::new();
|
||||||
|
let tab = web_tab("https://example.com/hover")?;
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 1.0));
|
||||||
|
assert_applied(store.record_hover_point_at(tab.id(), point(px(10.0), px(10.0)), 1.0, start));
|
||||||
|
assert_eq!(
|
||||||
|
store.record_hover_point_at(
|
||||||
|
tab.id(),
|
||||||
|
point(px(12.0), px(12.0)),
|
||||||
|
1.0,
|
||||||
|
start + Duration::from_millis(8),
|
||||||
|
),
|
||||||
|
WebSurfaceInputOutcome::NoChange,
|
||||||
|
);
|
||||||
|
assert_applied(store.record_hover_point_at(
|
||||||
|
tab.id(),
|
||||||
|
point(px(44.0), px(45.0)),
|
||||||
|
1.0,
|
||||||
|
start + Duration::from_millis(33),
|
||||||
|
));
|
||||||
|
|
||||||
|
let input = store.take_pending_input(tab.id(), tab.url().as_str());
|
||||||
|
|
||||||
|
assert_eq!(input.hover_point.map(|point| (point.x(), point.y())), Some((44, 45)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Pinning the per-tab isolation invariant. A click recorded against
|
/// Pinning the per-tab isolation invariant. A click recorded against
|
||||||
/// tab A must not be drained by, dropped by, or overwritten by any
|
/// tab A must not be drained by, dropped by, or overwritten by any
|
||||||
/// state mutation routed to tab B. The store keys every click on its
|
/// state mutation routed to tab B. The store keys every click on its
|
||||||
|
|||||||
@@ -162,20 +162,27 @@ impl LiveRuntimeWorker {
|
|||||||
cvar.notify_one();
|
cvar.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn submit_poll(&self, tab_id: String) {
|
pub(super) fn submit_poll(&self, tab_id: String) -> bool {
|
||||||
let (lock, cvar) = &*self.queue;
|
let (lock, cvar) = &*self.queue;
|
||||||
let mut q = match lock.lock() {
|
let mut q = match lock.lock() {
|
||||||
Ok(guard) => guard,
|
Ok(guard) => guard,
|
||||||
Err(poisoned) => poisoned.into_inner(),
|
Err(poisoned) => poisoned.into_inner(),
|
||||||
};
|
};
|
||||||
if q.shutdown {
|
if q.shutdown {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
// A pending Ensure already produces the latest frame after its
|
// A pending Ensure already produces the latest frame after its
|
||||||
// run; don't downgrade it to a Poll. Only insert if nothing is
|
// run; don't downgrade it to a Poll. Only insert if nothing is
|
||||||
// queued.
|
// queued.
|
||||||
q.pending.entry(tab_id.clone()).or_insert(WorkerRequest::Poll { tab_id });
|
let inserted = match q.pending.entry(tab_id.clone()) {
|
||||||
|
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||||
|
entry.insert(WorkerRequest::Poll { tab_id });
|
||||||
|
true
|
||||||
|
}
|
||||||
|
std::collections::btree_map::Entry::Occupied(_) => false,
|
||||||
|
};
|
||||||
cvar.notify_one();
|
cvar.notify_one();
|
||||||
|
inserted
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn submit_close(&self, tab_id: String) {
|
pub(super) fn submit_close(&self, tab_id: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user