From 42dc9af66c1c8922f344af5e89de3d6bb14fd124 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:25:37 -0400 Subject: [PATCH] perf(sidecar): skip redundant paint barriers --- .../src/bin/ely_servo_sidecar/live.rs | 25 +++++++----- crates/ely_servo_host/src/runtime.rs | 38 ++++++++++++------- 2 files changed, 41 insertions(+), 22 deletions(-) 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 5e49dcf..1148cf0 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 @@ -201,12 +201,13 @@ fn poll_frame( ) -> Result { host.tick(); let snapshot = host.snapshot(&session.webview_id)?; - if !should_paint_live_frame(snapshot.has_pending_frame(), session.awaiting_visible_frame) { + let has_pending_frame = snapshot.has_pending_frame(); + if !should_paint_live_frame(has_pending_frame, session.awaiting_visible_frame) { return Ok(LiveOutcome::empty()); } let (outcome, has_visible_content) = - paint_pending_frame(host, session, rendering_context_kind)?; + paint_pending_frame(host, session, rendering_context_kind, has_pending_frame)?; if has_visible_content { session.awaiting_visible_frame = false; session.ever_visible_frame = true; @@ -227,22 +228,26 @@ fn paint_pending_frame( host: &mut SoftwareServoHost, session: &mut LiveSession, rendering_context_kind: RenderingContextKind, + has_pending_frame: bool, ) -> Result<(LiveOutcome, bool), LiveSidecarError> { match rendering_context_kind { - RenderingContextKind::Software => paint_readback_frame(host, session), + RenderingContextKind::Software => paint_readback_frame(host, session, !has_pending_frame), #[cfg(all(feature = "hardware-render", target_os = "macos"))] - RenderingContextKind::Hardware => paint_hardware_surface_frame(host, session), + RenderingContextKind::Hardware => { + paint_hardware_surface_frame(host, session, has_pending_frame) + } #[cfg(not(all(feature = "hardware-render", target_os = "macos")))] - RenderingContextKind::Hardware => paint_readback_frame(host, session), + RenderingContextKind::Hardware => paint_readback_frame(host, session, !has_pending_frame), } } fn paint_readback_frame( host: &mut SoftwareServoHost, session: &LiveSession, + wait_for_completion: bool, ) -> Result<(LiveOutcome, bool), LiveSidecarError> { let paint_started_at = Instant::now(); - host.paint(&session.webview_id)?; + host.paint_with_readback(&session.webview_id, wait_for_completion)?; let snapshot = host.snapshot(&session.webview_id)?; let frame = host.last_rendered_frame()?; let paint_ns = elapsed_ns(paint_started_at); @@ -259,24 +264,26 @@ fn paint_readback_frame( fn paint_hardware_surface_frame( host: &mut SoftwareServoHost, session: &LiveSession, + has_pending_frame: bool, ) -> Result<(LiveOutcome, bool), LiveSidecarError> { if !session.ever_visible_frame { - return paint_initial_hardware_surface_frame(host, session); + return paint_initial_hardware_surface_frame(host, session, !has_pending_frame); } // 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) + paint_readback_frame(host, session, !has_pending_frame) } #[cfg(all(feature = "hardware-render", target_os = "macos"))] fn paint_initial_hardware_surface_frame( host: &mut SoftwareServoHost, session: &LiveSession, + wait_for_completion: bool, ) -> Result<(LiveOutcome, bool), LiveSidecarError> { let paint_started_at = Instant::now(); - host.paint(&session.webview_id)?; + host.paint_with_readback(&session.webview_id, wait_for_completion)?; let snapshot = host.snapshot(&session.webview_id)?; let frame = host.last_rendered_frame()?; let paint_ns = elapsed_ns(paint_started_at); diff --git a/crates/ely_servo_host/src/runtime.rs b/crates/ely_servo_host/src/runtime.rs index 8c6669f..5064d93 100644 --- a/crates/ely_servo_host/src/runtime.rs +++ b/crates/ely_servo_host/src/runtime.rs @@ -111,7 +111,7 @@ impl SoftwareServoHost { /// 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(|_| ()) + self.paint_webview(webview_id, false, true).map(|_| ()) } pub fn close_webview(&mut self, webview_id: &WebViewId) -> bool { @@ -146,6 +146,7 @@ impl SoftwareServoHost { &mut self, webview_id: &WebViewId, capture_frame: bool, + wait_for_completion: bool, ) -> Result, ServoHostError> { let rendering_context = self.webview(webview_id)?.rendering_context.clone(); rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?; @@ -154,18 +155,20 @@ impl SoftwareServoHost { // thread — it does NOT block until the framebuffer is consistent. // Without a barrier, `read_rendered_frame` below races the paint // thread and reliably reads the cleared-white state on data: URLs. - // Clear the pending-frame flag first so we can detect the *next* - // `notify_new_frame_ready` (the one our `paint()` triggers), then - // pump the event loop until Servo reports the new frame is ready - // or `paint_barrier_budget()` elapses. On timeout we fall through - // and read anyway, preserving the pre-T15 fast path for callers - // that explicitly disable the barrier with `ELY_PAINT_BARRIER_MS=0`. + // Clear the pending-frame flag first so barrier callers can + // detect the *next* `notify_new_frame_ready` (the one our + // `paint()` triggers), then pump the event loop until Servo + // reports the new frame is ready or `paint_barrier_budget()` + // elapses. When the caller already observed a pending frame, + // readback can use that ready frame and skip the extra wait. { let webview = self.webview(webview_id)?; webview.delegate.mark_frame_presented(); webview.webview.paint(); } - self.wait_for_paint_completion(webview_id); + if wait_for_completion { + self.wait_for_paint_completion(webview_id); + } let rendered_frame = if capture_frame { Some(Self::read_rendered_frame(rendering_context.as_ref())?) } else { @@ -175,6 +178,19 @@ impl SoftwareServoHost { self.webview(webview_id)?.delegate.mark_frame_presented(); Ok(rendered_frame) } + + pub fn paint_with_readback( + &mut self, + webview_id: &WebViewId, + wait_for_completion: bool, + ) -> Result<(), ServoHostError> { + let Some(rendered_frame) = self.paint_webview(webview_id, true, wait_for_completion)? + else { + return Err(ServoHostError::RenderedFrameUnavailable); + }; + self.last_rendered_frame = Some(rendered_frame); + Ok(()) + } } impl ServoHost for SoftwareServoHost { @@ -383,11 +399,7 @@ impl ServoHost for SoftwareServoHost { } fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> { - let Some(rendered_frame) = self.paint_webview(webview_id, true)? else { - return Err(ServoHostError::RenderedFrameUnavailable); - }; - self.last_rendered_frame = Some(rendered_frame); - Ok(()) + self.paint_with_readback(webview_id, true) } fn last_rendered_frame(&self) -> Result {