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:
@@ -40,6 +40,15 @@ pub(crate) struct IOSurfaceCache {
|
||||
pixel_buffers: HashMap<u64, CachedPixelBuffer>,
|
||||
}
|
||||
|
||||
// SAFETY: CVPixelBuffer wraps CVPixelBufferRef, a CoreFoundation type
|
||||
// Apple documents as safe to share across threads. The cache is owned
|
||||
// by ServoLiveClient which now lives on the LiveRuntimeWorker thread,
|
||||
// so the auto-Send check (rightly) rejects the raw pointer inside the
|
||||
// crate's `CVPixelBuffer`. The pointer is atomically refcounted CFTypeRef
|
||||
// and only mutated via Mach IPC, which is itself thread-safe.
|
||||
#[expect(unsafe_code)]
|
||||
unsafe impl Send for IOSurfaceCache {}
|
||||
|
||||
struct CachedPixelBuffer {
|
||||
pixel_buffer: CVPixelBuffer,
|
||||
width: u32,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,12 @@ pub(super) struct LiveFrameReport {
|
||||
pub(super) state: String,
|
||||
pub(super) width: u32,
|
||||
pub(super) height: u32,
|
||||
#[serde(default = "default_device_pixel_ratio")]
|
||||
pub(super) device_pixel_ratio: f32,
|
||||
#[serde(default)]
|
||||
pub(super) css_viewport_width: u32,
|
||||
#[serde(default)]
|
||||
pub(super) css_viewport_height: u32,
|
||||
pub(super) rgba_byte_count: usize,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
pub(super) non_white_pixel_count: u64,
|
||||
@@ -105,6 +111,10 @@ pub(super) struct LiveFrameReport {
|
||||
pub(super) sample_hash: u64,
|
||||
}
|
||||
|
||||
fn default_device_pixel_ratio() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Per-frame tag that tells the renderer which already-imported
|
||||
/// `MTLTexture` to sample. Emitted at `trace` instead of `info` because
|
||||
/// it fires every frame on the hardware path; the import event above
|
||||
|
||||
@@ -89,14 +89,19 @@ pub(super) fn default_sidecar_command() -> Result<SidecarCommandTarget, SidecarC
|
||||
SidecarCommandError::CurrentExecutableDirectoryUnavailable { path: current_exe.clone() }
|
||||
})?;
|
||||
let adjacent_sidecar = exe_dir.join(sidecar_binary_name());
|
||||
if adjacent_sidecar.is_file() && is_macos_app_bundle_exe_dir(exe_dir) {
|
||||
return Ok(SidecarCommandTarget::Binary(adjacent_sidecar));
|
||||
}
|
||||
let workspace_manifest = workspace_manifest_path();
|
||||
let workspace_target_sidecar =
|
||||
workspace_manifest.as_ref().and_then(|path| workspace_target_sidecar_path(path));
|
||||
let adjacent_is_workspace_target =
|
||||
workspace_target_sidecar.as_ref().is_some_and(|path| path == &adjacent_sidecar);
|
||||
let workspace_target_sidecar_exists =
|
||||
workspace_target_sidecar.as_ref().is_some_and(|path| path.is_file());
|
||||
let prefer_cargo_hardware_sidecar = rendering_context_from_env()
|
||||
== SidecarRenderingContext::Hardware
|
||||
&& adjacent_is_workspace_target
|
||||
&& (adjacent_is_workspace_target || workspace_target_sidecar_exists)
|
||||
&& workspace_manifest.as_ref().is_some_and(|path| path.is_file());
|
||||
if adjacent_sidecar.is_file() && !prefer_cargo_hardware_sidecar {
|
||||
return Ok(SidecarCommandTarget::Binary(adjacent_sidecar));
|
||||
@@ -155,11 +160,22 @@ fn sidecar_binary_name() -> String {
|
||||
format!("ely_servo_sidecar{}", env::consts::EXE_SUFFIX)
|
||||
}
|
||||
|
||||
fn is_macos_app_bundle_exe_dir(path: &Path) -> bool {
|
||||
path.file_name().is_some_and(|name| name == "MacOS")
|
||||
&& path
|
||||
.parent()
|
||||
.is_some_and(|contents| contents.file_name().is_some_and(|name| name == "Contents"))
|
||||
&& path
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.is_some_and(|bundle| bundle.extension().is_some_and(|extension| extension == "app"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
HARDWARE_SIDECAR_FEATURES, SOFTWARE_SIDECAR_FEATURES, SidecarRenderingContext,
|
||||
rendering_context_selection,
|
||||
is_macos_app_bundle_exe_dir, rendering_context_selection,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -180,6 +196,14 @@ mod tests {
|
||||
assert_eq!(context.sidecar_features(), SOFTWARE_SIDECAR_FEATURES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_macos_app_bundle_executable_directory() {
|
||||
assert!(is_macos_app_bundle_exe_dir(std::path::Path::new(
|
||||
"/tmp/ELY Browser.app/Contents/MacOS"
|
||||
)));
|
||||
assert!(!is_macos_app_bundle_exe_dir(std::path::Path::new("/tmp/target/debug")));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn defaults_to_hardware_rendering_context_on_macos() {
|
||||
|
||||
Reference in New Issue
Block a user