Scope Servo live runtimes by profile

This commit is contained in:
2026-05-13 02:07:01 -04:00
parent 05ce4afba4
commit e7b62e2daf
3 changed files with 298 additions and 78 deletions
@@ -31,7 +31,7 @@ pub(crate) fn transient_profile_data_dir(
))) )))
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum ProfileDataMode { pub(crate) enum ProfileDataMode {
Persistent, Persistent,
Transient, Transient,
+180 -77
View File
@@ -16,13 +16,23 @@ use super::{
}; };
pub(super) struct WebSurfaceRuntime { pub(super) struct WebSurfaceRuntime {
state: RuntimeState, clients: BTreeMap<WebSurfaceRuntimeScope, ScopedRuntimeClient>,
sessions: BTreeMap<TabId, WebSurfaceSession>, sessions: BTreeMap<TabId, WebSurfaceSession>,
client_factory: LiveRuntimeClientFactory,
} }
impl WebSurfaceRuntime { impl WebSurfaceRuntime {
pub(super) fn new() -> Self { pub(super) fn new() -> Self {
Self { state: RuntimeState::Empty, sessions: BTreeMap::new() } Self {
clients: BTreeMap::new(),
sessions: BTreeMap::new(),
client_factory: new_servo_live_client,
}
}
#[cfg(test)]
fn new_with_client_factory(client_factory: LiveRuntimeClientFactory) -> Self {
Self { clients: BTreeMap::new(), sessions: BTreeMap::new(), client_factory }
} }
pub(super) fn ensure_tab( pub(super) fn ensure_tab(
@@ -38,27 +48,25 @@ impl WebSurfaceRuntime {
let requested_url = tab.url().as_str().to_string(); let requested_url = tab.url().as_str().to_string();
let zoom_percent = tab.zoom_percent(); let zoom_percent = tab.zoom_percent();
let (state, sessions) = (&mut self.state, &mut self.sessions);
let RuntimeState::Ready { scope: active_scope, client, .. } = state else {
return Err("Servo live runtime is unavailable".to_string());
};
if active_scope != &scope {
return Err(active_scope.error_for(&scope));
}
let (scroll_delta_x, scroll_delta_y, scroll_point_x, scroll_point_y) = let (scroll_delta_x, scroll_delta_y, scroll_point_x, scroll_point_y) =
scroll_wire_fields(input.scroll_delta, input.scroll_point)?; scroll_wire_fields(input.scroll_delta, input.scroll_point)?;
let user_navigation_input = input_requests_history_navigation(&input); let user_navigation_input = input_requests_history_navigation(&input);
let session = sessions.entry(tab.id().clone()).or_insert_with(WebSurfaceSession::default);
let next_scroll_offset = input.scroll_offset; let next_scroll_offset = input.scroll_offset;
let started_loading = session.started_loading(&requested_url, size, zoom_percent); let started_loading = {
if started_loading { let session = session_for_scope(&mut self.sessions, tab.id(), scope.clone());
session.pending_user_navigation = false; let started_loading = session.started_loading(&requested_url, size, zoom_percent);
} if started_loading {
if user_navigation_input { session.pending_user_navigation = false;
session.pending_user_navigation = true; }
} if user_navigation_input {
let frame = client session.pending_user_navigation = true;
}
started_loading
};
let frame = self
.client_for_scope(&scope)?
.ensure(ServoLiveEnsureRequest { .ensure(ServoLiveEnsureRequest {
tab_id: tab.id().as_str().to_string(), tab_id: tab.id().as_str().to_string(),
profile_id: tab.profile_id().as_str().to_string(), profile_id: tab.profile_id().as_str().to_string(),
@@ -77,8 +85,7 @@ impl WebSurfaceRuntime {
hover_y: input.hover_point.map(|point| point.y()), hover_y: input.hover_point.map(|point| point.y()),
typed_text: input.typed_text, typed_text: input.typed_text,
site_permissions: permissions.iter().map(ServoLiveSitePermission::from).collect(), site_permissions: permissions.iter().map(ServoLiveSitePermission::from).collect(),
}) })?
.map_err(|error| error.to_string())?
.map(|frame| { .map(|frame| {
WebSurfaceFrame::from_live_frame( WebSurfaceFrame::from_live_frame(
requested_url.clone(), requested_url.clone(),
@@ -89,10 +96,13 @@ impl WebSurfaceRuntime {
.map_err(|error| error.to_string()) .map_err(|error| error.to_string())
}) })
.transpose()?; .transpose()?;
let url_change = frame let url_change = frame.as_ref().and_then(|frame| {
.as_ref() self.sessions
.and_then(|frame| session.url_change_for(tab.id(), requested_url.as_str(), frame)); .get_mut(tab.id())
.and_then(|session| session.url_change_for(tab.id(), requested_url.as_str(), frame))
});
let session = session_for_scope(&mut self.sessions, tab.id(), scope);
session.requested_url = requested_url.clone(); session.requested_url = requested_url.clone();
session.size = size; session.size = size;
session.zoom_percent = zoom_percent; session.zoom_percent = zoom_percent;
@@ -102,43 +112,37 @@ impl WebSurfaceRuntime {
} }
pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec<WebSurfaceRuntimeFrame> { pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec<WebSurfaceRuntimeFrame> {
let (state, sessions) = (&mut self.state, &mut self.sessions);
let RuntimeState::Ready { client, .. } = state else {
return Vec::new();
};
let mut frames = Vec::new(); let mut frames = Vec::new();
for (tab_id, session) in sessions { for job in visible_poll_jobs(&self.sessions, visible_tab_ids) {
if !visible_tab_ids.iter().any(|visible_tab_id| visible_tab_id == tab_id) { let result = match self.clients.get_mut(&job.scope) {
continue; Some(client) => client.client.poll(job.tab_id.as_str().to_string()),
} None => Err(missing_runtime_message(&job.scope)),
match client.poll(tab_id.as_str().to_string()) { };
match result {
Ok(Some(frame)) => match WebSurfaceFrame::from_live_frame( Ok(Some(frame)) => match WebSurfaceFrame::from_live_frame(
session.requested_url.clone(), job.requested_url.clone(),
session.scroll_offset, job.scroll_offset,
session.zoom_percent, job.zoom_percent,
frame, frame,
) { ) {
Ok(frame) => { Ok(frame) => {
let requested_url = session.requested_url.clone(); let url_change = self.sessions.get_mut(&job.tab_id).and_then(|session| {
let url_change = session.url_change_for(&job.tab_id, job.requested_url.as_str(), &frame)
session.url_change_for(tab_id, requested_url.as_str(), &frame); });
frames.push(WebSurfaceRuntimeFrame::Ready { frames.push(WebSurfaceRuntimeFrame::Ready {
tab_id: tab_id.clone(), tab_id: job.tab_id,
frame: Box::new(frame), frame: Box::new(frame),
url_change, url_change,
}) })
} }
Err(error) => frames.push(WebSurfaceRuntimeFrame::Failed { Err(error) => frames.push(WebSurfaceRuntimeFrame::Failed {
tab_id: tab_id.clone(), tab_id: job.tab_id,
message: error.to_string(), message: error.to_string(),
}), }),
}, },
Ok(None) => {} Ok(None) => {}
Err(error) => frames.push(WebSurfaceRuntimeFrame::Failed { Err(error) => frames
tab_id: tab_id.clone(), .push(WebSurfaceRuntimeFrame::Failed { tab_id: job.tab_id, message: error }),
message: error.to_string(),
}),
} }
} }
@@ -146,38 +150,82 @@ impl WebSurfaceRuntime {
} }
fn ensure_runtime(&mut self, scope: WebSurfaceRuntimeScope) -> Result<(), String> { fn ensure_runtime(&mut self, scope: WebSurfaceRuntimeScope) -> Result<(), String> {
match &self.state { if self.clients.contains_key(&scope) {
RuntimeState::Empty => { return Ok(());
let (config_dir, transient_profile_data_dir) = config_dir_for_scope(&scope)?;
let client = ServoLiveClient::new(config_dir).map_err(|error| error.to_string())?;
self.state = RuntimeState::Ready { scope, client, transient_profile_data_dir };
Ok(())
}
RuntimeState::Ready { scope: active_scope, .. } if active_scope == &scope => Ok(()),
RuntimeState::Ready { scope: active_scope, .. } => Err(active_scope.error_for(&scope)),
} }
let (config_dir, transient_profile_data_dir) = config_dir_for_scope(&scope)?;
let client = (self.client_factory)(config_dir)?;
self.clients.insert(scope, ScopedRuntimeClient { client, transient_profile_data_dir });
Ok(())
}
fn client_for_scope(
&mut self,
scope: &WebSurfaceRuntimeScope,
) -> Result<&mut dyn LiveRuntimeClient, String> {
match self.clients.get_mut(scope) {
Some(client) => Ok(client.client.as_mut()),
None => Err(missing_runtime_message(scope)),
}
}
#[cfg(test)]
fn client_count_for_test(&self) -> usize {
self.clients.len()
}
#[cfg(test)]
fn session_scope_for_test(&self, tab_id: &TabId) -> Option<&WebSurfaceRuntimeScope> {
self.sessions.get(tab_id).map(|session| &session.scope)
} }
} }
impl Drop for WebSurfaceRuntime { impl Drop for WebSurfaceRuntime {
fn drop(&mut self) { fn drop(&mut self) {
let RuntimeState::Ready { transient_profile_data_dir: Some(path), .. } = &self.state else { let transient_profile_data_dirs = self
return; .clients
}; .values()
let _ = fs::remove_dir_all(path); .filter_map(|client| client.transient_profile_data_dir.clone())
.collect::<Vec<_>>();
self.clients.clear();
for path in transient_profile_data_dirs {
let _ = fs::remove_dir_all(path);
}
} }
} }
enum RuntimeState { type LiveRuntimeClientFactory = fn(PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String>;
Empty,
Ready { trait LiveRuntimeClient {
scope: WebSurfaceRuntimeScope, fn ensure(&mut self, request: ServoLiveEnsureRequest) -> Result<Option<WebLiveFrame>, String>;
client: ServoLiveClient,
transient_profile_data_dir: Option<PathBuf>, fn poll(&mut self, tab_id: String) -> Result<Option<WebLiveFrame>, String>;
},
} }
#[derive(Clone, Debug, Eq, PartialEq)] type WebLiveFrame = crate::services::servo_live::ServoLiveFrame;
impl LiveRuntimeClient for ServoLiveClient {
fn ensure(&mut self, request: ServoLiveEnsureRequest) -> Result<Option<WebLiveFrame>, String> {
ServoLiveClient::ensure(self, request).map_err(|error| error.to_string())
}
fn poll(&mut self, tab_id: String) -> Result<Option<WebLiveFrame>, String> {
ServoLiveClient::poll(self, tab_id).map_err(|error| error.to_string())
}
}
fn new_servo_live_client(config_dir: PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String> {
ServoLiveClient::new(config_dir)
.map(|client| Box::new(client) as Box<dyn LiveRuntimeClient>)
.map_err(|error| error.to_string())
}
struct ScopedRuntimeClient {
client: Box<dyn LiveRuntimeClient>,
transient_profile_data_dir: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct WebSurfaceRuntimeScope { struct WebSurfaceRuntimeScope {
profile_id: ProfileId, profile_id: ProfileId,
profile_data_mode: ProfileDataMode, profile_data_mode: ProfileDataMode,
@@ -187,20 +235,11 @@ impl WebSurfaceRuntimeScope {
fn new(profile_id: ProfileId, profile_data_mode: ProfileDataMode) -> Self { fn new(profile_id: ProfileId, profile_data_mode: ProfileDataMode) -> Self {
Self { profile_id, profile_data_mode } Self { profile_id, profile_data_mode }
} }
fn error_for(&self, requested: &Self) -> String {
format!(
"Servo live runtime is already attached to profile {} ({:?}); requested profile {} ({:?})",
self.profile_id.as_str(),
self.profile_data_mode,
requested.profile_id.as_str(),
requested.profile_data_mode
)
}
} }
#[derive(Clone, Default)] #[derive(Clone)]
struct WebSurfaceSession { struct WebSurfaceSession {
scope: WebSurfaceRuntimeScope,
requested_url: String, requested_url: String,
size: WebSurfaceSize, size: WebSurfaceSize,
zoom_percent: u16, zoom_percent: u16,
@@ -209,6 +248,17 @@ struct WebSurfaceSession {
} }
impl WebSurfaceSession { impl WebSurfaceSession {
fn new(scope: WebSurfaceRuntimeScope) -> Self {
Self {
scope,
requested_url: String::new(),
size: WebSurfaceSize::default(),
zoom_percent: 0,
scroll_offset: WebSurfaceScrollOffset::default(),
pending_user_navigation: false,
}
}
fn started_loading( fn started_loading(
&self, &self,
requested_url: &str, requested_url: &str,
@@ -245,6 +295,15 @@ impl WebSurfaceSession {
} }
} }
#[derive(Clone)]
struct WebSurfacePollJob {
tab_id: TabId,
scope: WebSurfaceRuntimeScope,
requested_url: String,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
}
pub(super) struct WebSurfaceEnsureResult { pub(super) struct WebSurfaceEnsureResult {
pub(super) requested_url: String, pub(super) requested_url: String,
pub(super) started_loading: bool, pub(super) started_loading: bool,
@@ -290,6 +349,46 @@ fn config_dir_for_scope(
} }
} }
fn session_for_scope<'a>(
sessions: &'a mut BTreeMap<TabId, WebSurfaceSession>,
tab_id: &TabId,
scope: WebSurfaceRuntimeScope,
) -> &'a mut WebSurfaceSession {
let session =
sessions.entry(tab_id.clone()).or_insert_with(|| WebSurfaceSession::new(scope.clone()));
if session.scope != scope {
*session = WebSurfaceSession::new(scope);
}
session
}
fn visible_poll_jobs(
sessions: &BTreeMap<TabId, WebSurfaceSession>,
visible_tab_ids: &[TabId],
) -> Vec<WebSurfacePollJob> {
sessions
.iter()
.filter(|(tab_id, _)| {
visible_tab_ids.iter().any(|visible_tab_id| visible_tab_id == *tab_id)
})
.map(|(tab_id, session)| WebSurfacePollJob {
tab_id: tab_id.clone(),
scope: session.scope.clone(),
requested_url: session.requested_url.clone(),
scroll_offset: session.scroll_offset,
zoom_percent: session.zoom_percent,
})
.collect()
}
fn missing_runtime_message(scope: &WebSurfaceRuntimeScope) -> String {
format!(
"Servo live runtime is unavailable for profile {} ({:?})",
scope.profile_id.as_str(),
scope.profile_data_mode
)
}
fn scroll_wire_fields( fn scroll_wire_fields(
delta: Option<super::web_surface_geometry::WebSurfaceScrollDelta>, delta: Option<super::web_surface_geometry::WebSurfaceScrollDelta>,
point: Option<super::web_surface_geometry::WebSurfaceClickPoint>, point: Option<super::web_surface_geometry::WebSurfaceClickPoint>,
@@ -318,3 +417,7 @@ impl From<&WebSurfaceSitePermission> for ServoLiveSitePermission {
) )
} }
} }
#[cfg(test)]
#[path = "web_surface_runtime_tests.rs"]
mod tests;
@@ -0,0 +1,117 @@
use std::collections::BTreeMap;
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use crate::{
services::ProfileDataMode,
shell::{
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
web_surface_state::WebSurfacePendingInput,
},
};
use super::*;
#[test]
fn runtime_keeps_independent_clients_for_profile_scopes() -> Result<(), String> {
let mut runtime = WebSurfaceRuntime::new_with_client_factory(fake_client_factory);
let first_profile = ProfileId::new();
let second_profile = ProfileId::new();
let first_tab = web_tab(TabId::new(), first_profile.clone(), "https://example.com/first")?;
let second_tab = web_tab(TabId::new(), second_profile.clone(), "https://example.com/second")?;
runtime.ensure_tab(
&first_tab,
surface_size(),
ProfileDataMode::Transient,
&[],
pending_input(),
)?;
runtime.ensure_tab(
&second_tab,
surface_size(),
ProfileDataMode::Transient,
&[],
pending_input(),
)?;
runtime.ensure_tab(
&first_tab,
surface_size(),
ProfileDataMode::Transient,
&[],
pending_input(),
)?;
assert_eq!(runtime.client_count_for_test(), 2);
assert_eq!(
runtime.session_scope_for_test(first_tab.id()),
Some(&WebSurfaceRuntimeScope::new(first_profile, ProfileDataMode::Transient)),
);
assert_eq!(
runtime.session_scope_for_test(second_tab.id()),
Some(&WebSurfaceRuntimeScope::new(second_profile, ProfileDataMode::Transient)),
);
Ok(())
}
#[test]
fn session_scope_change_resets_tab_state() {
let tab_id = TabId::new();
let first_scope = WebSurfaceRuntimeScope::new(ProfileId::new(), ProfileDataMode::Persistent);
let second_scope = WebSurfaceRuntimeScope::new(ProfileId::new(), ProfileDataMode::Transient);
let mut sessions = BTreeMap::new();
let session = session_for_scope(&mut sessions, &tab_id, first_scope);
session.requested_url = "https://example.com/old".to_string();
session.size = surface_size();
session.zoom_percent = 150;
session.scroll_offset = WebSurfaceScrollOffset::default();
session.pending_user_navigation = true;
let session = session_for_scope(&mut sessions, &tab_id, second_scope.clone());
assert_eq!(session.scope, second_scope);
assert_eq!(session.requested_url, "");
assert_eq!(session.size, WebSurfaceSize::default());
assert_eq!(session.zoom_percent, 0);
assert_eq!(session.scroll_offset, WebSurfaceScrollOffset::default());
assert!(!session.pending_user_navigation);
}
struct FakeLiveRuntimeClient;
impl LiveRuntimeClient for FakeLiveRuntimeClient {
fn ensure(&mut self, _request: ServoLiveEnsureRequest) -> Result<Option<WebLiveFrame>, String> {
Ok(None)
}
fn poll(&mut self, _tab_id: String) -> Result<Option<WebLiveFrame>, String> {
Ok(None)
}
}
fn fake_client_factory(
_config_dir: std::path::PathBuf,
) -> Result<Box<dyn LiveRuntimeClient>, String> {
Ok(Box::new(FakeLiveRuntimeClient))
}
fn web_tab(tab_id: TabId, profile_id: ProfileId, url: &str) -> Result<BrowserTab, String> {
let url = UrlText::parse(url).map_err(|error| error.to_string())?;
Ok(BrowserTab::new(tab_id, SpaceId::new(), profile_id, "Web", url))
}
fn surface_size() -> WebSurfaceSize {
WebSurfaceSize { width: 640, height: 480, device_pixel_ratio_percent: 100 }
}
fn pending_input() -> WebSurfacePendingInput {
WebSurfacePendingInput {
scroll_offset: WebSurfaceScrollOffset::default(),
scroll_delta: None,
scroll_point: None,
click_point: None,
hover_point: None,
typed_text: None,
}
}