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:
@@ -2,8 +2,7 @@ use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ely_domain::{DEFAULT_ZOOM_PERCENT, ProfileId, TabId, UrlText};
|
||||
@@ -11,7 +10,7 @@ use ely_servo_host::{
|
||||
IOSurfaceIdentity, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest,
|
||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest,
|
||||
RenderingContextKind, ResizeRequest, ScrollRequest, ServoHost, ServoSurfaceSize,
|
||||
SoftwareServoHost,
|
||||
SoftwareServoHost, WebViewState,
|
||||
};
|
||||
|
||||
use super::args::LiveArgs;
|
||||
@@ -24,12 +23,6 @@ use super::live_protocol::{
|
||||
};
|
||||
use super::perf::{FramePerfAggregator, FramePerfSummary, elapsed_ns};
|
||||
|
||||
/// Per-`Ensure` budget for Servo to paint after input dispatch.
|
||||
/// 250 ms catches the common click + paint round trip within the
|
||||
/// same `Ensure` instead of waiting for the next 16 ms `Poll`.
|
||||
const LIVE_FRAME_WAIT_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
const LIVE_FRAME_WAIT_INTERVAL: Duration = Duration::from_millis(2);
|
||||
|
||||
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||
let LiveArgs { profile_data_dir, iosurface_mach_service, rendering_context_kind } = args;
|
||||
fs::create_dir_all(&profile_data_dir)?;
|
||||
@@ -64,7 +57,7 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||
// matching stop is the `stdout.flush()` inside
|
||||
// `write_outcome`.
|
||||
let frame_started_at = Instant::now();
|
||||
let mut outcome = match serde_json::from_str::<LiveRequest>(&line) {
|
||||
let outcome = match serde_json::from_str::<LiveRequest>(&line) {
|
||||
Ok(request) => handle_request(
|
||||
&mut host,
|
||||
&mut sessions,
|
||||
@@ -75,7 +68,11 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||
Err(error) => Err(LiveSidecarError::Json(error)),
|
||||
};
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
send_surface_port_if_needed(iosurface_mach_sender.as_mut(), &mut outcome);
|
||||
let outcome = {
|
||||
let mut outcome = outcome;
|
||||
send_surface_port_if_needed(iosurface_mach_sender.as_mut(), &mut outcome);
|
||||
outcome
|
||||
};
|
||||
write_outcome(&mut stdout, &mut perf, &mut pending_summary, outcome, frame_started_at)?;
|
||||
}
|
||||
|
||||
@@ -153,12 +150,11 @@ fn handle_request(
|
||||
typed_text,
|
||||
};
|
||||
if apply_input(host, session, input)? {
|
||||
// Tell poll_frame to actually wait for Servo to paint
|
||||
// a response to this input. The visible-content gate
|
||||
// is bypassed on the hardware path inside poll_frame,
|
||||
// so we return on the first `has_pending_frame=true`
|
||||
// (~3 ms in practice) rather than burning the full
|
||||
// LIVE_FRAME_WAIT_TIMEOUT.
|
||||
// The app tick calls this sidecar synchronously from
|
||||
// GPUI's update path. Mark that a fresh frame is
|
||||
// desired, then let poll_frame take one event-loop
|
||||
// step; a later 16 ms app tick will poll again if
|
||||
// Servo has not painted yet.
|
||||
session.awaiting_visible_frame = true;
|
||||
}
|
||||
let webview_id = session.webview_id.clone();
|
||||
@@ -363,33 +359,24 @@ fn poll_frame(
|
||||
session: &mut LiveSession,
|
||||
rendering_context_kind: RenderingContextKind,
|
||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||
let started_at = Instant::now();
|
||||
|
||||
loop {
|
||||
host.tick();
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
if snapshot.has_pending_frame() {
|
||||
let (outcome, has_visible_content) =
|
||||
paint_pending_frame(host, session, rendering_context_kind)?;
|
||||
if has_visible_content {
|
||||
session.awaiting_visible_frame = false;
|
||||
session.ever_visible_frame = true;
|
||||
return Ok(outcome);
|
||||
}
|
||||
if !session.awaiting_visible_frame {
|
||||
return Ok(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
if !session.awaiting_visible_frame {
|
||||
return Ok(LiveOutcome::empty());
|
||||
}
|
||||
if started_at.elapsed() >= LIVE_FRAME_WAIT_TIMEOUT {
|
||||
return Ok(LiveOutcome::empty());
|
||||
}
|
||||
|
||||
thread::sleep(LIVE_FRAME_WAIT_INTERVAL);
|
||||
host.tick();
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
if !snapshot.has_pending_frame() {
|
||||
return Ok(LiveOutcome::empty());
|
||||
}
|
||||
|
||||
let (outcome, has_visible_content) =
|
||||
paint_pending_frame(host, session, rendering_context_kind)?;
|
||||
if has_visible_content {
|
||||
session.awaiting_visible_frame = false;
|
||||
session.ever_visible_frame = true;
|
||||
return Ok(outcome);
|
||||
}
|
||||
if !session.awaiting_visible_frame {
|
||||
return Ok(outcome);
|
||||
}
|
||||
|
||||
Ok(LiveOutcome::empty())
|
||||
}
|
||||
|
||||
fn paint_pending_frame(
|
||||
@@ -418,7 +405,7 @@ fn paint_readback_frame(
|
||||
let encode_started_at = Instant::now();
|
||||
let has_visible_content = session.ever_visible_frame
|
||||
|| (frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0);
|
||||
let report = LiveFrameReport::new(&snapshot, &frame);
|
||||
let report = LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio());
|
||||
let encode_ns = elapsed_ns(encode_started_at);
|
||||
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
||||
Ok((LiveOutcome::from_frame(report, frame, timings), has_visible_content))
|
||||
@@ -429,17 +416,51 @@ fn paint_hardware_surface_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &LiveSession,
|
||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||
if !session.ever_visible_frame {
|
||||
return paint_initial_hardware_surface_frame(host, session);
|
||||
}
|
||||
|
||||
let paint_started_at = Instant::now();
|
||||
host.paint_without_readback(&session.webview_id)?;
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
let paint_ns = elapsed_ns(paint_started_at);
|
||||
let encode_started_at = Instant::now();
|
||||
let report = LiveFrameReport::new_hardware_surface(&snapshot, session.width, session.height);
|
||||
let report = LiveFrameReport::new_hardware_surface(
|
||||
&snapshot,
|
||||
session.width,
|
||||
session.height,
|
||||
session.device_pixel_ratio(),
|
||||
);
|
||||
let encode_ns = elapsed_ns(encode_started_at);
|
||||
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
||||
Ok((LiveOutcome::from_report(report, timings), true))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
fn paint_initial_hardware_surface_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &LiveSession,
|
||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||
let paint_started_at = Instant::now();
|
||||
host.paint(&session.webview_id)?;
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
let frame = host.last_rendered_frame()?;
|
||||
let paint_ns = elapsed_ns(paint_started_at);
|
||||
let encode_started_at = Instant::now();
|
||||
let report = LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio());
|
||||
let has_visible_content = frame.non_white_pixel_count() > 0
|
||||
&& frame.content_pixel_count() > 0
|
||||
&& hardware_snapshot_has_visible_document(&snapshot);
|
||||
let encode_ns = elapsed_ns(encode_started_at);
|
||||
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
||||
Ok((LiveOutcome::from_frame(report, frame, timings), has_visible_content))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
fn hardware_snapshot_has_visible_document(snapshot: &ely_servo_host::WebViewSnapshot) -> bool {
|
||||
snapshot.title().is_some() || matches!(snapshot.state(), WebViewState::Complete)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LiveSession {
|
||||
webview_id: ely_domain::WebViewId,
|
||||
@@ -468,12 +489,12 @@ struct LiveSession {
|
||||
}
|
||||
|
||||
impl LiveSession {
|
||||
fn new(webview_id: ely_domain::WebViewId, width: u32, height: u32) -> Self {
|
||||
fn new(webview_id: ely_domain::WebViewId, _width: u32, _height: u32) -> Self {
|
||||
Self {
|
||||
webview_id,
|
||||
requested_url: String::new(),
|
||||
width: width.max(1),
|
||||
height: height.max(1),
|
||||
width: 0,
|
||||
height: 0,
|
||||
page_zoom_percent: DEFAULT_ZOOM_PERCENT,
|
||||
hidpi_scale_milli: 0,
|
||||
scroll_x: 0,
|
||||
@@ -482,9 +503,32 @@ impl LiveSession {
|
||||
ever_visible_frame: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn device_pixel_ratio(&self) -> f32 {
|
||||
hidpi_scale_milli_to_f32(self.hidpi_scale_milli)
|
||||
}
|
||||
}
|
||||
|
||||
fn positive_scroll_component(current: i32, delta: i32) -> i32 {
|
||||
let value = i64::from(current) + i64::from(delta);
|
||||
value.clamp(0, i64::from(i32::MAX)) as i32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_live_session_forces_first_resize_after_hidpi() {
|
||||
let session = LiveSession::new(ely_domain::WebViewId::new(), 1280, 720);
|
||||
|
||||
assert_ne!(
|
||||
session.width, 1280,
|
||||
"first apply_layout must resize after hidpi has been pushed",
|
||||
);
|
||||
assert_ne!(
|
||||
session.height, 720,
|
||||
"first apply_layout must resize after hidpi has been pushed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ely_servo_host::SoftwareServoHost;
|
||||
#[cfg(any(test, all(feature = "hardware-render", target_os = "macos")))]
|
||||
use ely_servo_host::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||
use ely_servo_host::IOSurfaceHandle;
|
||||
use ely_servo_host::{IOSurfaceIdentity, SoftwareServoHost};
|
||||
|
||||
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
|
||||
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
||||
@@ -35,12 +35,18 @@ pub(super) fn populate_surface_fields(
|
||||
if outcome.response.frame.is_none() {
|
||||
return;
|
||||
}
|
||||
if outcome.frame.is_some() {
|
||||
return;
|
||||
}
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
{
|
||||
let Ok(Some(identity)) = host.peek_iosurface_identity(webview_id) else {
|
||||
return;
|
||||
};
|
||||
align_report_to_surface_identity(outcome, identity);
|
||||
if let Err(message) = require_report_matches_surface_identity(outcome, identity) {
|
||||
*outcome = LiveOutcome::error(message);
|
||||
return;
|
||||
}
|
||||
let handle = if surface_has_been_published(published_surface_ids, tab_id, identity) {
|
||||
None
|
||||
} else {
|
||||
@@ -105,11 +111,21 @@ fn handle_matches_identity(handle: IOSurfaceHandle, identity: IOSurfaceIdentity)
|
||||
}
|
||||
|
||||
#[cfg(any(test, all(feature = "hardware-render", target_os = "macos")))]
|
||||
fn align_report_to_surface_identity(outcome: &mut LiveOutcome, identity: IOSurfaceIdentity) {
|
||||
if let Some(frame) = outcome.response.frame.as_mut() {
|
||||
frame.width = identity.width;
|
||||
frame.height = identity.height;
|
||||
fn require_report_matches_surface_identity(
|
||||
outcome: &LiveOutcome,
|
||||
identity: IOSurfaceIdentity,
|
||||
) -> Result<(), String> {
|
||||
let Some(frame) = outcome.response.frame.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
if frame.width == identity.width && frame.height == identity.height {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"servo hardware surface size {}x{} did not match frame report {}x{}",
|
||||
identity.width, identity.height, frame.width, frame.height,
|
||||
))
|
||||
}
|
||||
|
||||
/// Serialise the response then stream the optional raw RGBA frame on
|
||||
@@ -181,7 +197,7 @@ mod tests {
|
||||
live_protocol::{LiveFrameReport, LiveOutcome, PartialFrameTimings},
|
||||
perf::FramePerfAggregator,
|
||||
};
|
||||
use super::{align_report_to_surface_identity, surface_publication_for, write_outcome};
|
||||
use super::{require_report_matches_surface_identity, surface_publication_for, write_outcome};
|
||||
|
||||
#[test]
|
||||
fn unpublished_surface_without_handle_leaves_selector_empty() {
|
||||
@@ -235,17 +251,22 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardware_report_uses_surface_identity_dimensions() -> Result<(), Box<dyn Error>> {
|
||||
let mut outcome = LiveOutcome::from_report(
|
||||
fn hardware_report_mismatch_is_reported() -> Result<(), Box<dyn Error>> {
|
||||
let outcome = LiveOutcome::from_report(
|
||||
report_with_size(2180, 1586),
|
||||
PartialFrameTimings { paint_ns: 1_000, encode_ns: 2_000 },
|
||||
);
|
||||
|
||||
align_report_to_surface_identity(&mut outcome, identity(7, 2168, 1566));
|
||||
let error = match require_report_matches_surface_identity(&outcome, identity(7, 2168, 1566))
|
||||
{
|
||||
Ok(()) => return Err("mismatched IOSurface dimensions must be reported".into()),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
let report = outcome.response.frame.ok_or("report must remain present")?;
|
||||
assert_eq!(report.width, 2168);
|
||||
assert_eq!(report.height, 1566);
|
||||
assert_eq!(
|
||||
error,
|
||||
"servo hardware surface size 2168x1566 did not match frame report 2180x1586",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -314,6 +335,9 @@ mod tests {
|
||||
state: "complete",
|
||||
width,
|
||||
height,
|
||||
device_pixel_ratio: 1.0,
|
||||
css_viewport_width: width,
|
||||
css_viewport_height: height,
|
||||
rgba_byte_count: 0,
|
||||
non_white_pixel_count: 0,
|
||||
content_pixel_count: 0,
|
||||
|
||||
@@ -176,6 +176,9 @@ pub(super) struct LiveFrameReport {
|
||||
pub state: &'static str,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub device_pixel_ratio: f32,
|
||||
pub css_viewport_width: u32,
|
||||
pub css_viewport_height: u32,
|
||||
pub rgba_byte_count: usize,
|
||||
pub non_white_pixel_count: u64,
|
||||
pub content_pixel_count: u64,
|
||||
@@ -183,13 +186,18 @@ pub(super) struct LiveFrameReport {
|
||||
}
|
||||
|
||||
impl LiveFrameReport {
|
||||
pub fn new(snapshot: &WebViewSnapshot, frame: &RenderedFrame) -> Self {
|
||||
pub fn new(snapshot: &WebViewSnapshot, frame: &RenderedFrame, device_pixel_ratio: f32) -> Self {
|
||||
let (css_viewport_width, css_viewport_height) =
|
||||
css_viewport_size(frame.width(), frame.height(), device_pixel_ratio);
|
||||
Self {
|
||||
loaded_url: snapshot.url().map(str::to_string),
|
||||
title: snapshot.title().map(str::to_string),
|
||||
state: state_label(snapshot.state()),
|
||||
width: frame.width(),
|
||||
height: frame.height(),
|
||||
device_pixel_ratio,
|
||||
css_viewport_width,
|
||||
css_viewport_height,
|
||||
rgba_byte_count: frame.rgba_bytes().len(),
|
||||
non_white_pixel_count: frame.non_white_pixel_count(),
|
||||
content_pixel_count: frame.content_pixel_count(),
|
||||
@@ -200,13 +208,23 @@ impl LiveFrameReport {
|
||||
/// Build a report for the hardware IOSurface path. Pixel metrics
|
||||
/// are unavailable because the path skips framebuffer readback.
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
pub fn new_hardware_surface(snapshot: &WebViewSnapshot, width: u32, height: u32) -> Self {
|
||||
pub fn new_hardware_surface(
|
||||
snapshot: &WebViewSnapshot,
|
||||
width: u32,
|
||||
height: u32,
|
||||
device_pixel_ratio: f32,
|
||||
) -> Self {
|
||||
let (css_viewport_width, css_viewport_height) =
|
||||
css_viewport_size(width, height, device_pixel_ratio);
|
||||
Self {
|
||||
loaded_url: snapshot.url().map(str::to_string),
|
||||
title: snapshot.title().map(str::to_string),
|
||||
state: state_label(snapshot.state()),
|
||||
width,
|
||||
height,
|
||||
device_pixel_ratio,
|
||||
css_viewport_width,
|
||||
css_viewport_height,
|
||||
rgba_byte_count: 0,
|
||||
non_white_pixel_count: 0,
|
||||
content_pixel_count: 0,
|
||||
@@ -215,6 +233,18 @@ impl LiveFrameReport {
|
||||
}
|
||||
}
|
||||
|
||||
fn css_viewport_size(width: u32, height: u32, device_pixel_ratio: f32) -> (u32, u32) {
|
||||
let dpr = if device_pixel_ratio.is_finite() && device_pixel_ratio > 0.0 {
|
||||
device_pixel_ratio
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
(
|
||||
((width as f32) / dpr).round().max(1.0) as u32,
|
||||
((height as f32) / dpr).round().max(1.0) as u32,
|
||||
)
|
||||
}
|
||||
|
||||
fn state_label(state: &WebViewState) -> &'static str {
|
||||
match state {
|
||||
WebViewState::Created => "created",
|
||||
|
||||
@@ -50,7 +50,9 @@ use euclid::Size2D;
|
||||
use gleam::gl::{self, Gl};
|
||||
use image::RgbaImage;
|
||||
use servo::{DeviceIntRect, RenderingContext};
|
||||
use surfman::chains::{PreserveBuffer, SwapChain};
|
||||
use surfman::chains::{PreserveBuffer, SwapChain, SwapChainAPI};
|
||||
#[cfg(target_os = "macos")]
|
||||
use surfman::platform::macos::cgl::surface::NativeSurface;
|
||||
use surfman::{
|
||||
Connection, Context, ContextAttributeFlags, ContextAttributes, Device, Error as SurfmanError,
|
||||
GLApi, NativeWidget, Surface, SurfaceAccess, SurfaceType,
|
||||
@@ -64,6 +66,10 @@ pub struct HardwareOffscreenContext {
|
||||
size: Cell<PhysicalSize<u32>>,
|
||||
inner: SurfmanInner,
|
||||
swap_chain: SwapChain<Device>,
|
||||
#[cfg(target_os = "macos")]
|
||||
held_presented_surface: RefCell<Option<Surface>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
last_presented_iosurface: RefCell<Option<PresentedIOSurface>>,
|
||||
}
|
||||
|
||||
impl HardwareOffscreenContext {
|
||||
@@ -86,7 +92,15 @@ impl HardwareOffscreenContext {
|
||||
inner.bind_surface(surface)?;
|
||||
inner.make_current()?;
|
||||
let swap_chain = inner.create_attached_swap_chain()?;
|
||||
Ok(Self { size: Cell::new(size), inner, swap_chain })
|
||||
Ok(Self {
|
||||
size: Cell::new(size),
|
||||
inner,
|
||||
swap_chain,
|
||||
#[cfg(target_os = "macos")]
|
||||
held_presented_surface: RefCell::new(None),
|
||||
#[cfg(target_os = "macos")]
|
||||
last_presented_iosurface: RefCell::new(None),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +108,8 @@ impl Drop for HardwareOffscreenContext {
|
||||
fn drop(&mut self) {
|
||||
let device = &mut self.inner.device.borrow_mut();
|
||||
let context = &mut self.inner.context.borrow_mut();
|
||||
#[cfg(target_os = "macos")]
|
||||
self.destroy_held_presented_surface(device, context);
|
||||
let _ = self.swap_chain.destroy(device, context);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +136,8 @@ impl RenderingContext for HardwareOffscreenContext {
|
||||
|
||||
let device = &mut self.inner.device.borrow_mut();
|
||||
let context = &mut self.inner.context.borrow_mut();
|
||||
#[cfg(target_os = "macos")]
|
||||
self.destroy_held_presented_surface(device, context);
|
||||
let size = Size2D::new(size.width as i32, size.height as i32);
|
||||
let _ = self.swap_chain.resize(device, context, size);
|
||||
}
|
||||
@@ -127,7 +145,11 @@ impl RenderingContext for HardwareOffscreenContext {
|
||||
fn present(&self) {
|
||||
let device = &mut self.inner.device.borrow_mut();
|
||||
let context = &mut self.inner.context.borrow_mut();
|
||||
#[cfg(target_os = "macos")]
|
||||
self.recycle_held_presented_surface();
|
||||
let _ = self.swap_chain.swap_buffers(device, context, PreserveBuffer::No);
|
||||
#[cfg(target_os = "macos")]
|
||||
self.capture_presented_iosurface(device);
|
||||
}
|
||||
|
||||
fn make_current(&self) -> Result<(), SurfmanError> {
|
||||
@@ -152,54 +174,64 @@ use crate::iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl HardwareOffscreenContext {
|
||||
/// Cheap, non-mutating identity probe of the currently bound
|
||||
/// surface. Reads `Device::context_surface_info` (no unbind, no
|
||||
/// mach port creation) so callers can dedup before paying the
|
||||
/// price of `current_iosurface_mach_port`.
|
||||
pub fn peek_iosurface_identity(&self) -> Result<IOSurfaceIdentity, SurfmanError> {
|
||||
let device = self.inner.device.borrow();
|
||||
let context = self.inner.context.borrow();
|
||||
let info = device.context_surface_info(&context)?.ok_or(SurfmanError::Failed)?;
|
||||
Ok(IOSurfaceIdentity {
|
||||
surface_id: info.id.0 as u64,
|
||||
width: u32::try_from(info.size.width).unwrap_or(0),
|
||||
height: u32::try_from(info.size.height).unwrap_or(0),
|
||||
})
|
||||
/// Cheap, non-mutating identity probe of the IOSurface that was
|
||||
/// just presented. Used by the sidecar to dedup mach port creation.
|
||||
pub fn peek_iosurface_identity(&self) -> Result<Option<IOSurfaceIdentity>, SurfmanError> {
|
||||
Ok(self.last_presented_iosurface.borrow().as_ref().map(|surface| surface.identity))
|
||||
}
|
||||
|
||||
/// Snapshot the IOSurface currently bound to the context and
|
||||
/// return its mach port name plus dimensions and stable surface
|
||||
/// id. Increments the IOSurface's mach-port use count; the
|
||||
/// Snapshot the just-presented IOSurface and return its mach port
|
||||
/// name plus dimensions and stable surface id. Increments the
|
||||
/// IOSurface's mach-port use count; the
|
||||
/// receiving process holds it via `IOSurfaceLookupFromMachPort` and
|
||||
/// is responsible for `mach_port_deallocate` once the import is
|
||||
/// finished.
|
||||
///
|
||||
/// Implementation note: surfman's CGL backend keeps the bound
|
||||
/// surface inside the GL context. To inspect it we temporarily
|
||||
/// `unbind_surface_from_context`, call `device.native_surface()`
|
||||
/// (which retains the `IOSurfaceRef`), then `bind_surface_to_context`
|
||||
/// again. The unbind path calls `glFlush` so the IOSurface contents
|
||||
/// are consistent for any reader importing it after this returns.
|
||||
pub fn current_iosurface_mach_port(&self) -> Result<IOSurfaceHandle, SurfmanError> {
|
||||
let device = &mut self.inner.device.borrow_mut();
|
||||
let context = &mut self.inner.context.borrow_mut();
|
||||
// `new` always binds a surface and `current_iosurface_mach_port`
|
||||
// is the only method that unbinds; the `None` branch only fires
|
||||
// if the invariant has been broken from outside.
|
||||
let surface =
|
||||
device.unbind_surface_from_context(context)?.ok_or(SurfmanError::Failed)?;
|
||||
let native = device.native_surface(&surface);
|
||||
let mach_port = native.0.create_mach_port();
|
||||
let info = device.surface_info(&surface);
|
||||
let handle = IOSurfaceHandle {
|
||||
let presented = self.last_presented_iosurface.borrow();
|
||||
let presented = presented.as_ref().ok_or(SurfmanError::Failed)?;
|
||||
let mach_port = presented.native.0.create_mach_port();
|
||||
Ok(IOSurfaceHandle {
|
||||
mach_port_name: mach_port,
|
||||
surface_id: presented.identity.surface_id,
|
||||
width: presented.identity.width,
|
||||
height: presented.identity.height,
|
||||
})
|
||||
}
|
||||
|
||||
fn capture_presented_iosurface(&self, device: &mut Device) {
|
||||
let Some(surface) = self.swap_chain.take_pending_surface() else {
|
||||
self.last_presented_iosurface.borrow_mut().take();
|
||||
return;
|
||||
};
|
||||
let info = device.surface_info(&surface);
|
||||
let native = device.native_surface(&surface);
|
||||
let identity = IOSurfaceIdentity {
|
||||
surface_id: info.id.0 as u64,
|
||||
width: u32::try_from(info.size.width).unwrap_or(0),
|
||||
height: u32::try_from(info.size.height).unwrap_or(0),
|
||||
};
|
||||
device.bind_surface_to_context(context, surface).map_err(|(error, _)| error)?;
|
||||
Ok(handle)
|
||||
self.held_presented_surface.replace(Some(surface));
|
||||
self.last_presented_iosurface.replace(Some(PresentedIOSurface { identity, native }));
|
||||
}
|
||||
|
||||
fn recycle_held_presented_surface(&self) {
|
||||
if let Some(surface) = self.held_presented_surface.borrow_mut().take() {
|
||||
self.swap_chain.recycle_surface(surface);
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy_held_presented_surface(&self, device: &mut Device, context: &mut Context) {
|
||||
self.last_presented_iosurface.borrow_mut().take();
|
||||
if let Some(mut surface) = self.held_presented_surface.borrow_mut().take() {
|
||||
let _ = device.destroy_surface(context, &mut surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct PresentedIOSurface {
|
||||
identity: IOSurfaceIdentity,
|
||||
native: NativeSurface,
|
||||
}
|
||||
|
||||
/// Trimmed mirror of `paint_api::rendering_context::SurfmanRenderingContext`.
|
||||
@@ -281,12 +313,10 @@ impl SurfmanInner {
|
||||
fn bind_surface(&self, surface: Surface) -> Result<(), SurfmanError> {
|
||||
let device = &self.device.borrow();
|
||||
let context = &mut self.context.borrow_mut();
|
||||
device
|
||||
.bind_surface_to_context(context, surface)
|
||||
.map_err(|(err, mut surface)| {
|
||||
let _ = device.destroy_surface(context, &mut surface);
|
||||
err
|
||||
})?;
|
||||
device.bind_surface_to_context(context, surface).map_err(|(err, mut surface)| {
|
||||
let _ = device.destroy_surface(context, &mut surface);
|
||||
err
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ mod runtime_webview;
|
||||
pub use error::ServoHostError;
|
||||
#[cfg(feature = "hardware-render")]
|
||||
pub use hardware_rendering_context::HardwareOffscreenContext;
|
||||
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||
pub use host::{
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest,
|
||||
MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest,
|
||||
RenderedFrame, RenderedFrameSummary, ResizeRequest, ScreenshotRequest, ScrollRequest,
|
||||
ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
|
||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
|
||||
RenderedFrameSummary, ResizeRequest, ScreenshotRequest, ScrollRequest, ServoHost,
|
||||
TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||
};
|
||||
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||
#[cfg(feature = "servo-engine")]
|
||||
pub use runtime::{RenderingContextKind, ServoSurfaceSize, SoftwareServoHost};
|
||||
|
||||
@@ -108,8 +108,8 @@ impl SoftwareServoHost {
|
||||
|
||||
/// Paint and present the webview's current surface while leaving
|
||||
/// framebuffer readback to callers that explicitly need RGBA
|
||||
/// bytes. The live hardware path uses this before publishing the
|
||||
/// IOSurface handle to the renderer process.
|
||||
/// bytes. The live hardware path exports the just-presented
|
||||
/// IOSurface from the rendering context.
|
||||
pub fn paint_without_readback(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> {
|
||||
self.paint_webview(webview_id, false).map(|_| ())
|
||||
}
|
||||
@@ -204,9 +204,13 @@ impl ServoHost for SoftwareServoHost {
|
||||
|
||||
webview.delegate.set_state(WebViewState::Loading);
|
||||
if should_create_initial_document {
|
||||
let hidpi_scale_factor = webview.webview.hidpi_scale_factor();
|
||||
webview.webview = WebViewBuilder::new(&servo, webview.rendering_context.clone())
|
||||
.delegate(webview.delegate.clone())
|
||||
.url(url)
|
||||
// The live path pushes DPR before first navigation. Preserve that scale when
|
||||
// replacing the about:blank WebView so CSS viewport = physical surface / DPR.
|
||||
.hidpi_scale_factor(hidpi_scale_factor)
|
||||
.build();
|
||||
// Cosmetic: makes the freshly built WebView paint its
|
||||
// first frame. The input-accepting invariant lives in
|
||||
@@ -444,10 +448,7 @@ impl SoftwareServoHost {
|
||||
let Some(hardware) = webview.hardware_context.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
hardware
|
||||
.peek_iosurface_identity()
|
||||
.map(Some)
|
||||
.map_err(|_| ServoHostError::RenderingContextUnavailable)
|
||||
hardware.peek_iosurface_identity().map_err(|_| ServoHostError::RenderingContextUnavailable)
|
||||
}
|
||||
|
||||
/// Mint a fresh mach port for the IOSurface bound to this
|
||||
|
||||
@@ -82,6 +82,12 @@ struct LiveFrameReport {
|
||||
width: u32,
|
||||
#[serde(default)]
|
||||
height: u32,
|
||||
#[serde(default)]
|
||||
device_pixel_ratio: f32,
|
||||
#[serde(default)]
|
||||
css_viewport_width: u32,
|
||||
#[serde(default)]
|
||||
css_viewport_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -127,16 +133,16 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
||||
let stdout = child.stdout.take().ok_or("sidecar stdout missing")?;
|
||||
let mut reader = BufReader::new(stdout);
|
||||
|
||||
let outcome =
|
||||
match drive_bench(&mut stdin, &mut reader, &kind, &tab, &profile_id, &url, frames) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(error) => {
|
||||
drop(stdin);
|
||||
let _ = child.kill();
|
||||
cleanup(&profile_data_dir)?;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let outcome = match drive_bench(&mut stdin, &mut reader, &kind, &tab, &profile_id, &url, frames)
|
||||
{
|
||||
Ok(outcome) => outcome,
|
||||
Err(error) => {
|
||||
drop(stdin);
|
||||
let _ = child.kill();
|
||||
cleanup(&profile_data_dir)?;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
drop(stdin);
|
||||
let _ = child.wait();
|
||||
@@ -146,8 +152,8 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
||||
print_surface_handles(&kind, &outcome.surface_handles);
|
||||
print_current_surface_summary(&kind, &outcome.current_surface_ids);
|
||||
eprintln!(
|
||||
"\n=== ELY_PERF_KIND={kind} rgba_bytes_received={} ===",
|
||||
outcome.rgba_bytes_received
|
||||
"\n=== ELY_PERF_KIND={kind} bootstrap_rgba_bytes={} steady_state_rgba_bytes={} ===",
|
||||
outcome.bootstrap_rgba_bytes, outcome.steady_state_rgba_bytes,
|
||||
);
|
||||
assert!(
|
||||
!outcome.summaries.is_empty(),
|
||||
@@ -174,17 +180,10 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
||||
!outcome.current_surface_ids.is_empty(),
|
||||
"hardware path must report current_surface_id on every frame"
|
||||
);
|
||||
// T10.6: once the receiver samples the IOSurface directly,
|
||||
// the sidecar drops the RGBA payload. The initial navigate
|
||||
// response may still carry bytes (no current_surface_id yet
|
||||
// because surfman hasn't bound the painted surface), but the
|
||||
// steady-state per-frame cost must be zero.
|
||||
assert!(
|
||||
outcome.rgba_bytes_received < (frames as u64) * 1_024,
|
||||
"hardware path leaked {} RGBA bytes across {} frames \
|
||||
(expected ~0 — the wire-drop optimisation regressed)",
|
||||
outcome.rgba_bytes_received,
|
||||
frames + 1,
|
||||
assert_eq!(
|
||||
outcome.steady_state_rgba_bytes, 0,
|
||||
"hardware path leaked {} RGBA bytes while scrolling",
|
||||
outcome.steady_state_rgba_bytes,
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
@@ -198,10 +197,11 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
||||
// Software path keeps streaming pixels — every frame must
|
||||
// carry a full RGBA payload.
|
||||
let viewport_bytes = (1024u64) * (768u64) * 4;
|
||||
let total_rgba_bytes = outcome.bootstrap_rgba_bytes + outcome.steady_state_rgba_bytes;
|
||||
assert!(
|
||||
outcome.rgba_bytes_received >= viewport_bytes,
|
||||
total_rgba_bytes >= viewport_bytes,
|
||||
"software path delivered only {} bytes — expected at least one full frame ({})",
|
||||
outcome.rgba_bytes_received,
|
||||
total_rgba_bytes,
|
||||
viewport_bytes,
|
||||
);
|
||||
}
|
||||
@@ -212,7 +212,8 @@ struct BenchOutcome {
|
||||
summaries: Vec<FramePerfSummary>,
|
||||
surface_handles: Vec<BenchSurfaceHandle>,
|
||||
current_surface_ids: Vec<u64>,
|
||||
rgba_bytes_received: u64,
|
||||
bootstrap_rgba_bytes: u64,
|
||||
steady_state_rgba_bytes: u64,
|
||||
}
|
||||
|
||||
fn spawn_sidecar(kind: &str, profile_data_dir: &PathBuf) -> Result<Child, Box<dyn Error>> {
|
||||
@@ -241,23 +242,22 @@ fn drive_bench(
|
||||
let mut summaries = Vec::new();
|
||||
let mut surface_handles = Vec::new();
|
||||
let mut current_surface_ids = Vec::new();
|
||||
let mut rgba_bytes_received: u64 = 0;
|
||||
let mut bootstrap_rgba_bytes: u64 = 0;
|
||||
let mut steady_state_rgba_bytes: u64 = 0;
|
||||
|
||||
let navigate = build_ensure(tab, profile_id, url, 0, 0, false);
|
||||
write_request(stdin, &navigate)?;
|
||||
let response = read_response(reader, RESPONSE_TIMEOUT)?;
|
||||
assert_frame_viewport_report(&response);
|
||||
record_summary(&response, kind, &mut summaries);
|
||||
record_surface_handle(&response, kind, &mut surface_handles);
|
||||
record_current_surface_id(&response, &mut current_surface_ids);
|
||||
rgba_bytes_received += response.frame.as_ref().map_or(0, |f| f.rgba_byte_count as u64);
|
||||
record_rgba_bytes(&response, &mut bootstrap_rgba_bytes, &mut steady_state_rgba_bytes);
|
||||
|
||||
let mut accumulated_scroll = 0;
|
||||
for frame_index in 0..frames {
|
||||
let scroll_delta_y = if frame_index % 80 == 79 {
|
||||
-SCROLL_STEP_PX * 60
|
||||
} else {
|
||||
SCROLL_STEP_PX
|
||||
};
|
||||
let scroll_delta_y =
|
||||
if frame_index % 80 == 79 { -SCROLL_STEP_PX * 60 } else { SCROLL_STEP_PX };
|
||||
accumulated_scroll += scroll_delta_y;
|
||||
let request = build_ensure(tab, profile_id, url, 0, scroll_delta_y, true);
|
||||
write_request(stdin, &request)?;
|
||||
@@ -265,14 +265,56 @@ fn drive_bench(
|
||||
if let Some(error) = response.error.as_ref() {
|
||||
return Err(format!("sidecar error at frame {frame_index}: {error}").into());
|
||||
}
|
||||
assert_frame_viewport_report(&response);
|
||||
record_summary(&response, kind, &mut summaries);
|
||||
record_surface_handle(&response, kind, &mut surface_handles);
|
||||
record_current_surface_id(&response, &mut current_surface_ids);
|
||||
rgba_bytes_received += response.frame.as_ref().map_or(0, |f| f.rgba_byte_count as u64);
|
||||
record_rgba_bytes(&response, &mut bootstrap_rgba_bytes, &mut steady_state_rgba_bytes);
|
||||
}
|
||||
let _ = accumulated_scroll;
|
||||
|
||||
Ok(BenchOutcome { summaries, surface_handles, current_surface_ids, rgba_bytes_received })
|
||||
Ok(BenchOutcome {
|
||||
summaries,
|
||||
surface_handles,
|
||||
current_surface_ids,
|
||||
bootstrap_rgba_bytes,
|
||||
steady_state_rgba_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn record_rgba_bytes(
|
||||
response: &LiveResponse,
|
||||
bootstrap_rgba_bytes: &mut u64,
|
||||
steady_state_rgba_bytes: &mut u64,
|
||||
) {
|
||||
let rgba_byte_count = response.frame.as_ref().map_or(0, |frame| frame.rgba_byte_count as u64);
|
||||
if response.current_surface_id.is_some() {
|
||||
*steady_state_rgba_bytes += rgba_byte_count;
|
||||
} else {
|
||||
*bootstrap_rgba_bytes += rgba_byte_count;
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_frame_viewport_report(response: &LiveResponse) {
|
||||
let Some(frame) = response.frame.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let dpr = if frame.device_pixel_ratio.is_finite() && frame.device_pixel_ratio > 0.0 {
|
||||
frame.device_pixel_ratio
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let expected_width = ((frame.width as f32) / dpr).round().max(1.0) as u32;
|
||||
let expected_height = ((frame.height as f32) / dpr).round().max(1.0) as u32;
|
||||
|
||||
assert_eq!(
|
||||
frame.css_viewport_width, expected_width,
|
||||
"CSS viewport width must match physical width divided by DPR",
|
||||
);
|
||||
assert_eq!(
|
||||
frame.css_viewport_height, expected_height,
|
||||
"CSS viewport height must match physical height divided by DPR",
|
||||
);
|
||||
}
|
||||
|
||||
fn record_surface_handle(
|
||||
@@ -314,9 +356,7 @@ fn print_current_surface_summary(kind: &str, current_surface_ids: &[u64]) {
|
||||
for id in current_surface_ids {
|
||||
*counts.entry(*id).or_default() += 1;
|
||||
}
|
||||
eprintln!(
|
||||
"\n=== ELY_PERF_KIND={kind} current_surface_id histogram (per-frame selector) ===",
|
||||
);
|
||||
eprintln!("\n=== ELY_PERF_KIND={kind} current_surface_id histogram (per-frame selector) ===",);
|
||||
for (surface_id, count) in counts.iter() {
|
||||
eprintln!("surface_id=0x{:x} frames={}", surface_id, count);
|
||||
}
|
||||
@@ -332,11 +372,7 @@ fn build_ensure(
|
||||
) -> String {
|
||||
let hover_x = if include_hover { Some(256u32) } else { None };
|
||||
let hover_y = if include_hover { Some(256u32) } else { None };
|
||||
let scroll_point = if scroll_dx != 0 || scroll_dy != 0 {
|
||||
Some((256u32, 256u32))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let scroll_point = if scroll_dx != 0 || scroll_dy != 0 { Some((256u32, 256u32)) } else { None };
|
||||
let hover_x_json = match hover_x {
|
||||
Some(value) => format!("{value}"),
|
||||
None => "null".to_string(),
|
||||
@@ -417,17 +453,22 @@ fn read_response_with_bytes(
|
||||
|
||||
fn record_summary(response: &LiveResponse, kind: &str, summaries: &mut Vec<FramePerfSummary>) {
|
||||
if let Some(perf) = response.perf.as_ref() {
|
||||
assert_eq!(
|
||||
perf.context, kind,
|
||||
"sidecar context label must match requested kind"
|
||||
);
|
||||
assert_eq!(perf.context, kind, "sidecar context label must match requested kind");
|
||||
eprintln!(
|
||||
"[perf {kind}] window={} paint p50/p95/p99={}/{}/{} encode {}/{}/{} write {}/{}/{} total {}/{}/{} (µs)",
|
||||
perf.window,
|
||||
perf.paint_p50_us, perf.paint_p95_us, perf.paint_p99_us,
|
||||
perf.encode_p50_us, perf.encode_p95_us, perf.encode_p99_us,
|
||||
perf.write_p50_us, perf.write_p95_us, perf.write_p99_us,
|
||||
perf.total_p50_us, perf.total_p95_us, perf.total_p99_us,
|
||||
perf.paint_p50_us,
|
||||
perf.paint_p95_us,
|
||||
perf.paint_p99_us,
|
||||
perf.encode_p50_us,
|
||||
perf.encode_p95_us,
|
||||
perf.encode_p99_us,
|
||||
perf.write_p50_us,
|
||||
perf.write_p95_us,
|
||||
perf.write_p99_us,
|
||||
perf.total_p50_us,
|
||||
perf.total_p95_us,
|
||||
perf.total_p99_us,
|
||||
);
|
||||
summaries.push(perf.clone());
|
||||
}
|
||||
@@ -438,19 +479,35 @@ fn print_summaries(kind: &str, frames: u32, summaries: &[FramePerfSummary]) {
|
||||
eprintln!(
|
||||
"{:<8} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
|
||||
"win",
|
||||
"paint50", "paint95", "paint99",
|
||||
"enc50", "enc95", "enc99",
|
||||
"wr50", "wr95", "wr99",
|
||||
"tot50", "tot95", "tot99",
|
||||
"paint50",
|
||||
"paint95",
|
||||
"paint99",
|
||||
"enc50",
|
||||
"enc95",
|
||||
"enc99",
|
||||
"wr50",
|
||||
"wr95",
|
||||
"wr99",
|
||||
"tot50",
|
||||
"tot95",
|
||||
"tot99",
|
||||
);
|
||||
for summary in summaries {
|
||||
eprintln!(
|
||||
"{:<8} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
|
||||
summary.window,
|
||||
summary.paint_p50_us, summary.paint_p95_us, summary.paint_p99_us,
|
||||
summary.encode_p50_us, summary.encode_p95_us, summary.encode_p99_us,
|
||||
summary.write_p50_us, summary.write_p95_us, summary.write_p99_us,
|
||||
summary.total_p50_us, summary.total_p95_us, summary.total_p99_us,
|
||||
summary.paint_p50_us,
|
||||
summary.paint_p95_us,
|
||||
summary.paint_p99_us,
|
||||
summary.encode_p50_us,
|
||||
summary.encode_p95_us,
|
||||
summary.encode_p99_us,
|
||||
summary.write_p50_us,
|
||||
summary.write_p95_us,
|
||||
summary.write_p99_us,
|
||||
summary.total_p50_us,
|
||||
summary.total_p95_us,
|
||||
summary.total_p99_us,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -586,11 +643,7 @@ fn drive_solid_color_render(
|
||||
let report = report.ok_or("never received a frame with bytes")?;
|
||||
let width = report.width as usize;
|
||||
let height = report.height as usize;
|
||||
assert_eq!(
|
||||
bytes.len(),
|
||||
width * height * 4,
|
||||
"rgba byte count must match width × height × 4",
|
||||
);
|
||||
assert_eq!(bytes.len(), width * height * 4, "rgba byte count must match width × height × 4",);
|
||||
|
||||
// Sample 9 evenly-spaced points in the inner quartile of the
|
||||
// viewport. Solid backgrounds should pass every sample; if Servo
|
||||
|
||||
@@ -10,9 +10,10 @@ use std::{
|
||||
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText};
|
||||
use ely_servo_host::{
|
||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PageZoomRequest,
|
||||
PermissionDecision, PermissionRequest, ResizeRequest, ScreenshotRequest, ScrollRequest,
|
||||
ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, WebViewState,
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
||||
PageZoomRequest, PermissionDecision, PermissionRequest, ResizeRequest, ScreenshotRequest,
|
||||
ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest,
|
||||
WebViewState,
|
||||
};
|
||||
|
||||
const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
|
||||
@@ -25,6 +26,7 @@ const PRD_SITE_COMPATIBILITY_CASES: &[PrdSiteCompatibilityCase] = &[
|
||||
PrdSiteCompatibilityCase { url: "https://servo.org/", title_fragment: "Servo" },
|
||||
];
|
||||
const SOFTWARE_HOST_CHILD_ENV: &str = "ELY_SERVO_SOFTWARE_HOST_CHILD";
|
||||
const DPR_VIEWPORT_CHILD_ENV: &str = "ELY_SERVO_DPR_VIEWPORT_CHILD";
|
||||
const CLICK_PROBE_URL: &str = "data:text/html,%3C!doctype%20html%3E%3Ctitle%3EClick%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f7f7f7%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3B%7D%3C%2Fstyle%3E%3Cbutton%20onclick%3D%22document.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Clicked%27%3Bthis.textContent%3D%27Clicked%27%3B%22%3ETap%3C%2Fbutton%3E";
|
||||
const DRAG_PROBE_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3EDrag%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f7f7f7%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3B%7D%3C%2Fstyle%3E%3Cbutton%20id%3Dbox%3EDrag%3C%2Fbutton%3E%3Cscript%3Elet%20dragging%3Dfalse%3Bconst%20box%3Ddocument.getElementById%28%27box%27%29%3BaddEventListener%28%27mousedown%27%2Cevent%3D%3E%7Bif%28event.target%3D%3D%3Dbox%29%7Bdragging%3Dtrue%3B%7D%7D%29%3BaddEventListener%28%27mousemove%27%2Cevent%3D%3E%7Bif%28dragging%26%26event.clientX%3E280%29%7Bdocument.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Dragged%27%3Bbox.textContent%3D%27Dragged%27%3B%7D%7D%29%3BaddEventListener%28%27mouseup%27%2C%28%29%3D%3E%7Bdragging%3Dfalse%3B%7D%29%3B%3C%2Fscript%3E";
|
||||
const TOUCH_PROBE_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3ETouch%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f7f7f7%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3Btouch-action%3Amanipulation%3B%7D%3C%2Fstyle%3E%3Cbutton%20ontouchstart%3D%22document.body.dataset.touch%3D%27start%27%3B%22%20onclick%3D%22document.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Touched%27%3Bthis.textContent%3D%27Touched%27%3B%22%3ETap%3C%2Fbutton%3E";
|
||||
@@ -36,6 +38,13 @@ struct PrdSiteCompatibilityCase {
|
||||
title_fragment: &'static str,
|
||||
}
|
||||
|
||||
struct DprViewportCase {
|
||||
physical_width: u32,
|
||||
physical_height: u32,
|
||||
dpr: f32,
|
||||
expected_css_width: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manages_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
||||
if env::var_os(SOFTWARE_HOST_CHILD_ENV).is_none() {
|
||||
@@ -67,6 +76,122 @@ fn run_isolated_software_host_lifecycle() -> Result<(), Box<dyn Error>> {
|
||||
.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_navigation_preserves_dpr_for_css_viewport() -> Result<(), Box<dyn Error>> {
|
||||
if env::var_os(DPR_VIEWPORT_CHILD_ENV).is_none() {
|
||||
return run_isolated_dpr_viewport_test();
|
||||
}
|
||||
|
||||
exercise_dpr_viewport_cases()
|
||||
}
|
||||
|
||||
fn run_isolated_dpr_viewport_test() -> Result<(), Box<dyn Error>> {
|
||||
let output = Command::new(env::current_exe()?)
|
||||
.arg("--exact")
|
||||
.arg("first_navigation_preserves_dpr_for_css_viewport")
|
||||
.env(DPR_VIEWPORT_CHILD_ENV, "1")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"isolated DPR viewport test failed\nstatus: {}\nstdout: {}\nstderr: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
fn exercise_dpr_viewport_cases() -> Result<(), Box<dyn Error>> {
|
||||
let mut host = SoftwareServoHost::new(ServoSurfaceSize::new(1, 1))?;
|
||||
let profile_id = ProfileId::new();
|
||||
let cases = [
|
||||
DprViewportCase {
|
||||
physical_width: 800,
|
||||
physical_height: 600,
|
||||
dpr: 1.0,
|
||||
expected_css_width: 800,
|
||||
},
|
||||
DprViewportCase {
|
||||
physical_width: 960,
|
||||
physical_height: 640,
|
||||
dpr: 2.0,
|
||||
expected_css_width: 480,
|
||||
},
|
||||
DprViewportCase {
|
||||
physical_width: 1250,
|
||||
physical_height: 800,
|
||||
dpr: 1.25,
|
||||
expected_css_width: 1000,
|
||||
},
|
||||
DprViewportCase {
|
||||
physical_width: 1440,
|
||||
physical_height: 900,
|
||||
dpr: 1.5,
|
||||
expected_css_width: 960,
|
||||
},
|
||||
DprViewportCase {
|
||||
physical_width: 1750,
|
||||
physical_height: 1000,
|
||||
dpr: 1.75,
|
||||
expected_css_width: 1000,
|
||||
},
|
||||
DprViewportCase {
|
||||
physical_width: 1500,
|
||||
physical_height: 900,
|
||||
dpr: 2.5,
|
||||
expected_css_width: 600,
|
||||
},
|
||||
DprViewportCase {
|
||||
physical_width: 2160,
|
||||
physical_height: 1440,
|
||||
dpr: 3.0,
|
||||
expected_css_width: 720,
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let tab_id = TabId::new();
|
||||
let webview_id = host.create_webview_with_size(
|
||||
tab_id.clone(),
|
||||
profile_id.clone(),
|
||||
ServoSurfaceSize::new(case.physical_width, case.physical_height),
|
||||
)?;
|
||||
|
||||
host.set_hidpi_scale(HidpiScaleRequest {
|
||||
webview_id: webview_id.clone(),
|
||||
scale_factor: case.dpr,
|
||||
})?;
|
||||
host.navigate(NavigationRequest {
|
||||
webview_id: webview_id.clone(),
|
||||
tab_id,
|
||||
url: UrlText::parse(viewport_probe_url(case.expected_css_width + 1))?,
|
||||
})?;
|
||||
|
||||
let snapshot = wait_for_rendered_webview(&mut host, &webview_id, None)?;
|
||||
assert_eq!(snapshot.state(), &WebViewState::Complete, "snapshot: {snapshot:?}");
|
||||
|
||||
let frame = host.last_rendered_frame()?;
|
||||
assert_eq!(frame.width(), case.physical_width, "DPR case frame width");
|
||||
assert_eq!(frame.height(), case.physical_height, "DPR case frame height");
|
||||
assert_eq!(
|
||||
center_pixel_rgb(&frame),
|
||||
[238, 32, 77],
|
||||
"CSS viewport must equal physical width divided by DPR: physical={} dpr={} expected_css={}",
|
||||
case.physical_width,
|
||||
case.dpr,
|
||||
case.expected_css_width,
|
||||
);
|
||||
host.close_webview(&webview_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exercise_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
||||
let mut host = SoftwareServoHost::new(ServoSurfaceSize::new(INITIAL_WIDTH, INITIAL_HEIGHT))?;
|
||||
let tab_id = TabId::new();
|
||||
@@ -340,3 +465,33 @@ fn assert_frame_has_dimensions_and_content(
|
||||
assert!(frame.content_pixel_count() >= minimum_content_pixels, "{label}: {frame:?}");
|
||||
assert_ne!(frame.sample_hash(), 0, "{label}: {frame:?}");
|
||||
}
|
||||
|
||||
fn center_pixel_rgb(frame: &ely_servo_host::RenderedFrame) -> [u8; 3] {
|
||||
let x = frame.width() / 2;
|
||||
let y = frame.height() / 2;
|
||||
let index = ((y * frame.width() + x) * 4) as usize;
|
||||
let rgba = &frame.rgba_bytes()[index..index + 4];
|
||||
[rgba[0], rgba[1], rgba[2]]
|
||||
}
|
||||
|
||||
fn viewport_probe_url(min_width_threshold: u32) -> String {
|
||||
let html = format!(
|
||||
"<!doctype html><title>DPR Probe</title><style>\
|
||||
html,body{{margin:0;width:100%;height:100%;background:rgb(238,32,77);}}\
|
||||
@media (min-width:{min_width_threshold}px){{html,body{{background:rgb(0,57,255);}}}}\
|
||||
</style>",
|
||||
);
|
||||
format!("data:text/html,{}", percent_encode_for_data_url(&html))
|
||||
}
|
||||
|
||||
fn percent_encode_for_data_url(value: &str) -> String {
|
||||
value
|
||||
.bytes()
|
||||
.map(|byte| match byte {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
(byte as char).to_string()
|
||||
}
|
||||
_ => format!("%{byte:02X}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user