Move Servo IPC off UI thread

Root cause of the post-tab lag: the GPUI 16 ms timer was calling
`WebSurfaceRuntime::ensure_tab` and `tick` on the UI thread, and each
call did a synchronous `serde_json` write plus `read_line` against the
Servo sidecar over stdin/stdout. With even one visible tab, every
frame stalled on cross-process IPC.

Introduce `web_surface_worker.rs` — a per-profile worker thread that
owns the `ServoLiveClient`, drains a coalescing request queue
(latest Ensure/Poll per tab wins, no unbounded growth), and ships
results back through a `std::sync::mpsc` channel. `WebSurfaceRuntime`
now submits work non-blockingly and drains responses in `tick`; the
UI thread never blocks on the sidecar.

Adjacent in-flight cleanup riding along: hardware IOSurface
rendering-context completion (sidecar `live_protocol`,
`hardware_rendering_context`, GPUI BGRA surface shader), CSS viewport
size + device pixel ratio plumbing into `ServoLiveFrame`, and the
Send opt-ins for `CVPixelBuffer`-bearing types so frames can cross
the thread boundary.
This commit is contained in:
2026-05-15 16:41:40 -04:00
parent f4c650c4d8
commit 90c029eddb
29 changed files with 2113 additions and 496 deletions
+98 -29
View File
@@ -11,8 +11,9 @@ use super::{
web_surface_permissions::WebSurfaceSitePermission,
web_surface_runtime::{WebSurfaceRuntime, WebSurfaceRuntimeFrame, WebSurfaceUrlChange},
web_surface_state::{
PerTabSurface, WebSurfaceClickState, WebSurfaceInputOutcome, WebSurfaceKeyboardFocusState,
WebSurfacePendingInput, WebSurfaceScrollState, WebSurfaceState, WebSurfaceTextInputState,
PerTabSurface, WebSurfaceClickState, WebSurfaceEnsureKey, WebSurfaceInputOutcome,
WebSurfaceKeyboardFocusState, WebSurfacePendingInput, WebSurfaceScrollState,
WebSurfaceState, WebSurfaceTextInputState,
},
};
@@ -30,6 +31,11 @@ impl WebSurfaceStore {
Self { runtime: WebSurfaceRuntime::new(), surfaces: BTreeMap::new(), keyboard_focus: None }
}
#[cfg(test)]
pub(super) fn new_with_runtime(runtime: WebSurfaceRuntime) -> Self {
Self { runtime, surfaces: BTreeMap::new(), keyboard_focus: None }
}
pub(super) fn state(&self, tab_id: &TabId) -> Option<&WebSurfaceState> {
self.surfaces.get(tab_id).and_then(|surface| surface.state.as_ref())
}
@@ -39,37 +45,41 @@ impl WebSurfaceStore {
tab: &BrowserTab,
profile_data_mode: ProfileDataMode,
permissions: &[WebSurfaceSitePermission],
) -> Option<WebSurfaceUrlChange> {
) -> WebSurfaceEnsureOutcome {
if !is_external_web_url(tab.url().as_str()) {
return None;
return WebSurfaceEnsureOutcome::default();
}
let requested_url = tab.url().as_str().to_string();
let size = self.surfaces.get(tab.id()).and_then(|surface| surface.viewport_size)?;
let Some(size) = self.surfaces.get(tab.id()).and_then(|surface| surface.viewport_size)
else {
return WebSurfaceEnsureOutcome::default();
};
let ensure_key =
WebSurfaceEnsureKey::new(requested_url.clone(), size, tab.zoom_percent(), permissions);
if self.surfaces.get(tab.id()).is_some_and(|surface| !surface.should_ensure(&ensure_key)) {
return WebSurfaceEnsureOutcome::default();
}
let input = self.take_pending_input(tab.id(), requested_url.as_str());
let previous_frame =
self.previous_ready_frame(tab.id(), requested_url.as_str(), tab.zoom_percent());
match self.runtime.ensure_tab(tab, size, profile_data_mode, permissions, input) {
Ok(result) if result.frame.is_some() => {
let url_change = result.url_change;
let Some(frame) = result.frame else {
return url_change;
};
self.surface_mut(tab.id()).state = Some(WebSurfaceState::Ready(frame));
url_change
Ok(result) => {
self.surface_mut(tab.id()).mark_ensured(ensure_key);
if result.started_loading {
self.surface_mut(tab.id()).state = Some(WebSurfaceState::Loading {
requested_url: result.requested_url,
previous_frame,
});
return WebSurfaceEnsureOutcome { changed: true, url_change: None };
}
WebSurfaceEnsureOutcome::default()
}
Ok(result) if result.started_loading => {
self.surface_mut(tab.id()).state = Some(WebSurfaceState::Loading {
requested_url: result.requested_url,
previous_frame,
});
result.url_change
}
Ok(result) => result.url_change,
Err(message) => {
self.surface_mut(tab.id()).mark_ensured(ensure_key);
self.surface_mut(tab.id()).state = Some(WebSurfaceState::Failed { message });
None
WebSurfaceEnsureOutcome { changed: true, url_change: None }
}
}
}
@@ -81,6 +91,23 @@ impl WebSurfaceStore {
for frame in frames {
match frame {
WebSurfaceRuntimeFrame::Ready { tab_id, frame, url_change } => {
match self.initial_display_gate_message(&tab_id, &frame, false) {
Ok(()) => {}
Err(message) => {
self.surface_mut(&tab_id).state =
Some(WebSurfaceState::Failed { message });
result.changed = true;
continue;
}
}
if self.should_hold_initial_frame(&tab_id, &frame, false) {
self.surface_mut(&tab_id).state = Some(WebSurfaceState::Loading {
requested_url: frame.requested_url.clone(),
previous_frame: None,
});
result.changed = true;
continue;
}
self.surface_mut(&tab_id).state = Some(WebSurfaceState::Ready(*frame));
result.changed = true;
if let Some(url_change) = url_change {
@@ -148,6 +175,7 @@ impl WebSurfaceStore {
None => delta,
});
surface.pending_scroll_point = Some(point);
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
@@ -172,21 +200,13 @@ impl WebSurfaceStore {
let Some(current_size) = surface.viewport_size else {
surface.viewport_size = Some(size);
surface.pending_viewport_size = None;
return WebSurfaceInputOutcome::Applied;
};
if current_size == size {
surface.pending_viewport_size = None;
return WebSurfaceInputOutcome::NoChange;
}
if surface.pending_viewport_size != Some(size) {
surface.pending_viewport_size = Some(size);
return WebSurfaceInputOutcome::Buffered;
}
surface.pending_viewport_size = None;
surface.viewport_size = Some(size);
WebSurfaceInputOutcome::Applied
}
@@ -246,6 +266,7 @@ impl WebSurfaceStore {
let surface = self.surface_mut(tab_id);
surface.typed_text = None;
surface.click_point = Some(state);
surface.mark_pending_input_started();
WebSurfaceInputOutcome::Applied
}
@@ -288,6 +309,7 @@ impl WebSurfaceStore {
}
entry.text.push_str(text);
surface.mark_pending_input_started();
WebSurfaceInputOutcome::Applied
}
@@ -313,8 +335,10 @@ impl WebSurfaceStore {
.filter(|state| state.requested_url == requested_url)
.map(|state| state.text);
let hover_point = surface.hover_point.take();
let enqueued_at = surface.pending_input_started_at.take();
WebSurfacePendingInput {
enqueued_at,
scroll_offset,
scroll_delta,
scroll_point,
@@ -346,6 +370,40 @@ impl WebSurfaceStore {
}
}
fn should_hold_initial_frame(
&self,
tab_id: &TabId,
frame: &WebSurfaceFrame,
has_previous_frame: bool,
) -> bool {
!has_previous_frame
&& self
.previous_ready_frame(tab_id, frame.requested_url.as_str(), frame.zoom_percent())
.is_none()
&& matches!(frame.has_visible_content_for_initial_display(), Ok(false))
}
fn initial_display_gate_message(
&self,
tab_id: &TabId,
frame: &WebSurfaceFrame,
has_previous_frame: bool,
) -> Result<(), String> {
if has_previous_frame
|| self
.previous_ready_frame(tab_id, frame.requested_url.as_str(), frame.zoom_percent())
.is_some()
{
return Ok(());
}
frame.has_visible_content_for_initial_display().map(|_| ()).map_err(|error| {
format!(
"Servo hardware surface initial content check failed for {}: {error}",
frame.requested_url
)
})
}
fn surface_mut(&mut self, tab_id: &TabId) -> &mut PerTabSurface {
self.surfaces.entry(tab_id.clone()).or_insert_with(PerTabSurface::new)
}
@@ -362,12 +420,23 @@ impl WebSurfaceStore {
pub(super) fn surface_for_test(&self, tab_id: &TabId) -> Option<&PerTabSurface> {
self.surfaces.get(tab_id)
}
#[cfg(test)]
pub(super) fn flush_runtime_for_test(&self) {
self.runtime.flush_for_test();
}
}
pub(super) fn is_external_web_url(url: &str) -> bool {
url.starts_with("https://") || url.starts_with("http://")
}
#[derive(Default)]
pub(super) struct WebSurfaceEnsureOutcome {
pub(super) changed: bool,
pub(super) url_change: Option<WebSurfaceUrlChange>,
}
#[derive(Default)]
pub(super) struct WebSurfaceTickResult {
pub(super) changed: bool,