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
+96 -5
View File
@@ -1,3 +1,5 @@
use std::time::Instant;
use ely_domain::TabId;
use gpui::{Bounds, Pixels};
@@ -6,6 +8,7 @@ use super::{
web_surface_geometry::{
WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceScrollOffset, WebSurfaceSize,
},
web_surface_permissions::WebSurfaceSitePermission,
};
pub(super) struct WebSurfaceScrollState {
@@ -43,6 +46,7 @@ pub(super) struct WebSurfaceTextInputState {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct WebSurfacePendingInput {
pub(super) enqueued_at: Option<Instant>,
pub(super) scroll_offset: WebSurfaceScrollOffset,
pub(super) scroll_delta: Option<WebSurfaceScrollDelta>,
pub(super) scroll_point: Option<WebSurfaceClickPoint>,
@@ -68,9 +72,6 @@ pub(super) enum WebSurfaceInputOutcome {
Applied,
/// Same value as currently recorded — nothing to flush downstream.
NoChange,
/// First sighting of a new value; held back until a second
/// matching measurement confirms it (viewport-resize debounce).
Buffered,
/// Geometry constructor rejected the input (zero/NaN/negative
/// bounds). The viewport never measured cleanly.
DroppedInvalidBounds,
@@ -113,11 +114,12 @@ pub(super) enum WebSurfaceState {
pub(super) struct PerTabSurface {
pub(super) viewport_bounds: Option<Bounds<Pixels>>,
pub(super) viewport_size: Option<WebSurfaceSize>,
pub(super) pending_viewport_size: Option<WebSurfaceSize>,
pub(super) last_ensure_key: Option<WebSurfaceEnsureKey>,
pub(super) hover_point: Option<WebSurfaceClickPoint>,
pub(super) click_point: Option<WebSurfaceClickState>,
pub(super) pending_scroll_delta: Option<WebSurfaceScrollDelta>,
pub(super) pending_scroll_point: Option<WebSurfaceClickPoint>,
pub(super) pending_input_started_at: Option<Instant>,
pub(super) scroll_offset: Option<WebSurfaceScrollState>,
pub(super) typed_text: Option<WebSurfaceTextInputState>,
pub(super) state: Option<WebSurfaceState>,
@@ -128,17 +130,38 @@ impl PerTabSurface {
Self {
viewport_bounds: None,
viewport_size: None,
pending_viewport_size: None,
last_ensure_key: None,
hover_point: None,
click_point: None,
pending_scroll_delta: None,
pending_scroll_point: None,
pending_input_started_at: None,
scroll_offset: None,
typed_text: None,
state: None,
}
}
pub(super) fn mark_pending_input_started(&mut self) {
self.pending_input_started_at.get_or_insert_with(Instant::now);
}
pub(super) fn should_ensure(&self, key: &WebSurfaceEnsureKey) -> bool {
self.last_ensure_key.as_ref() != Some(key) || self.has_pending_input()
}
pub(super) fn mark_ensured(&mut self, key: WebSurfaceEnsureKey) {
self.last_ensure_key = Some(key);
}
fn has_pending_input(&self) -> bool {
self.hover_point.is_some()
|| self.click_point.is_some()
|| self.pending_scroll_delta.is_some()
|| self.pending_scroll_point.is_some()
|| self.typed_text.is_some()
}
pub(super) fn scroll_offset_for(&self, requested_url: &str) -> WebSurfaceScrollOffset {
self.scroll_offset
.as_ref()
@@ -147,3 +170,71 @@ impl PerTabSurface {
.unwrap_or_default()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct WebSurfaceEnsureKey {
requested_url: String,
size: WebSurfaceSize,
zoom_percent: u16,
permissions: Vec<WebSurfaceSitePermission>,
}
impl WebSurfaceEnsureKey {
pub(super) fn new(
requested_url: String,
size: WebSurfaceSize,
zoom_percent: u16,
permissions: &[WebSurfaceSitePermission],
) -> Self {
Self { requested_url, size, zoom_percent, permissions: permissions.to_vec() }
}
}
#[cfg(test)]
mod tests {
use gpui::{point, px};
use super::*;
#[test]
fn unchanged_surface_without_input_skips_ensure() {
let key = ensure_key("https://example.com/", 800, 600);
let mut surface = PerTabSurface::new();
assert!(surface.should_ensure(&key));
surface.mark_ensured(key.clone());
assert!(!surface.should_ensure(&key));
}
#[test]
fn pending_input_forces_ensure_even_when_key_matches() {
let key = ensure_key("https://example.com/", 800, 600);
let mut surface = PerTabSurface::new();
surface.mark_ensured(key.clone());
surface.pending_scroll_delta =
WebSurfaceScrollDelta::from_point(point(px(0.0), px(120.0)), 1.0);
assert!(surface.should_ensure(&key));
}
#[test]
fn viewport_change_forces_ensure() {
let old_key = ensure_key("https://example.com/", 800, 600);
let new_key = ensure_key("https://example.com/", 1024, 768);
let mut surface = PerTabSurface::new();
surface.mark_ensured(old_key);
assert!(surface.should_ensure(&new_key));
}
fn ensure_key(url: &str, width: u32, height: u32) -> WebSurfaceEnsureKey {
WebSurfaceEnsureKey::new(
url.to_string(),
WebSurfaceSize { width, height, device_pixel_ratio_percent: 100 },
100,
&[],
)
}
}