perf(app): gate payloadless surfaces on app imports

This commit is contained in:
2026-05-16 03:17:45 -04:00
parent dbfcbdb377
commit 54fcce29f3
8 changed files with 748 additions and 430 deletions
+46 -25
View File
@@ -73,33 +73,18 @@ impl IOSurfaceCache {
/// dimensions: duplicate handles for the same sized IOSurface are /// dimensions: duplicate handles for the same sized IOSurface are
/// discarded, while a resized IOSurface that reuses the same /// discarded, while a resized IOSurface that reuses the same
/// `surface_id` replaces the cached pixel buffer. /// `surface_id` replaces the cached pixel buffer.
#[cfg(test)]
pub fn import( pub fn import(
&mut self, &mut self,
mach_port_name: u32, mach_port_name: u32,
surface_id: u64, surface_id: u64,
) -> Result<(), SurfaceImportError> { ) -> Result<(), SurfaceImportError> {
let Some(iosurface) = objc2_io_surface::IOSurfaceRef::lookup_from_mach_port(mach_port_name) let pixel_buffer = import_pixel_buffer_from_mach_port(mach_port_name)?;
else { self.insert_pixel_buffer(surface_id, pixel_buffer);
return Err(SurfaceImportError::LookupFailed { port: mach_port_name }); Ok(())
}; }
// Both objc2-io-surface and the legacy `io_surface` crate wrap pub(crate) fn insert_pixel_buffer(&mut self, surface_id: u64, pixel_buffer: CVPixelBuffer) {
// the same C `__IOSurface` pointer. CVPixelBufferCreateWithIOSurface
// (via core-video) expects the legacy crate's wrapper. Reach for
// the raw pointer and let TCFType CFRetain it independently so
// both Rust handles can drop without double-freeing.
let raw_ptr: *const std::ffi::c_void =
(&*iosurface) as *const objc2_io_surface::IOSurfaceRef as *const std::ffi::c_void;
#[allow(deprecated)]
let io_surface_view: IOSurface = {
#[expect(unsafe_code)]
unsafe {
IOSurface::wrap_under_get_rule(raw_ptr as io_surface::IOSurfaceRef)
}
};
let pixel_buffer = CVPixelBuffer::from_io_surface(&io_surface_view, None)
.map_err(|status| SurfaceImportError::PixelBufferBuildFailed { status })?;
let width = pixel_buffer.get_width() as u32; let width = pixel_buffer.get_width() as u32;
let height = pixel_buffer.get_height() as u32; let height = pixel_buffer.get_height() as u32;
@@ -108,13 +93,10 @@ impl IOSurfaceCache {
.get(&surface_id) .get(&surface_id)
.is_some_and(|cached| cached.width == width && cached.height == height) .is_some_and(|cached| cached.width == width && cached.height == height)
{ {
deallocate_mach_port(mach_port_name); return;
return Ok(());
} }
self.pixel_buffers.insert(surface_id, CachedPixelBuffer { pixel_buffer, width, height }); self.pixel_buffers.insert(surface_id, CachedPixelBuffer { pixel_buffer, width, height });
deallocate_mach_port(mach_port_name);
Ok(())
} }
/// Look up an already-imported pixel buffer by `surface_id`. The /// Look up an already-imported pixel buffer by `surface_id`. The
@@ -127,12 +109,51 @@ impl IOSurfaceCache {
self.pixel_buffers.get(&surface_id).map(|cached| cached.pixel_buffer.clone()) self.pixel_buffers.get(&surface_id).map(|cached| cached.pixel_buffer.clone())
} }
pub(crate) fn surface_ids(&self) -> Vec<u64> {
self.pixel_buffers.keys().copied().collect()
}
#[cfg(test)] #[cfg(test)]
pub fn cached_surface_count(&self) -> usize { pub fn cached_surface_count(&self) -> usize {
self.pixel_buffers.len() self.pixel_buffers.len()
} }
} }
pub(crate) fn import_pixel_buffer_from_mach_port(
mach_port_name: u32,
) -> Result<CVPixelBuffer, SurfaceImportError> {
let result = build_pixel_buffer_from_mach_port(mach_port_name);
deallocate_mach_port(mach_port_name);
result
}
fn build_pixel_buffer_from_mach_port(
mach_port_name: u32,
) -> Result<CVPixelBuffer, SurfaceImportError> {
let Some(iosurface) = objc2_io_surface::IOSurfaceRef::lookup_from_mach_port(mach_port_name)
else {
return Err(SurfaceImportError::LookupFailed { port: mach_port_name });
};
// Both objc2-io-surface and the legacy `io_surface` crate wrap
// the same C `__IOSurface` pointer. CVPixelBufferCreateWithIOSurface
// (via core-video) expects the legacy crate's wrapper. Reach for
// the raw pointer and let TCFType CFRetain it independently so
// both Rust handles can drop without double-freeing.
let raw_ptr: *const std::ffi::c_void =
(&*iosurface) as *const objc2_io_surface::IOSurfaceRef as *const std::ffi::c_void;
#[allow(deprecated)]
let io_surface_view: IOSurface = {
#[expect(unsafe_code)]
unsafe {
IOSurface::wrap_under_get_rule(raw_ptr as io_surface::IOSurfaceRef)
}
};
CVPixelBuffer::from_io_surface(&io_surface_view, None)
.map_err(|status| SurfaceImportError::PixelBufferBuildFailed { status })
}
/// Release one send right against the mach port we received. The /// Release one send right against the mach port we received. The
/// IOSurface itself stays alive because the `CVPixelBuffer` (or the /// IOSurface itself stays alive because the `CVPixelBuffer` (or the
/// sidecar's surfman) still retain it. /// sidecar's surfman) still retain it.
+101 -360
View File
@@ -1,36 +1,37 @@
use std::{ use std::{
io::{self, BufRead, BufReader, Read, Write}, io::{BufRead, BufReader, Read, Write},
path::PathBuf, path::PathBuf,
process::{Child, ChildStdin, ChildStdout, Stdio}, process::{Child, ChildStdin, ChildStdout, Stdio},
time::Duration,
}; };
#[cfg(target_os = "macos")]
#[path = "servo_live_iosurface_importer.rs"]
mod iosurface_importer;
#[path = "servo_live_types.rs"]
mod types;
/// Environment variable that lets the user pick the rendering context /// Environment variable that lets the user pick the rendering context
/// kind used by the spawned sidecar. Accepted values: `software` /// kind used by the spawned sidecar. Accepted values: `software`
/// and `hardware`. macOS defaults to the hardware path and receives /// and `hardware`. macOS defaults to the hardware path and receives
/// IOSurface mach send rights over a side Mach channel. /// IOSurface mach send rights over a side Mach channel.
use ely_domain::SitePermissionDecision;
use serde::Serialize;
use thiserror::Error;
#[path = "servo_live_wire.rs"] #[path = "servo_live_wire.rs"]
mod wire; mod wire;
pub(crate) use types::{
ServoLiveEnsureRequest, ServoLiveError, ServoLiveFrame, ServoLiveSitePermission,
};
use super::servo_sidecar_command::{ use super::servo_sidecar_command::{
SidecarCommandError, SidecarRenderingContext, default_sidecar_command, SidecarRenderingContext, default_sidecar_command, rendering_context_from_env,
rendering_context_from_env,
}; };
use wire::{ use wire::{
LiveFrameReport, LiveRequest, LiveResponse, LiveSurfaceHandle, log_frame_perf, LiveRequest, LiveResponse, LiveSurfaceHandle, log_frame_perf, log_iosurface_current,
log_iosurface_current, log_iosurface_handle, log_iosurface_handle,
}; };
#[cfg(target_os = "macos")]
use super::iosurface_mach::{IOSurfaceMachError, IOSurfaceMachReceiver};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use super::iosurface_metal::IOSurfaceCache; use super::iosurface_metal::IOSurfaceCache;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use core_video::pixel_buffer::CVPixelBuffer; use iosurface_importer::{IOSurfaceImportResult, IOSurfaceImportWorker};
pub(crate) struct ServoLiveClient { pub(crate) struct ServoLiveClient {
child: Child, child: Child,
@@ -42,7 +43,7 @@ pub(crate) struct ServoLiveClient {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
iosurface_cache: IOSurfaceCache, iosurface_cache: IOSurfaceCache,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
iosurface_receiver: Option<IOSurfaceMachReceiver>, iosurface_importer: Option<IOSurfaceImportWorker>,
} }
impl ServoLiveClient { impl ServoLiveClient {
@@ -57,10 +58,13 @@ impl ServoLiveClient {
command.arg("live").arg("--profile-data-dir").arg(profile_data_dir); command.arg("live").arg("--profile-data-dir").arg(profile_data_dir);
command.arg("--rendering-context").arg(rendering_context.cli_arg()); command.arg("--rendering-context").arg(rendering_context.cli_arg());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let iosurface_receiver = if rendering_context == SidecarRenderingContext::Hardware { let iosurface_importer = if rendering_context == SidecarRenderingContext::Hardware {
let receiver = IOSurfaceMachReceiver::new()?; let receiver = super::iosurface_mach::IOSurfaceMachReceiver::new()?;
command.arg("--iosurface-mach-service").arg(receiver.service_name()); command.arg("--iosurface-mach-service").arg(receiver.service_name());
Some(receiver) Some(
IOSurfaceImportWorker::new(receiver)
.map_err(ServoLiveError::IOSurfaceImportWorker)?,
)
} else { } else {
None None
}; };
@@ -81,7 +85,7 @@ impl ServoLiveClient {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
iosurface_cache: IOSurfaceCache::new(), iosurface_cache: IOSurfaceCache::new(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
iosurface_receiver, iosurface_importer,
}) })
} }
@@ -89,6 +93,9 @@ impl ServoLiveClient {
&mut self, &mut self,
request: ServoLiveEnsureRequest, request: ServoLiveEnsureRequest,
) -> Result<Option<ServoLiveFrame>, ServoLiveError> { ) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
#[cfg(target_os = "macos")]
self.drain_iosurface_imports()?;
let ready_surface_ids = self.ready_surface_ids();
self.request(LiveRequest::Ensure { self.request(LiveRequest::Ensure {
tab_id: request.tab_id, tab_id: request.tab_id,
profile_id: request.profile_id, profile_id: request.profile_id,
@@ -107,11 +114,15 @@ impl ServoLiveClient {
hover_y: request.hover_y, hover_y: request.hover_y,
typed_text: request.typed_text, typed_text: request.typed_text,
site_permissions: request.site_permissions, site_permissions: request.site_permissions,
ready_surface_ids,
}) })
} }
pub fn poll(&mut self, tab_id: String) -> Result<Option<ServoLiveFrame>, ServoLiveError> { pub fn poll(&mut self, tab_id: String) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
self.request(LiveRequest::Poll { tab_id }) #[cfg(target_os = "macos")]
self.drain_iosurface_imports()?;
let ready_surface_ids = self.ready_surface_ids();
self.request(LiveRequest::Poll { tab_id, ready_surface_ids })
} }
pub fn close(&mut self, tab_id: String) -> Result<(), ServoLiveError> { pub fn close(&mut self, tab_id: String) -> Result<(), ServoLiveError> {
@@ -183,363 +194,93 @@ impl ServoLiveClient {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if let Some(handle) = surface_handle.as_ref() { if let Some(handle) = surface_handle.as_ref() {
// Drain the stdout payload before IOSurface import so the self.queue_iosurface_handle(*handle)?;
// sidecar cannot block writing RGBA bytes while this worker self.drain_iosurface_imports()?;
// is inside IOSurfaceLookupFromMachPort.
self.import_iosurface_handle(handle)?;
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if let Some(surface_id) = current_surface_id { if let Some(surface_id) = current_surface_id {
let pixel_buffer = self.iosurface_cache.pixel_buffer_for(surface_id); let pixel_buffer = self.iosurface_cache.pixel_buffer_for(surface_id);
if pixel_buffer.is_none() && !has_software_payload { if pixel_buffer.is_none() && !has_software_payload {
return Err(ServoLiveError::IOSurfacePixelBufferMissing { surface_id }); return Ok(None);
} }
frame.pixel_buffer = pixel_buffer; frame.set_pixel_buffer(pixel_buffer);
} }
Ok(Some(frame)) Ok(Some(frame))
} }
} }
#[cfg(not(target_os = "macos"))]
impl ServoLiveClient {
fn ready_surface_ids(&self) -> Vec<u64> {
Vec::new()
}
}
#[cfg(target_os = "macos")]
impl ServoLiveClient {
fn ready_surface_ids(&self) -> Vec<u64> {
self.iosurface_cache.surface_ids()
}
fn queue_iosurface_handle(&mut self, handle: LiveSurfaceHandle) -> Result<(), ServoLiveError> {
let Some(importer) = self.iosurface_importer.as_ref() else {
return Err(ServoLiveError::IOSurfaceImportFailed {
surface_id: handle.surface_id,
mach_port_name: handle.mach_port_name,
message: "IOSurface import worker is unavailable".to_string(),
});
};
importer.submit(handle).map_err(|failure| ServoLiveError::IOSurfaceImportFailed {
surface_id: failure.surface_id,
mach_port_name: failure.mach_port_name,
message: failure.message,
})
}
fn drain_iosurface_imports(&mut self) -> Result<(), ServoLiveError> {
let Some(importer) = self.iosurface_importer.as_ref() else {
return Ok(());
};
for result in importer.drain() {
match result {
IOSurfaceImportResult::Imported(imported) => {
self.iosurface_cache
.insert_pixel_buffer(imported.surface_id, imported.pixel_buffer);
tracing::info!(
target: "ely::servo::iosurface",
surface_id = imported.surface_id,
width = imported.width,
height = imported.height,
"imported IOSurface into CVPixelBuffer cache",
);
}
IOSurfaceImportResult::Failed(failure) => {
tracing::warn!(
target: "ely::servo::iosurface",
surface_id = failure.surface_id,
width = failure.width,
height = failure.height,
mach_port_name = failure.mach_port_name,
message = %failure.message,
"IOSurface import worker failed",
);
return Err(ServoLiveError::IOSurfaceImportFailed {
surface_id: failure.surface_id,
mach_port_name: failure.mach_port_name,
message: failure.message,
});
}
}
}
Ok(())
}
}
impl Drop for ServoLiveClient { impl Drop for ServoLiveClient {
fn drop(&mut self) { fn drop(&mut self) {
let _ = self.child.kill(); let _ = self.child.kill();
let _ = self.child.wait(); let _ = self.child.wait();
} }
} }
#[cfg(target_os = "macos")]
impl ServoLiveClient {
/// Convert the sidecar's `surface_handle` into a `CVPixelBuffer`
/// in the local cache. A later frame with a missing pixel buffer
/// becomes a web-surface error instead of a blank ready frame.
fn import_iosurface_handle(
&mut self,
handle: &LiveSurfaceHandle,
) -> Result<(), ServoLiveError> {
let mach_port_name = match self.iosurface_receiver.as_mut() {
Some(receiver) => {
receiver.receive_port_for_surface(handle.surface_id, Duration::from_secs(1))?
}
None => handle.mach_port_name,
};
match self.iosurface_cache.import(mach_port_name, handle.surface_id) {
Ok(()) => tracing::info!(
target: "ely::servo::iosurface",
surface_id = handle.surface_id,
width = handle.width,
height = handle.height,
"imported IOSurface into CVPixelBuffer cache",
),
Err(error) => {
tracing::warn!(
target: "ely::servo::iosurface",
error = %error,
surface_id = handle.surface_id,
"IOSurface→CVPixelBuffer import failed",
);
return Err(ServoLiveError::IOSurfaceImportFailed {
surface_id: handle.surface_id,
mach_port_name,
message: error.to_string(),
});
}
}
Ok(())
}
}
pub(crate) struct ServoLiveEnsureRequest {
pub(crate) tab_id: String,
pub(crate) profile_id: String,
pub(crate) url: String,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) page_zoom_percent: u16,
/// Display scale factor (1.0 standard, 2.0 Retina). Servo's
/// WebView lays out CSS pixels = device pixels / hidpi factor;
/// without this, a Retina viewport gets desktop-CSS-pixel layout
/// and every visible element renders at half its expected size.
pub(crate) device_pixel_ratio: f32,
pub(crate) scroll_delta_x: i32,
pub(crate) scroll_delta_y: i32,
pub(crate) scroll_point_x: Option<u32>,
pub(crate) scroll_point_y: Option<u32>,
pub(crate) click_x: Option<u32>,
pub(crate) click_y: Option<u32>,
pub(crate) hover_x: Option<u32>,
pub(crate) hover_y: Option<u32>,
pub(crate) typed_text: Option<String>,
pub(crate) site_permissions: Vec<ServoLiveSitePermission>,
}
#[derive(Clone, Debug, Serialize)]
pub(crate) struct ServoLiveSitePermission {
pub(crate) origin: String,
pub(crate) feature: String,
pub(crate) decision: String,
}
impl ServoLiveSitePermission {
pub fn new(
origin: impl Into<String>,
feature: impl Into<String>,
decision: SitePermissionDecision,
) -> Self {
Self { origin: origin.into(), feature: feature.into(), decision: decision.as_str().into() }
}
}
pub(crate) struct ServoLiveFrame {
loaded_url: Option<String>,
title: Option<String>,
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"))]
content_pixel_count: u64,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: u64,
rgba_bytes: Vec<u8>,
/// Hardware-path surface: the imported IOSurface published by the
/// sidecar, wrapped as a CVPixelBuffer for GPUI's `surface(...)`.
#[cfg(target_os = "macos")]
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"))]
content_pixel_count: report.content_pixel_count,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: report.sample_hash,
rgba_bytes,
#[cfg(target_os = "macos")]
pixel_buffer: None,
}
}
/// Returns the imported `CVPixelBuffer` matching the frame's
/// current hardware surface.
#[cfg(target_os = "macos")]
#[must_use]
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
self.pixel_buffer.as_ref()
}
#[must_use]
pub fn loaded_url(&self) -> Option<&str> {
self.loaded_url.as_deref()
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
#[must_use]
pub fn render_state(&self) -> &str {
self.render_state.as_str()
}
#[must_use]
pub fn width(&self) -> u32 {
self.width
}
#[must_use]
pub fn height(&self) -> u32 {
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 {
self.non_white_pixel_count
}
#[cfg(all(test, feature = "live-site-smoke"))]
#[must_use]
pub fn content_pixel_count(&self) -> u64 {
self.content_pixel_count
}
#[cfg(all(test, feature = "live-site-smoke"))]
#[must_use]
pub fn sample_hash(&self) -> u64 {
self.sample_hash
}
#[must_use]
pub fn into_rgba_bytes(self) -> Vec<u8> {
self.rgba_bytes
}
#[cfg(test)]
pub(crate) fn for_test(width: u32, height: u32, rgba_bytes: Vec<u8>) -> Self {
Self {
loaded_url: Some("https://example.com/".to_string()),
title: Some("Example".to_string()),
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"))]
content_pixel_count: 0,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: 0,
rgba_bytes,
#[cfg(target_os = "macos")]
pixel_buffer: None,
}
}
#[cfg(all(test, target_os = "macos"))]
pub(crate) fn for_test_with_pixel_buffer(
width: u32,
height: u32,
pixel_buffer: CVPixelBuffer,
) -> Self {
Self {
loaded_url: Some("https://example.com/".to_string()),
title: Some("Example".to_string()),
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"))]
content_pixel_count: 0,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: 0,
rgba_bytes: Vec::new(),
pixel_buffer: Some(pixel_buffer),
}
}
}
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}")]
SidecarBinaryUnavailable { path: PathBuf },
#[error("failed to run servo live sidecar: {0}")]
Command(#[source] io::Error),
#[error("servo live sidecar pipe is unavailable: {name}")]
PipeUnavailable { name: &'static str },
#[error("servo live sidecar exited")]
SidecarExited,
#[error("servo live sidecar failed: {message}")]
SidecarFailed { message: String },
#[error("failed to read servo live frame bytes: {0}")]
FrameRead(#[source] io::Error),
#[error(
"servo live sidecar advertised {advertised} frame bytes which exceeds \
the {width}x{height} pixel budget ({pixel_budget} bytes)"
)]
FrameBudgetExceeded { advertised: usize, pixel_budget: u64, width: u32, height: u32 },
#[cfg(target_os = "macos")]
#[error(
"servo live IOSurface import failed for surface {surface_id:#x} \
mach port 0x{mach_port_name:x}: {message}"
)]
IOSurfaceImportFailed { surface_id: u64, mach_port_name: u32, message: String },
#[cfg(target_os = "macos")]
#[error("servo live IOSurface {surface_id:#x} was selected before its pixel buffer import")]
IOSurfacePixelBufferMissing { surface_id: u64 },
#[cfg(target_os = "macos")]
#[error(transparent)]
IOSurfaceMach(#[from] IOSurfaceMachError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[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,
}
}
}
@@ -0,0 +1,147 @@
#![cfg(target_os = "macos")]
use std::{
io,
sync::mpsc,
thread::{self, JoinHandle},
time::Duration,
};
use core_video::pixel_buffer::CVPixelBuffer;
use crate::services::{
iosurface_mach::IOSurfaceMachReceiver, iosurface_metal::import_pixel_buffer_from_mach_port,
};
use super::wire::LiveSurfaceHandle;
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(1);
pub(super) struct IOSurfaceImportWorker {
request_tx: Option<mpsc::Sender<LiveSurfaceHandle>>,
result_rx: mpsc::Receiver<IOSurfaceImportResult>,
thread: Option<JoinHandle<()>>,
}
impl IOSurfaceImportWorker {
pub(super) fn new(receiver: IOSurfaceMachReceiver) -> Result<Self, io::Error> {
let (request_tx, request_rx) = mpsc::channel();
let (result_tx, result_rx) = mpsc::channel();
let thread = thread::Builder::new()
.name("ely-iosurface-import".to_string())
.spawn(move || run_import_worker(receiver, request_rx, result_tx))?;
Ok(Self { request_tx: Some(request_tx), result_rx, thread: Some(thread) })
}
pub(super) fn submit(&self, handle: LiveSurfaceHandle) -> Result<(), IOSurfaceImportFailure> {
let Some(request_tx) = self.request_tx.as_ref() else {
return Err(IOSurfaceImportFailure::worker_stopped(handle));
};
request_tx.send(handle).map_err(|error| IOSurfaceImportFailure::worker_stopped(error.0))
}
pub(super) fn drain(&self) -> Vec<IOSurfaceImportResult> {
let mut results = Vec::new();
while let Ok(result) = self.result_rx.try_recv() {
results.push(result);
}
results
}
}
impl Drop for IOSurfaceImportWorker {
fn drop(&mut self) {
self.request_tx.take();
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
pub(super) enum IOSurfaceImportResult {
Imported(ImportedIOSurface),
Failed(IOSurfaceImportFailure),
}
pub(super) struct ImportedIOSurface {
pub(super) surface_id: u64,
pub(super) width: u32,
pub(super) height: u32,
pub(super) pixel_buffer: CVPixelBuffer,
}
// SAFETY: CVPixelBuffer is a CoreFoundation object with atomic
// retain/release semantics. This wrapper crosses from the importer
// thread to the live worker thread; GPUI presentation already receives
// the same handle through ServoLiveFrame's Send contract.
#[expect(unsafe_code)]
unsafe impl Send for ImportedIOSurface {}
pub(super) struct IOSurfaceImportFailure {
pub(super) surface_id: u64,
pub(super) width: u32,
pub(super) height: u32,
pub(super) mach_port_name: u32,
pub(super) message: String,
}
impl IOSurfaceImportFailure {
fn worker_stopped(handle: LiveSurfaceHandle) -> Self {
Self {
surface_id: handle.surface_id,
width: handle.width,
height: handle.height,
mach_port_name: handle.mach_port_name,
message: "IOSurface import worker stopped".to_string(),
}
}
}
fn run_import_worker(
mut receiver: IOSurfaceMachReceiver,
request_rx: mpsc::Receiver<LiveSurfaceHandle>,
result_tx: mpsc::Sender<IOSurfaceImportResult>,
) {
while let Ok(handle) = request_rx.recv() {
let result = import_surface_handle(&mut receiver, handle);
if result_tx.send(result).is_err() {
return;
}
}
}
fn import_surface_handle(
receiver: &mut IOSurfaceMachReceiver,
handle: LiveSurfaceHandle,
) -> IOSurfaceImportResult {
let mach_port_name = match receiver.receive_port_for_surface(handle.surface_id, RECEIVE_TIMEOUT)
{
Ok(mach_port_name) => mach_port_name,
Err(error) => {
return IOSurfaceImportResult::Failed(IOSurfaceImportFailure {
surface_id: handle.surface_id,
width: handle.width,
height: handle.height,
mach_port_name: handle.mach_port_name,
message: error.to_string(),
});
}
};
match import_pixel_buffer_from_mach_port(mach_port_name) {
Ok(pixel_buffer) => IOSurfaceImportResult::Imported(ImportedIOSurface {
surface_id: handle.surface_id,
width: handle.width,
height: handle.height,
pixel_buffer,
}),
Err(error) => IOSurfaceImportResult::Failed(IOSurfaceImportFailure {
surface_id: handle.surface_id,
width: handle.width,
height: handle.height,
mach_port_name,
message: error.to_string(),
}),
}
}
@@ -0,0 +1,308 @@
use std::{io, path::PathBuf};
use ely_domain::SitePermissionDecision;
use serde::Serialize;
use thiserror::Error;
use super::wire::LiveFrameReport;
use crate::services::servo_sidecar_command::SidecarCommandError;
#[cfg(target_os = "macos")]
use crate::services::iosurface_mach::IOSurfaceMachError;
#[cfg(target_os = "macos")]
use core_video::pixel_buffer::CVPixelBuffer;
pub(crate) struct ServoLiveEnsureRequest {
pub(crate) tab_id: String,
pub(crate) profile_id: String,
pub(crate) url: String,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) page_zoom_percent: u16,
/// Display scale factor (1.0 standard, 2.0 Retina). Servo's
/// WebView lays out CSS pixels = device pixels / hidpi factor;
/// without this, a Retina viewport gets desktop-CSS-pixel layout
/// and every visible element renders at half its expected size.
pub(crate) device_pixel_ratio: f32,
pub(crate) scroll_delta_x: i32,
pub(crate) scroll_delta_y: i32,
pub(crate) scroll_point_x: Option<u32>,
pub(crate) scroll_point_y: Option<u32>,
pub(crate) click_x: Option<u32>,
pub(crate) click_y: Option<u32>,
pub(crate) hover_x: Option<u32>,
pub(crate) hover_y: Option<u32>,
pub(crate) typed_text: Option<String>,
pub(crate) site_permissions: Vec<ServoLiveSitePermission>,
}
#[derive(Clone, Debug, Serialize)]
pub(crate) struct ServoLiveSitePermission {
pub(crate) origin: String,
pub(crate) feature: String,
pub(crate) decision: String,
}
impl ServoLiveSitePermission {
pub fn new(
origin: impl Into<String>,
feature: impl Into<String>,
decision: SitePermissionDecision,
) -> Self {
Self { origin: origin.into(), feature: feature.into(), decision: decision.as_str().into() }
}
}
pub(crate) struct ServoLiveFrame {
loaded_url: Option<String>,
title: Option<String>,
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"))]
content_pixel_count: u64,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: u64,
rgba_bytes: Vec<u8>,
#[cfg(target_os = "macos")]
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 {
pub(super) 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"))]
content_pixel_count: report.content_pixel_count,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: report.sample_hash,
rgba_bytes,
#[cfg(target_os = "macos")]
pixel_buffer: None,
}
}
#[cfg(target_os = "macos")]
pub(super) fn set_pixel_buffer(&mut self, pixel_buffer: Option<CVPixelBuffer>) {
self.pixel_buffer = pixel_buffer;
}
#[cfg(target_os = "macos")]
#[must_use]
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
self.pixel_buffer.as_ref()
}
#[must_use]
pub fn loaded_url(&self) -> Option<&str> {
self.loaded_url.as_deref()
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
#[must_use]
pub fn render_state(&self) -> &str {
self.render_state.as_str()
}
#[must_use]
pub fn width(&self) -> u32 {
self.width
}
#[must_use]
pub fn height(&self) -> u32 {
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 {
self.non_white_pixel_count
}
#[cfg(all(test, feature = "live-site-smoke"))]
#[must_use]
pub fn content_pixel_count(&self) -> u64 {
self.content_pixel_count
}
#[cfg(all(test, feature = "live-site-smoke"))]
#[must_use]
pub fn sample_hash(&self) -> u64 {
self.sample_hash
}
#[must_use]
pub fn into_rgba_bytes(self) -> Vec<u8> {
self.rgba_bytes
}
#[cfg(test)]
pub(crate) fn for_test(width: u32, height: u32, rgba_bytes: Vec<u8>) -> Self {
Self {
loaded_url: Some("https://example.com/".to_string()),
title: Some("Example".to_string()),
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"))]
content_pixel_count: 0,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: 0,
rgba_bytes,
#[cfg(target_os = "macos")]
pixel_buffer: None,
}
}
#[cfg(all(test, target_os = "macos"))]
pub(crate) fn for_test_with_pixel_buffer(
width: u32,
height: u32,
pixel_buffer: CVPixelBuffer,
) -> Self {
Self {
loaded_url: Some("https://example.com/".to_string()),
title: Some("Example".to_string()),
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"))]
content_pixel_count: 0,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: 0,
rgba_bytes: Vec::new(),
pixel_buffer: Some(pixel_buffer),
}
}
}
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}")]
SidecarBinaryUnavailable { path: PathBuf },
#[error("failed to run servo live sidecar: {0}")]
Command(#[source] io::Error),
#[error("servo live sidecar pipe is unavailable: {name}")]
PipeUnavailable { name: &'static str },
#[error("servo live sidecar exited")]
SidecarExited,
#[error("servo live sidecar failed: {message}")]
SidecarFailed { message: String },
#[error("failed to read servo live frame bytes: {0}")]
FrameRead(#[source] io::Error),
#[error(
"servo live sidecar advertised {advertised} frame bytes which exceeds \
the {width}x{height} pixel budget ({pixel_budget} bytes)"
)]
FrameBudgetExceeded { advertised: usize, pixel_budget: u64, width: u32, height: u32 },
#[cfg(target_os = "macos")]
#[error(
"servo live IOSurface import failed for surface {surface_id:#x} \
mach port 0x{mach_port_name:x}: {message}"
)]
IOSurfaceImportFailed { surface_id: u64, mach_port_name: u32, message: String },
#[cfg(target_os = "macos")]
#[error("failed to spawn servo live IOSurface importer: {0}")]
IOSurfaceImportWorker(#[source] io::Error),
#[cfg(target_os = "macos")]
#[error(transparent)]
IOSurfaceMach(#[from] IOSurfaceMachError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[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,
}
}
}
@@ -23,9 +23,11 @@ pub(super) enum LiveRequest {
hover_y: Option<u32>, hover_y: Option<u32>,
typed_text: Option<String>, typed_text: Option<String>,
site_permissions: Vec<ServoLiveSitePermission>, site_permissions: Vec<ServoLiveSitePermission>,
ready_surface_ids: Vec<u64>,
}, },
Poll { Poll {
tab_id: String, tab_id: String,
ready_surface_ids: Vec<u64>,
}, },
Close { Close {
tab_id: String, tab_id: String,
@@ -26,7 +26,8 @@ use super::perf::{FramePerfAggregator, FramePerfSummary, elapsed_ns};
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> { pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
let LiveArgs { profile_data_dir, iosurface_mach_service, rendering_context_kind } = args; let LiveArgs { profile_data_dir, iosurface_mach_service, rendering_context_kind } = args;
let publish_readback_surface_fields = iosurface_mach_service.is_none(); let publish_readback_surface_fields = true;
let require_client_ready_surfaces = iosurface_mach_service.is_some();
fs::create_dir_all(&profile_data_dir)?; fs::create_dir_all(&profile_data_dir)?;
let context_label = rendering_context_label(rendering_context_kind); let context_label = rendering_context_label(rendering_context_kind);
let mut host = SoftwareServoHost::new_with_config_dir_and_kind( let mut host = SoftwareServoHost::new_with_config_dir_and_kind(
@@ -66,6 +67,7 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
&mut published_surface_ids, &mut published_surface_ids,
rendering_context_kind, rendering_context_kind,
publish_readback_surface_fields, publish_readback_surface_fields,
require_client_ready_surfaces,
request, request,
), ),
Err(error) => Err(LiveSidecarError::Json(error)), Err(error) => Err(LiveSidecarError::Json(error)),
@@ -95,6 +97,7 @@ fn handle_request(
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>, published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
rendering_context_kind: RenderingContextKind, rendering_context_kind: RenderingContextKind,
publish_readback_surface_fields: bool, publish_readback_surface_fields: bool,
require_client_ready_surfaces: bool,
request: LiveRequest, request: LiveRequest,
) -> Result<LiveOutcome, LiveSidecarError> { ) -> Result<LiveOutcome, LiveSidecarError> {
match request { match request {
@@ -116,6 +119,7 @@ fn handle_request(
hover_y, hover_y,
typed_text, typed_text,
site_permissions, site_permissions,
ready_surface_ids,
} => { } => {
let tab = TabId::parse(tab_id.clone())?; let tab = TabId::parse(tab_id.clone())?;
let profile = ProfileId::parse(profile_id)?; let profile = ProfileId::parse(profile_id)?;
@@ -162,8 +166,17 @@ fn handle_request(
session.awaiting_visible_frame = true; session.awaiting_visible_frame = true;
} }
let webview_id = session.webview_id.clone(); let webview_id = session.webview_id.clone();
let mut outcome = let mut outcome = poll_frame(
poll_frame(host, session, rendering_context_kind, &tab_id, published_surface_ids)?; host,
session,
rendering_context_kind,
payloadless_readiness(
&tab_id,
published_surface_ids,
&ready_surface_ids,
require_client_ready_surfaces,
),
)?;
populate_surface_fields( populate_surface_fields(
host, host,
&webview_id, &webview_id,
@@ -174,13 +187,22 @@ fn handle_request(
); );
Ok(outcome) Ok(outcome)
} }
LiveRequest::Poll { tab_id } => { LiveRequest::Poll { tab_id, ready_surface_ids } => {
let Some(session) = sessions.get_mut(&tab_id) else { let Some(session) = sessions.get_mut(&tab_id) else {
return Ok(LiveOutcome::empty()); return Ok(LiveOutcome::empty());
}; };
let webview_id = session.webview_id.clone(); let webview_id = session.webview_id.clone();
let mut outcome = let mut outcome = poll_frame(
poll_frame(host, session, rendering_context_kind, &tab_id, published_surface_ids)?; host,
session,
rendering_context_kind,
payloadless_readiness(
&tab_id,
published_surface_ids,
&ready_surface_ids,
require_client_ready_surfaces,
),
)?;
populate_surface_fields( populate_surface_fields(
host, host,
&webview_id, &webview_id,
@@ -205,8 +227,7 @@ fn poll_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &mut LiveSession, session: &mut LiveSession,
rendering_context_kind: RenderingContextKind, rendering_context_kind: RenderingContextKind,
tab_id: &str, readiness: PayloadlessReadiness<'_>,
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
) -> Result<LiveOutcome, LiveSidecarError> { ) -> Result<LiveOutcome, LiveSidecarError> {
host.tick(); host.tick();
let snapshot = host.snapshot(&session.webview_id)?; let snapshot = host.snapshot(&session.webview_id)?;
@@ -215,14 +236,8 @@ fn poll_frame(
return Ok(LiveOutcome::empty()); return Ok(LiveOutcome::empty());
} }
let (outcome, has_visible_content) = paint_pending_frame( let (outcome, has_visible_content) =
host, paint_pending_frame(host, session, rendering_context_kind, readiness, has_pending_frame)?;
session,
rendering_context_kind,
tab_id,
published_surface_ids,
has_pending_frame,
)?;
if has_visible_content { if has_visible_content {
session.awaiting_visible_frame = false; session.awaiting_visible_frame = false;
session.ever_visible_frame = true; session.ever_visible_frame = true;
@@ -243,23 +258,18 @@ fn paint_pending_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &mut LiveSession, session: &mut LiveSession,
rendering_context_kind: RenderingContextKind, rendering_context_kind: RenderingContextKind,
tab_id: &str, readiness: PayloadlessReadiness<'_>,
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
has_pending_frame: bool, has_pending_frame: bool,
) -> Result<(LiveOutcome, bool), LiveSidecarError> { ) -> Result<(LiveOutcome, bool), LiveSidecarError> {
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))] #[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
let _ = (tab_id, published_surface_ids); let _ = readiness;
match rendering_context_kind { match rendering_context_kind {
RenderingContextKind::Software => paint_readback_frame(host, session, !has_pending_frame), RenderingContextKind::Software => paint_readback_frame(host, session, !has_pending_frame),
#[cfg(all(feature = "hardware-render", target_os = "macos"))] #[cfg(all(feature = "hardware-render", target_os = "macos"))]
RenderingContextKind::Hardware => paint_hardware_surface_frame( RenderingContextKind::Hardware => {
host, paint_hardware_surface_frame(host, session, readiness, has_pending_frame)
session, }
tab_id,
published_surface_ids,
has_pending_frame,
),
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))] #[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
RenderingContextKind::Hardware => paint_readback_frame(host, session, !has_pending_frame), RenderingContextKind::Hardware => paint_readback_frame(host, session, !has_pending_frame),
} }
@@ -288,19 +298,17 @@ fn paint_readback_frame(
fn paint_hardware_surface_frame( fn paint_hardware_surface_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &LiveSession, session: &LiveSession,
tab_id: &str, readiness: PayloadlessReadiness<'_>,
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
has_pending_frame: bool, has_pending_frame: bool,
) -> Result<(LiveOutcome, bool), LiveSidecarError> { ) -> Result<(LiveOutcome, bool), LiveSidecarError> {
if !session.ever_visible_frame { if !session.ever_visible_frame {
return paint_initial_hardware_surface_frame(host, session, !has_pending_frame); return paint_initial_hardware_surface_frame(host, session, !has_pending_frame);
} }
if !payloadless_surface_pool_ready(published_surface_ids, tab_id, session.width, session.height) if !payloadless_surface_pool_ready(readiness, session.width, session.height) {
{
return paint_readback_frame(host, session, !has_pending_frame); return paint_readback_frame(host, session, !has_pending_frame);
} }
let (outcome, identity) = paint_hardware_surface_report(host, session, !has_pending_frame)?; let (outcome, identity) = paint_hardware_surface_report(host, session, !has_pending_frame)?;
if !surface_has_been_published(published_surface_ids, tab_id, identity) { if !surface_has_been_published(readiness.published_surface_ids, readiness.tab_id, identity) {
return paint_readback_frame(host, session, true); return paint_readback_frame(host, session, true);
} }
Ok((outcome, true)) Ok((outcome, true))
@@ -352,19 +360,62 @@ fn paint_hardware_surface_report(
#[cfg(all(feature = "hardware-render", target_os = "macos"))] #[cfg(all(feature = "hardware-render", target_os = "macos"))]
fn payloadless_surface_pool_ready( fn payloadless_surface_pool_ready(
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>, readiness: PayloadlessReadiness<'_>,
tab_id: &str,
width: u32, width: u32,
height: u32, height: u32,
) -> bool { ) -> bool {
published_surface_ids.get(tab_id).is_some_and(|published| { let Some(published) = readiness.published_surface_ids.get(readiness.tab_id) else {
published return false;
};
let matching = published
.iter()
.filter(|identity| identity.width == width && identity.height == height)
.take(2)
.copied()
.collect::<Vec<_>>();
if matching.len() < 2 {
return false;
}
!readiness.require_client_ready_surfaces
|| matching
.iter() .iter()
.filter(|identity| identity.width == width && identity.height == height) .all(|identity| readiness.ready_surface_ids.contains(&identity.surface_id))
.take(2) }
.count()
>= 2 #[derive(Clone, Copy)]
}) struct PayloadlessReadiness<'a> {
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
tab_id: &'a str,
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
published_surface_ids: &'a HashMap<String, HashSet<IOSurfaceIdentity>>,
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
ready_surface_ids: &'a [u64],
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
require_client_ready_surfaces: bool,
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
_marker: std::marker::PhantomData<&'a ()>,
}
fn payloadless_readiness<'a>(
tab_id: &'a str,
published_surface_ids: &'a HashMap<String, HashSet<IOSurfaceIdentity>>,
ready_surface_ids: &'a [u64],
require_client_ready_surfaces: bool,
) -> PayloadlessReadiness<'a> {
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
{
PayloadlessReadiness {
tab_id,
published_surface_ids,
ready_surface_ids,
require_client_ready_surfaces,
}
}
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
{
let _ = (tab_id, published_surface_ids, ready_surface_ids, require_client_ready_surfaces);
PayloadlessReadiness { _marker: std::marker::PhantomData }
}
} }
#[cfg(all(feature = "hardware-render", target_os = "macos"))] #[cfg(all(feature = "hardware-render", target_os = "macos"))]
@@ -390,4 +441,49 @@ mod tests {
assert!(!should_paint_live_frame(false, false)); assert!(!should_paint_live_frame(false, false));
assert!(should_paint_live_frame(true, false)); assert!(should_paint_live_frame(true, false));
} }
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
#[test]
fn payloadless_pool_waits_for_client_ready_surfaces_when_required() {
let published = published_identities([identity(7, 800, 600), identity(8, 800, 600)]);
assert!(!payloadless_surface_pool_ready(readiness(&published, &[7], true), 800, 600));
assert!(payloadless_surface_pool_ready(readiness(&published, &[7, 8], true), 800, 600));
}
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
#[test]
fn payloadless_pool_uses_published_surfaces_for_no_mach_clients() {
let published = published_identities([identity(7, 800, 600), identity(8, 800, 600)]);
assert!(payloadless_surface_pool_ready(readiness(&published, &[], false), 800, 600));
}
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
fn readiness<'a>(
published_surface_ids: &'a HashMap<String, HashSet<IOSurfaceIdentity>>,
ready_surface_ids: &'a [u64],
require_client_ready_surfaces: bool,
) -> PayloadlessReadiness<'a> {
PayloadlessReadiness {
tab_id: "tab",
published_surface_ids,
ready_surface_ids,
require_client_ready_surfaces,
}
}
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
fn published_identities(
identities: [IOSurfaceIdentity; 2],
) -> HashMap<String, HashSet<IOSurfaceIdentity>> {
let mut published = HashMap::new();
published.insert("tab".to_string(), identities.into_iter().collect());
published
}
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
fn identity(surface_id: u64, width: u32, height: u32) -> IOSurfaceIdentity {
IOSurfaceIdentity { surface_id, width, height }
}
} }
@@ -11,11 +11,10 @@ use ely_servo_host::{IOSurfaceIdentity, SoftwareServoHost};
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings}; use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns}; use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
/// Populate the hardware surface protocol fields on `outcome`. Mach /// Populate the hardware surface protocol fields on `outcome`. Readback
/// app clients keep readback frames free of surface fields because /// warm-up frames publish IOSurface handles so the app can import them
/// synchronous IOSurface import can block the live worker; the no-Mach /// on its dedicated importer thread before steady-state payloadless
/// bench path publishes readback warm-up handles so it can validate /// frames select the rotating surface ids. Two pieces of state ride out
/// payloadless steady-state frames. Two pieces of state ride out
/// together: /// together:
/// ///
/// * `current_surface_id` — set on every payload-bearing hardware /// * `current_surface_id` — set on every payload-bearing hardware
@@ -43,9 +43,13 @@ pub(super) enum LiveRequest {
hover_y: Option<u32>, hover_y: Option<u32>,
typed_text: Option<String>, typed_text: Option<String>,
site_permissions: Vec<LiveSitePermission>, site_permissions: Vec<LiveSitePermission>,
#[serde(default)]
ready_surface_ids: Vec<u64>,
}, },
Poll { Poll {
tab_id: String, tab_id: String,
#[serde(default)]
ready_surface_ids: Vec<u64>,
}, },
Close { Close {
tab_id: String, tab_id: String,