This commit is contained in:
2026-05-18 13:58:36 -04:00
parent d076dad356
commit 68a4507dbe
143 changed files with 15715 additions and 7204 deletions
+240 -89
View File
@@ -6,6 +6,7 @@ use std::{
};
use ely_domain::{BrowserTab, ProfileId, TabId};
use gpui::NativeSurfaceHandle;
use crate::services::{
ProfileDataMode,
@@ -23,7 +24,9 @@ use super::{
};
pub(super) struct WebSurfaceRuntime {
workers: BTreeMap<WebSurfaceRuntimeScope, ScopedWorker>,
worker: Option<ScopedWorker>,
direct_client: Option<ScopedDirectClient>,
pending_direct_responses: Vec<WorkerResponse>,
sessions: BTreeMap<TabId, WebSurfaceSession>,
client_factory: LiveRuntimeClientFactory,
}
@@ -31,7 +34,9 @@ pub(super) struct WebSurfaceRuntime {
impl WebSurfaceRuntime {
pub(super) fn new() -> Self {
Self {
workers: BTreeMap::new(),
worker: None,
direct_client: None,
pending_direct_responses: Vec::new(),
sessions: BTreeMap::new(),
client_factory: new_servo_live_client,
}
@@ -39,9 +44,16 @@ impl WebSurfaceRuntime {
#[cfg(test)]
pub(super) fn new_with_client_factory(client_factory: LiveRuntimeClientFactory) -> Self {
Self { workers: BTreeMap::new(), sessions: BTreeMap::new(), client_factory }
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,
@@ -49,11 +61,48 @@ impl WebSurfaceRuntime {
profile_data_mode: ProfileDataMode,
permissions: &[WebSurfaceSitePermission],
input: WebSurfacePendingInput,
) -> Result<WebSurfaceEnsureResult, String> {
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<WebSurfaceEnsureResult, String> {
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<NativeSurfaceHandle>,
profile_data_mode: ProfileDataMode,
permissions: &[WebSurfaceSitePermission],
input: WebSurfacePendingInput,
) -> Result<WebSurfaceEnsureResult, String> {
let scope = WebSurfaceRuntimeScope::new(tab.profile_id().clone(), profile_data_mode);
self.ensure_worker(scope.clone())?;
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);
@@ -88,6 +137,7 @@ impl WebSurfaceRuntime {
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,
@@ -100,10 +150,15 @@ impl WebSurfaceRuntime {
site_permissions: permissions.iter().map(ServoLiveSitePermission::from).collect(),
};
let Some(scoped) = self.workers.get(&scope) else {
return Err("Servo worker was created but is no longer registered".to_string());
};
scoped.worker.submit_ensure(request);
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);
}
@@ -114,68 +169,19 @@ impl WebSurfaceRuntime {
pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec<WebSurfaceRuntimeFrame> {
let mut frames = Vec::new();
let mut dead_scopes = Vec::new();
let scopes: Vec<WebSurfaceRuntimeScope> = self.workers.keys().cloned().collect();
let now = Instant::now();
for scope in scopes {
let responses = self
.workers
.get(&scope)
.map(|scoped| scoped.worker.drain_responses())
.unwrap_or_default();
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::SidecarExited => dead_scopes.push(scope.clone()),
}
}
}
for scope in dead_scopes {
self.workers.remove(&scope);
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;
@@ -183,11 +189,22 @@ impl WebSurfaceRuntime {
if !session.cadence.should_poll(poll_now) {
continue;
}
let Some(scoped) = self.workers.get(&session.scope) else {
continue;
};
let _ = scoped.worker.submit_poll(tab_id.as_str().to_string());
session.cadence.note_poll_submitted(poll_now);
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
@@ -201,38 +218,156 @@ impl WebSurfaceRuntime {
visible_tab_ids
.iter()
.filter_map(|tab_id| self.sessions.get(tab_id))
.filter(|session| self.workers.contains_key(&session.scope))
.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) {
let Some(session) = self.sessions.remove(tab_id) else {
if self.sessions.remove(tab_id).is_none() {
return;
};
if let Some(scoped) = self.workers.get(&session.scope) {
}
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.workers.contains_key(&scope) {
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)?;
let worker = LiveRuntimeWorker::new(client)?;
self.workers.insert(scope, ScopedWorker { worker, transient_profile_data_dir });
self.direct_client = Some(ScopedDirectClient { client, transient_profile_data_dir });
Ok(())
}
fn ensure_direct(
&mut self,
request: ServoLiveEnsureRequest,
tab_id: String,
) -> Result<Option<WorkerResponse>, 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<String>) -> (Vec<WorkerResponse>, 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<WorkerResponse>,
now: Instant,
frames: &mut Vec<WebSurfaceRuntimeFrame>,
) -> 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<TabId> {
self.sessions.keys().find(|key| key.as_str() == tab_id).cloned()
}
#[cfg(test)]
pub(super) fn client_count_for_test(&self) -> usize {
self.workers.len()
usize::from(self.worker.is_some()) + usize::from(self.direct_client.is_some())
}
#[cfg(test)]
@@ -242,23 +377,34 @@ impl WebSurfaceRuntime {
#[cfg(test)]
pub(super) fn flush_for_test(&self) {
for scoped in self.workers.values() {
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;
};
if let Some(path) = scoped.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;
};
if let Some(path) = scoped.transient_profile_data_dir {
let _ = fs::remove_dir_all(path);
}
}
}
impl Drop for WebSurfaceRuntime {
fn drop(&mut self) {
let transient_profile_data_dirs = self
.workers
.values()
.filter_map(|scoped| scoped.transient_profile_data_dir.clone())
.collect::<Vec<_>>();
self.workers.clear();
for path in transient_profile_data_dirs {
let _ = fs::remove_dir_all(path);
}
self.remove_worker();
self.remove_direct_client();
}
}
@@ -276,6 +422,11 @@ struct ScopedWorker {
transient_profile_data_dir: Option<PathBuf>,
}
struct ScopedDirectClient {
client: Box<dyn LiveRuntimeClient>,
transient_profile_data_dir: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(super) struct WebSurfaceRuntimeScope {
profile_id: ProfileId,