fix(web-surface): bound live input paints

This commit is contained in:
2026-05-16 01:02:38 -04:00
parent 8841d74ac5
commit 234bf4c8a5
2 changed files with 75 additions and 69 deletions
@@ -125,8 +125,8 @@ fn assert_web_surface_scrolls_prd_site() -> Result<(), Box<dyn Error>> {
let profile_id = ProfileId::new(); let profile_id = ProfileId::new();
let case = PRD_TOP_SITE_CASES let case = PRD_TOP_SITE_CASES
.iter() .iter()
.find(|case| case.url == "https://servo.org/") .find(|case| case.url == "https://github.com")
.ok_or("missing servo.org live-site case")?; .ok_or("missing github.com live-site case")?;
let tab = web_tab(profile_id, case.url)?; let tab = web_tab(profile_id, case.url)?;
assert_eq!( assert_eq!(
@@ -299,19 +299,14 @@ fn wait_for_ready_frame(
let mut last_error = None; let mut last_error = None;
loop { loop {
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(last_error.unwrap_or_else(|| format!("timed out rendering {}", case.url)));
}
store.tick(std::slice::from_ref(tab_id)); store.tick(std::slice::from_ref(tab_id));
match store.state(tab_id) { match store.state(tab_id) {
Some(WebSurfaceState::Ready(frame)) => { Some(WebSurfaceState::Ready(frame)) => {
if let Err(error) = validate_prd_frame(frame, case, 0) { if let Err(error) = validate_prd_frame(frame, case, 0) {
last_error = Some(error); last_error = Some(error);
thread::sleep(LIVE_SITE_WAIT_INTERVAL); } else {
continue; return Ok(frame.clone());
} }
return Ok(frame.clone());
} }
Some(WebSurfaceState::Failed { message, .. }) => { Some(WebSurfaceState::Failed { message, .. }) => {
return Err(format!("{} failed: {message}", case.url)); return Err(format!("{} failed: {message}", case.url));
@@ -319,6 +314,10 @@ fn wait_for_ready_frame(
Some(WebSurfaceState::Loading { .. }) | None => {} Some(WebSurfaceState::Loading { .. }) | None => {}
} }
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(last_error.unwrap_or_else(|| format!("timed out rendering {}", case.url)));
}
thread::sleep(LIVE_SITE_WAIT_INTERVAL); thread::sleep(LIVE_SITE_WAIT_INTERVAL);
} }
} }
@@ -334,12 +333,6 @@ fn wait_for_ready_frame_at_scroll(
let mut last_error = None; let mut last_error = None;
loop { loop {
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(last_error.unwrap_or_else(|| {
format!("timed out rendering {} at scroll y={expected_scroll_y}", case.url)
}));
}
store.tick(std::slice::from_ref(tab_id)); store.tick(std::slice::from_ref(tab_id));
match store.state(tab_id) { match store.state(tab_id) {
Some(WebSurfaceState::Ready(frame)) Some(WebSurfaceState::Ready(frame))
@@ -347,10 +340,7 @@ fn wait_for_ready_frame_at_scroll(
{ {
if let Err(error) = validate_prd_frame(frame, case, expected_scroll_y) { if let Err(error) = validate_prd_frame(frame, case, expected_scroll_y) {
last_error = Some(error); last_error = Some(error);
thread::sleep(LIVE_SITE_WAIT_INTERVAL); } else if !frame.has_hardware_surface()
continue;
}
if !frame.has_hardware_surface()
&& let Some(previous_sample_hash) = previous_sample_hash && let Some(previous_sample_hash) = previous_sample_hash
&& frame.sample_hash() == previous_sample_hash && frame.sample_hash() == previous_sample_hash
{ {
@@ -358,18 +348,29 @@ fn wait_for_ready_frame_at_scroll(
"{} scroll y={expected_scroll_y} sample hash unchanged", "{} scroll y={expected_scroll_y} sample hash unchanged",
case.url case.url
)); ));
thread::sleep(LIVE_SITE_WAIT_INTERVAL); } else {
continue; return Ok(frame.clone());
} }
return Ok(frame.clone());
} }
Some(WebSurfaceState::Ready(_)) => {} Some(WebSurfaceState::Ready(frame)) => {
last_error = Some(format!(
"{} expected scroll y={expected_scroll_y}, observed {}",
case.url,
frame.detail_label()
));
}
Some(WebSurfaceState::Failed { message, .. }) => { Some(WebSurfaceState::Failed { message, .. }) => {
return Err(format!("{} failed: {message}", case.url)); return Err(format!("{} failed: {message}", case.url));
} }
Some(WebSurfaceState::Loading { .. }) | None => {} Some(WebSurfaceState::Loading { .. }) | None => {}
} }
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(last_error.unwrap_or_else(|| {
format!("timed out rendering {} at scroll y={expected_scroll_y}", case.url)
}));
}
thread::sleep(LIVE_SITE_WAIT_INTERVAL); thread::sleep(LIVE_SITE_WAIT_INTERVAL);
} }
} }
@@ -385,12 +386,6 @@ fn wait_for_ready_frame_at_size(
let mut last_error = None; let mut last_error = None;
loop { loop {
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(last_error.unwrap_or_else(|| {
format!("timed out rendering {} at {expected_width}x{expected_height}", case.url)
}));
}
store.tick(std::slice::from_ref(tab_id)); store.tick(std::slice::from_ref(tab_id));
match store.state(tab_id) { match store.state(tab_id) {
Some(WebSurfaceState::Ready(frame)) Some(WebSurfaceState::Ready(frame))
@@ -401,10 +396,9 @@ fn wait_for_ready_frame_at_size(
validate_prd_frame_at_size(frame, case, expected_width, expected_height) validate_prd_frame_at_size(frame, case, expected_width, expected_height)
{ {
last_error = Some(error); last_error = Some(error);
thread::sleep(LIVE_SITE_WAIT_INTERVAL); } else {
continue; return Ok(frame.clone());
} }
return Ok(frame.clone());
} }
Some(WebSurfaceState::Ready(_)) => {} Some(WebSurfaceState::Ready(_)) => {}
Some(WebSurfaceState::Failed { message, .. }) => { Some(WebSurfaceState::Failed { message, .. }) => {
@@ -413,6 +407,12 @@ fn wait_for_ready_frame_at_size(
Some(WebSurfaceState::Loading { .. }) | None => {} Some(WebSurfaceState::Loading { .. }) | None => {}
} }
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(last_error.unwrap_or_else(|| {
format!("timed out rendering {} at {expected_width}x{expected_height}", case.url)
}));
}
thread::sleep(LIVE_SITE_WAIT_INTERVAL); thread::sleep(LIVE_SITE_WAIT_INTERVAL);
} }
} }
@@ -427,6 +427,25 @@ fn wait_for_ready_frame_at_css_size(
let mut last_error = None; let mut last_error = None;
loop { loop {
store.tick(std::slice::from_ref(tab_id));
match store.state(tab_id) {
Some(WebSurfaceState::Ready(frame))
if frame.size().width == expected.physical_width
&& frame.size().height == expected.physical_height =>
{
if let Err(error) = validate_prd_frame_at_css_size(frame, case, expected) {
last_error = Some(error);
} else {
return Ok(frame.clone());
}
}
Some(WebSurfaceState::Ready(_)) => {}
Some(WebSurfaceState::Failed { message, .. }) => {
return Err(format!("{} failed: {message}", case.url));
}
Some(WebSurfaceState::Loading { .. }) | None => {}
}
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT { if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(last_error.unwrap_or_else(|| { return Err(last_error.unwrap_or_else(|| {
format!( format!(
@@ -440,26 +459,6 @@ fn wait_for_ready_frame_at_css_size(
})); }));
} }
store.tick(std::slice::from_ref(tab_id));
match store.state(tab_id) {
Some(WebSurfaceState::Ready(frame))
if frame.size().width == expected.physical_width
&& frame.size().height == expected.physical_height =>
{
if let Err(error) = validate_prd_frame_at_css_size(frame, case, expected) {
last_error = Some(error);
thread::sleep(LIVE_SITE_WAIT_INTERVAL);
continue;
}
return Ok(frame.clone());
}
Some(WebSurfaceState::Ready(_)) => {}
Some(WebSurfaceState::Failed { message, .. }) => {
return Err(format!("{} failed: {message}", case.url));
}
Some(WebSurfaceState::Loading { .. }) | None => {}
}
thread::sleep(LIVE_SITE_WAIT_INTERVAL); thread::sleep(LIVE_SITE_WAIT_INTERVAL);
} }
} }
@@ -6,11 +6,13 @@ use std::{
}; };
use ely_domain::{DEFAULT_ZOOM_PERCENT, ProfileId, TabId, UrlText}; use ely_domain::{DEFAULT_ZOOM_PERCENT, ProfileId, TabId, UrlText};
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
use ely_servo_host::WebViewState;
use ely_servo_host::{ use ely_servo_host::{
IOSurfaceIdentity, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest, IOSurfaceIdentity, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest,
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest,
RenderingContextKind, ResizeRequest, ScrollRequest, ServoHost, ServoSurfaceSize, RenderingContextKind, ResizeRequest, ScrollRequest, ServoHost, ServoSurfaceSize,
SoftwareServoHost, WebViewState, SoftwareServoHost,
}; };
use super::args::LiveArgs; use super::args::LiveArgs;
@@ -361,7 +363,7 @@ fn poll_frame(
) -> Result<LiveOutcome, LiveSidecarError> { ) -> Result<LiveOutcome, LiveSidecarError> {
host.tick(); host.tick();
let snapshot = host.snapshot(&session.webview_id)?; let snapshot = host.snapshot(&session.webview_id)?;
if !snapshot.has_pending_frame() { if !should_paint_live_frame(snapshot.has_pending_frame(), session.awaiting_visible_frame) {
return Ok(LiveOutcome::empty()); return Ok(LiveOutcome::empty());
} }
@@ -379,6 +381,10 @@ fn poll_frame(
Ok(LiveOutcome::empty()) Ok(LiveOutcome::empty())
} }
fn should_paint_live_frame(has_pending_frame: bool, awaiting_visible_frame: bool) -> bool {
has_pending_frame || awaiting_visible_frame
}
fn paint_pending_frame( fn paint_pending_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &mut LiveSession, session: &mut LiveSession,
@@ -419,21 +425,11 @@ fn paint_hardware_surface_frame(
if !session.ever_visible_frame { if !session.ever_visible_frame {
return paint_initial_hardware_surface_frame(host, session); return paint_initial_hardware_surface_frame(host, session);
} }
// Cross-process IOSurface lookup can block the app-side worker for
let paint_started_at = Instant::now(); // seconds on macOS. Live app frames use readback so scroll/click
host.paint_without_readback(&session.webview_id)?; // input stays bounded by the paint barrier instead of the surface
let snapshot = host.snapshot(&session.webview_id)?; // import path.
let paint_ns = elapsed_ns(paint_started_at); paint_readback_frame(host, session)
let encode_started_at = Instant::now();
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"))] #[cfg(all(feature = "hardware-render", target_os = "macos"))]
@@ -531,4 +527,15 @@ mod tests {
"first apply_layout must resize after hidpi has been pushed", "first apply_layout must resize after hidpi has been pushed",
); );
} }
#[test]
fn awaiting_visible_frame_forces_paint_without_pending_flag() {
assert!(should_paint_live_frame(false, true));
}
#[test]
fn idle_poll_waits_for_pending_frame() {
assert!(!should_paint_live_frame(false, false));
assert!(should_paint_live_frame(true, false));
}
} }