use std::{ collections::BTreeMap, fs, path::PathBuf, time::{Duration, Instant}, }; use ely_domain::{BrowserTab, TabId}; use gpui::NativeSurfaceHandle; use crate::services::{ ProfileDataMode, servo_live::{ServoLiveClient, ServoLiveEnsureRequest, ServoLiveSitePermission}, }; use super::{ web_surface_frame::WebSurfaceFrame, web_surface_geometry::WebSurfaceSize, web_surface_permissions::WebSurfaceSitePermission, web_surface_runtime_session::{ WebSurfaceRuntimeScope, WebSurfaceSession, config_dir_for_scope, session_for_scope, }, web_surface_runtime_wire::{ input_requests_history_navigation, log_ensure_submitted, pending_input_kind, scroll_wire_fields, }, web_surface_state::WebSurfacePendingInput, web_surface_worker::{LiveRuntimeClient, LiveRuntimeWorker, WorkerResponse}, }; pub(super) use super::web_surface_runtime_session::{ WebSurfaceEnsureResult, WebSurfaceRuntimeFrame, WebSurfaceUrlChange, WebSurfaceUrlChangeKind, }; pub(super) struct WebSurfaceRuntime { worker: Option, direct_client: Option, pending_direct_responses: Vec, sessions: BTreeMap, client_factory: LiveRuntimeClientFactory, } impl WebSurfaceRuntime { pub(super) fn new() -> Self { Self { worker: None, direct_client: None, pending_direct_responses: Vec::new(), sessions: BTreeMap::new(), client_factory: new_servo_live_client, } } #[cfg(test)] pub(super) fn new_with_client_factory(client_factory: LiveRuntimeClientFactory) -> Self { Self { worker: None, direct_client: None, pending_direct_responses: Vec::new(), sessions: BTreeMap::new(), client_factory, } } #[cfg(test)] pub(super) fn ensure_tab( &mut self, tab: &BrowserTab, size: WebSurfaceSize, profile_data_mode: ProfileDataMode, permissions: &[WebSurfaceSitePermission], input: WebSurfacePendingInput, ) -> Result { self.ensure_tab_inner(tab, size, None, profile_data_mode, permissions, input) } pub(super) fn ensure_tab_with_native_surface( &mut self, tab: &BrowserTab, size: WebSurfaceSize, native_surface: NativeSurfaceHandle, profile_data_mode: ProfileDataMode, permissions: &[WebSurfaceSitePermission], input: WebSurfacePendingInput, ) -> Result { self.ensure_tab_inner( tab, size, Some(native_surface), profile_data_mode, permissions, input, ) } fn ensure_tab_inner( &mut self, tab: &BrowserTab, size: WebSurfaceSize, native_surface: Option, profile_data_mode: ProfileDataMode, permissions: &[WebSurfaceSitePermission], input: WebSurfacePendingInput, ) -> Result { let scope = WebSurfaceRuntimeScope::new(tab.profile_id().clone(), profile_data_mode); let use_direct_client = native_surface.is_some(); if use_direct_client { self.ensure_direct_client(scope.clone())?; } else { self.ensure_worker(scope.clone())?; } let requested_url = tab.url().as_str().to_string(); let tab_id_string = tab.id().as_str().to_string(); let zoom_percent = tab.zoom_percent(); let enqueued_at = input.enqueued_at; let input_kind = pending_input_kind(&input); let (scroll_delta_x, scroll_delta_y, scroll_point_x, scroll_point_y) = scroll_wire_fields(input.scroll_delta, input.scroll_point)?; let user_navigation_input = input_requests_history_navigation(&input); let next_scroll_offset = input.scroll_offset; let submitted_at = Instant::now(); let started_loading = { let session = session_for_scope(&mut self.sessions, tab.id(), scope.clone()); let started_loading = session.started_loading(&requested_url, size, zoom_percent); if started_loading { session.pending_user_navigation = false; } if user_navigation_input { session.pending_user_navigation = true; } session.requested_url = requested_url.clone(); session.size = size; session.zoom_percent = zoom_percent; session.scroll_offset = next_scroll_offset; session.cadence.note_ensure(input_kind, started_loading, submitted_at); started_loading }; let request = ServoLiveEnsureRequest { tab_id: tab.id().as_str().to_string(), profile_id: tab.profile_id().as_str().to_string(), url: requested_url.clone(), width: size.width, height: size.height, page_zoom_percent: zoom_percent, device_pixel_ratio: size.device_pixel_ratio_f32(), native_surface, scroll_delta_x, scroll_delta_y, scroll_point_x, scroll_point_y, click_x: input.click_point.map(|point| point.x()), click_y: input.click_point.map(|point| point.y()), hover_x: input.hover_point.map(|point| point.x()), hover_y: input.hover_point.map(|point| point.y()), typed_text: input.typed_text, site_permissions: permissions.iter().map(ServoLiveSitePermission::from).collect(), }; if use_direct_client { let response = self.ensure_direct(request, tab_id_string.clone())?; self.pending_direct_responses.extend(response); } else { let Some(scoped) = self.worker.as_ref() else { return Err("Servo worker was created but is no longer registered".to_string()); }; scoped.worker.submit_ensure(request); } if let Some(session) = self.sessions.get_mut(tab.id()) { session.cadence.note_poll_submitted(submitted_at); } log_ensure_submitted(tab, size, input_kind.label(), enqueued_at, started_loading); Ok(WebSurfaceEnsureResult { requested_url, started_loading }) } pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec { let mut frames = Vec::new(); let now = Instant::now(); let mut responses = std::mem::take(&mut self.pending_direct_responses); responses.extend( self.worker.as_ref().map(|scoped| scoped.worker.drain_responses()).unwrap_or_default(), ); let runtime_unavailable = self.collect_responses(responses, now, &mut frames); if runtime_unavailable { self.remove_worker(); self.remove_direct_client(); } let poll_now = Instant::now(); let mut direct_polls = Vec::new(); 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; } if let Some(scoped) = self.worker.as_ref() { let _ = scoped.worker.submit_poll(tab_id.as_str().to_string()); session.cadence.note_poll_submitted(poll_now); } else if self.direct_client.is_some() { direct_polls.push(tab_id.as_str().to_string()); session.cadence.note_poll_submitted(poll_now); } } if !direct_polls.is_empty() { let (responses, runtime_unavailable) = self.poll_direct(direct_polls); let unavailable_from_responses = self.collect_responses(responses, Instant::now(), &mut frames); if runtime_unavailable || unavailable_from_responses { self.remove_direct_client(); } } 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(|_| self.worker.is_some() || self.direct_client.is_some()) .map(|session| session.cadence.next_poll_delay(now)) .min() } pub(super) fn close_tab(&mut self, tab_id: &TabId) { if self.sessions.remove(tab_id).is_none() { return; } let direct_result = self .direct_client .as_mut() .map(|scoped| scoped.client.close(tab_id.as_str().to_string())); if direct_result.as_ref().is_some_and(|result| { result.as_ref().is_err_and(|error| error.is_runtime_unavailable()) }) { self.remove_direct_client(); } if let Some(scoped) = self.worker.as_ref() { scoped.worker.submit_close(tab_id.as_str().to_string()); } } fn ensure_worker(&mut self, scope: WebSurfaceRuntimeScope) -> Result<(), String> { if self.worker.is_some() { return Ok(()); } let (config_dir, transient_profile_data_dir) = config_dir_for_scope(&scope)?; let client_factory = self.client_factory; let worker = LiveRuntimeWorker::new(move || client_factory(config_dir))?; self.worker = Some(ScopedWorker { worker, transient_profile_data_dir }); Ok(()) } fn ensure_direct_client(&mut self, scope: WebSurfaceRuntimeScope) -> Result<(), String> { if self.direct_client.is_some() { return Ok(()); } let (config_dir, transient_profile_data_dir) = config_dir_for_scope(&scope)?; let client = (self.client_factory)(config_dir)?; self.direct_client = Some(ScopedDirectClient { client, transient_profile_data_dir }); Ok(()) } fn ensure_direct( &mut self, request: ServoLiveEnsureRequest, tab_id: String, ) -> Result, String> { let Some(scoped) = self.direct_client.as_mut() else { return Err("Servo client was created but is no longer registered".to_string()); }; match scoped.client.ensure(request) { Ok(Some(frame)) => Ok(Some(WorkerResponse::Frame { tab_id, frame })), Ok(None) => Ok(None), Err(error) => { let message = error.to_string(); if error.is_runtime_unavailable() { self.remove_direct_client(); } Err(message) } } } fn poll_direct(&mut self, tab_ids: Vec) -> (Vec, bool) { let Some(scoped) = self.direct_client.as_mut() else { return (Vec::new(), false); }; let mut responses = Vec::new(); let mut runtime_unavailable = false; for tab_id in tab_ids { match scoped.client.poll(tab_id.clone()) { Ok(Some(frame)) => responses.push(WorkerResponse::Frame { tab_id, frame }), Ok(None) => {} Err(error) if error.is_runtime_unavailable() => { runtime_unavailable = true; responses.push(WorkerResponse::RuntimeUnavailable); } Err(error) => { responses.push(WorkerResponse::Failed { tab_id, message: error.to_string() }) } } } (responses, runtime_unavailable) } fn collect_responses( &mut self, responses: Vec, now: Instant, frames: &mut Vec, ) -> bool { let mut runtime_unavailable = false; for response in responses { match response { WorkerResponse::Frame { tab_id, frame } => { let Some(tab_id_obj) = self.lookup_session_tab_id(&tab_id) else { continue; }; let session = match self.sessions.get_mut(&tab_id_obj) { Some(session) => session, None => continue, }; let requested_url = session.requested_url.clone(); let scroll_offset = session.scroll_offset; let zoom_percent = session.zoom_percent; session.cadence.note_frame(frame.render_state(), now); match WebSurfaceFrame::from_live_frame( requested_url.clone(), scroll_offset, zoom_percent, frame, ) { Ok(frame) => { let url_change = session.url_change_for(&tab_id_obj, requested_url.as_str(), &frame); frames.push(WebSurfaceRuntimeFrame::Ready { tab_id: tab_id_obj, frame: Box::new(frame), url_change, }); } Err(error) => frames.push(WebSurfaceRuntimeFrame::Failed { tab_id: tab_id_obj, message: error.to_string(), }), } } WorkerResponse::Failed { tab_id, message } => { let Some(tab_id_obj) = self.lookup_session_tab_id(&tab_id) else { continue; }; frames.push(WebSurfaceRuntimeFrame::Failed { tab_id: tab_id_obj, message }); } WorkerResponse::RuntimeUnavailable => { runtime_unavailable = true; } } } runtime_unavailable } fn lookup_session_tab_id(&self, tab_id: &str) -> Option { self.sessions.keys().find(|key| key.as_str() == tab_id).cloned() } #[cfg(test)] pub(super) fn client_count_for_test(&self) -> usize { usize::from(self.worker.is_some()) + usize::from(self.direct_client.is_some()) } #[cfg(test)] pub(super) fn session_scope_for_test(&self, tab_id: &TabId) -> Option<&WebSurfaceRuntimeScope> { self.sessions.get(tab_id).map(|session| &session.scope) } #[cfg(test)] pub(super) fn flush_for_test(&self) { if let Some(scoped) = self.worker.as_ref() { scoped.worker.wait_until_idle(); } } fn remove_worker(&mut self) { let Some(scoped) = self.worker.take() else { return; }; let ScopedWorker { worker, transient_profile_data_dir } = scoped; drop(worker); if let Some(path) = transient_profile_data_dir { let _ = fs::remove_dir_all(path); } } fn remove_direct_client(&mut self) { let Some(scoped) = self.direct_client.take() else { return; }; let ScopedDirectClient { client, transient_profile_data_dir } = scoped; drop(client); if let Some(path) = transient_profile_data_dir { let _ = fs::remove_dir_all(path); } } } impl Drop for WebSurfaceRuntime { fn drop(&mut self) { self.remove_worker(); self.remove_direct_client(); } } pub(super) type LiveRuntimeClientFactory = fn(PathBuf) -> Result, String>; fn new_servo_live_client(config_dir: PathBuf) -> Result, String> { ServoLiveClient::new(config_dir) .map(|client| Box::new(client) as Box) .map_err(|error| error.to_string()) } struct ScopedWorker { worker: LiveRuntimeWorker, transient_profile_data_dir: Option, } struct ScopedDirectClient { client: Box, transient_profile_data_dir: Option, } #[cfg(test)] #[path = "web_surface_runtime_tests.rs"] mod tests;