From 234bf4c8a58afa903943006a6deb1cdf31eb20ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 16 May 2026 01:02:38 -0400 Subject: [PATCH] fix(web-surface): bound live input paints --- .../src/shell/web_surface_live_site_tests.rs | 103 +++++++++--------- .../src/bin/ely_servo_sidecar/live.rs | 41 ++++--- 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/crates/ely_app/src/shell/web_surface_live_site_tests.rs b/crates/ely_app/src/shell/web_surface_live_site_tests.rs index 72a6ef0..516eebe 100644 --- a/crates/ely_app/src/shell/web_surface_live_site_tests.rs +++ b/crates/ely_app/src/shell/web_surface_live_site_tests.rs @@ -125,8 +125,8 @@ fn assert_web_surface_scrolls_prd_site() -> Result<(), Box> { let profile_id = ProfileId::new(); let case = PRD_TOP_SITE_CASES .iter() - .find(|case| case.url == "https://servo.org/") - .ok_or("missing servo.org live-site case")?; + .find(|case| case.url == "https://github.com") + .ok_or("missing github.com live-site case")?; let tab = web_tab(profile_id, case.url)?; assert_eq!( @@ -299,19 +299,14 @@ fn wait_for_ready_frame( let mut last_error = None; 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)); match store.state(tab_id) { Some(WebSurfaceState::Ready(frame)) => { if let Err(error) = validate_prd_frame(frame, case, 0) { last_error = Some(error); - thread::sleep(LIVE_SITE_WAIT_INTERVAL); - continue; + } else { + return Ok(frame.clone()); } - return Ok(frame.clone()); } Some(WebSurfaceState::Failed { message, .. }) => { return Err(format!("{} failed: {message}", case.url)); @@ -319,6 +314,10 @@ fn wait_for_ready_frame( 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); } } @@ -334,12 +333,6 @@ fn wait_for_ready_frame_at_scroll( let mut last_error = None; 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)); match store.state(tab_id) { 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) { last_error = Some(error); - thread::sleep(LIVE_SITE_WAIT_INTERVAL); - continue; - } - if !frame.has_hardware_surface() + } else if !frame.has_hardware_surface() && let Some(previous_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", case.url )); - thread::sleep(LIVE_SITE_WAIT_INTERVAL); - continue; + } else { + 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, .. }) => { return Err(format!("{} failed: {message}", case.url)); } 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); } } @@ -385,12 +386,6 @@ fn wait_for_ready_frame_at_size( let mut last_error = None; 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)); match store.state(tab_id) { 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) { last_error = Some(error); - thread::sleep(LIVE_SITE_WAIT_INTERVAL); - continue; + } else { + return Ok(frame.clone()); } - return Ok(frame.clone()); } Some(WebSurfaceState::Ready(_)) => {} Some(WebSurfaceState::Failed { message, .. }) => { @@ -413,6 +407,12 @@ fn wait_for_ready_frame_at_size( 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); } } @@ -427,6 +427,25 @@ fn wait_for_ready_frame_at_css_size( let mut last_error = None; 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 { return Err(last_error.unwrap_or_else(|| { 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); } } diff --git a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs index 5408467..aa715b4 100644 --- a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs +++ b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs @@ -6,11 +6,13 @@ use std::{ }; 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::{ IOSurfaceIdentity, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderingContextKind, ResizeRequest, ScrollRequest, ServoHost, ServoSurfaceSize, - SoftwareServoHost, WebViewState, + SoftwareServoHost, }; use super::args::LiveArgs; @@ -361,7 +363,7 @@ fn poll_frame( ) -> Result { host.tick(); 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()); } @@ -379,6 +381,10 @@ fn poll_frame( 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( host: &mut SoftwareServoHost, session: &mut LiveSession, @@ -419,21 +425,11 @@ fn paint_hardware_surface_frame( 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, - 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)) + // Cross-process IOSurface lookup can block the app-side worker for + // seconds on macOS. Live app frames use readback so scroll/click + // input stays bounded by the paint barrier instead of the surface + // import path. + paint_readback_frame(host, session) } #[cfg(all(feature = "hardware-render", target_os = "macos"))] @@ -531,4 +527,15 @@ mod tests { "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)); + } }