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
+66
View File
@@ -289,6 +289,9 @@ pub(crate) struct ServoLiveFrame {
render_state: String,
width: u32,
height: u32,
device_pixel_ratio: f32,
css_viewport_width: u32,
css_viewport_height: u32,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: u64,
#[cfg(all(test, feature = "live-site-smoke"))]
@@ -302,14 +305,26 @@ pub(crate) struct ServoLiveFrame {
pixel_buffer: Option<CVPixelBuffer>,
}
// SAFETY: CVPixelBuffer wraps CVPixelBufferRef, a CoreFoundation type
// Apple documents as safe to share across threads. The Rust core-video
// crate does not mark it Send, so the worker thread needs this opt-in
// to ship hardware frames back to the UI thread via mpsc::Sender.
#[cfg(target_os = "macos")]
#[expect(unsafe_code)]
unsafe impl Send for ServoLiveFrame {}
impl ServoLiveFrame {
fn from_parts(report: LiveFrameReport, rgba_bytes: Vec<u8>) -> Self {
let (css_viewport_width, css_viewport_height) = css_viewport_size_from_report(&report);
Self {
loaded_url: report.loaded_url,
title: report.title,
render_state: report.state,
width: report.width,
height: report.height,
device_pixel_ratio: report.device_pixel_ratio,
css_viewport_width,
css_viewport_height,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: report.non_white_pixel_count,
#[cfg(all(test, feature = "live-site-smoke"))]
@@ -355,6 +370,21 @@ impl ServoLiveFrame {
self.height
}
#[must_use]
pub fn device_pixel_ratio(&self) -> f32 {
self.device_pixel_ratio
}
#[must_use]
pub fn css_viewport_width(&self) -> u32 {
self.css_viewport_width
}
#[must_use]
pub fn css_viewport_height(&self) -> u32 {
self.css_viewport_height
}
#[cfg(all(test, feature = "live-site-smoke"))]
#[must_use]
pub fn non_white_pixel_count(&self) -> u64 {
@@ -386,6 +416,9 @@ impl ServoLiveFrame {
render_state: "complete".to_string(),
width,
height,
device_pixel_ratio: 1.0,
css_viewport_width: width,
css_viewport_height: height,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: 0,
#[cfg(all(test, feature = "live-site-smoke"))]
@@ -410,6 +443,9 @@ impl ServoLiveFrame {
render_state: "complete".to_string(),
width,
height,
device_pixel_ratio: 1.0,
css_viewport_width: width,
css_viewport_height: height,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: 0,
#[cfg(all(test, feature = "live-site-smoke"))]
@@ -422,6 +458,20 @@ impl ServoLiveFrame {
}
}
fn css_viewport_size_from_report(report: &LiveFrameReport) -> (u32, u32) {
let dpr = if report.device_pixel_ratio.is_finite() && report.device_pixel_ratio > 0.0 {
report.device_pixel_ratio
} else {
1.0
};
let fallback_width = ((report.width as f32) / dpr).round().max(1.0) as u32;
let fallback_height = ((report.height as f32) / dpr).round().max(1.0) as u32;
(
if report.css_viewport_width > 0 { report.css_viewport_width } else { fallback_width },
if report.css_viewport_height > 0 { report.css_viewport_height } else { fallback_height },
)
}
#[derive(Debug, Error)]
pub(crate) enum ServoLiveError {
#[error("servo sidecar binary is unavailable at {path}")]
@@ -469,3 +519,19 @@ pub(crate) enum ServoLiveError {
#[error(transparent)]
SidecarCommand(#[from] SidecarCommandError),
}
impl ServoLiveError {
pub(crate) fn is_sidecar_process_unusable(&self) -> bool {
match self {
Self::SidecarExited => true,
Self::Command(error) | Self::FrameRead(error) => matches!(
error.kind(),
io::ErrorKind::BrokenPipe
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::ConnectionReset
| io::ErrorKind::UnexpectedEof
),
_ => false,
}
}
}