perf(sidecar): skip redundant paint barriers

This commit is contained in:
2026-05-16 01:25:37 -04:00
parent 8ad85ce7f7
commit 42dc9af66c
2 changed files with 41 additions and 22 deletions
@@ -201,12 +201,13 @@ 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 !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()); return Ok(LiveOutcome::empty());
} }
let (outcome, has_visible_content) = 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 { if has_visible_content {
session.awaiting_visible_frame = false; session.awaiting_visible_frame = false;
session.ever_visible_frame = true; session.ever_visible_frame = true;
@@ -227,22 +228,26 @@ fn paint_pending_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &mut LiveSession, session: &mut LiveSession,
rendering_context_kind: RenderingContextKind, rendering_context_kind: RenderingContextKind,
has_pending_frame: bool,
) -> Result<(LiveOutcome, bool), LiveSidecarError> { ) -> Result<(LiveOutcome, bool), LiveSidecarError> {
match rendering_context_kind { 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"))] #[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")))] #[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( fn paint_readback_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &LiveSession, session: &LiveSession,
wait_for_completion: bool,
) -> Result<(LiveOutcome, bool), LiveSidecarError> { ) -> Result<(LiveOutcome, bool), LiveSidecarError> {
let paint_started_at = Instant::now(); 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 snapshot = host.snapshot(&session.webview_id)?;
let frame = host.last_rendered_frame()?; let frame = host.last_rendered_frame()?;
let paint_ns = elapsed_ns(paint_started_at); let paint_ns = elapsed_ns(paint_started_at);
@@ -259,24 +264,26 @@ fn paint_readback_frame(
fn paint_hardware_surface_frame( fn paint_hardware_surface_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &LiveSession, session: &LiveSession,
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); return paint_initial_hardware_surface_frame(host, session, !has_pending_frame);
} }
// Cross-process IOSurface lookup can block the app-side worker for // Cross-process IOSurface lookup can block the app-side worker for
// seconds on macOS. Live app frames use readback so scroll/click // seconds on macOS. Live app frames use readback so scroll/click
// input stays bounded by the paint barrier instead of the surface // input stays bounded by the paint barrier instead of the surface
// import path. // import path.
paint_readback_frame(host, session) paint_readback_frame(host, session, !has_pending_frame)
} }
#[cfg(all(feature = "hardware-render", target_os = "macos"))] #[cfg(all(feature = "hardware-render", target_os = "macos"))]
fn paint_initial_hardware_surface_frame( fn paint_initial_hardware_surface_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &LiveSession, session: &LiveSession,
wait_for_completion: bool,
) -> Result<(LiveOutcome, bool), LiveSidecarError> { ) -> Result<(LiveOutcome, bool), LiveSidecarError> {
let paint_started_at = Instant::now(); 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 snapshot = host.snapshot(&session.webview_id)?;
let frame = host.last_rendered_frame()?; let frame = host.last_rendered_frame()?;
let paint_ns = elapsed_ns(paint_started_at); let paint_ns = elapsed_ns(paint_started_at);
+25 -13
View File
@@ -111,7 +111,7 @@ impl SoftwareServoHost {
/// bytes. The live hardware path exports the just-presented /// bytes. The live hardware path exports the just-presented
/// IOSurface from the rendering context. /// IOSurface from the rendering context.
pub fn paint_without_readback(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> { 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 { pub fn close_webview(&mut self, webview_id: &WebViewId) -> bool {
@@ -146,6 +146,7 @@ impl SoftwareServoHost {
&mut self, &mut self,
webview_id: &WebViewId, webview_id: &WebViewId,
capture_frame: bool, capture_frame: bool,
wait_for_completion: bool,
) -> Result<Option<RenderedFrame>, ServoHostError> { ) -> Result<Option<RenderedFrame>, ServoHostError> {
let rendering_context = self.webview(webview_id)?.rendering_context.clone(); let rendering_context = self.webview(webview_id)?.rendering_context.clone();
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?; rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
@@ -154,18 +155,20 @@ impl SoftwareServoHost {
// thread — it does NOT block until the framebuffer is consistent. // thread — it does NOT block until the framebuffer is consistent.
// Without a barrier, `read_rendered_frame` below races the paint // Without a barrier, `read_rendered_frame` below races the paint
// thread and reliably reads the cleared-white state on data: URLs. // thread and reliably reads the cleared-white state on data: URLs.
// Clear the pending-frame flag first so we can detect the *next* // Clear the pending-frame flag first so barrier callers can
// `notify_new_frame_ready` (the one our `paint()` triggers), then // detect the *next* `notify_new_frame_ready` (the one our
// pump the event loop until Servo reports the new frame is ready // `paint()` triggers), then pump the event loop until Servo
// or `paint_barrier_budget()` elapses. On timeout we fall through // reports the new frame is ready or `paint_barrier_budget()`
// and read anyway, preserving the pre-T15 fast path for callers // elapses. When the caller already observed a pending frame,
// that explicitly disable the barrier with `ELY_PAINT_BARRIER_MS=0`. // readback can use that ready frame and skip the extra wait.
{ {
let webview = self.webview(webview_id)?; let webview = self.webview(webview_id)?;
webview.delegate.mark_frame_presented(); webview.delegate.mark_frame_presented();
webview.webview.paint(); 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 { let rendered_frame = if capture_frame {
Some(Self::read_rendered_frame(rendering_context.as_ref())?) Some(Self::read_rendered_frame(rendering_context.as_ref())?)
} else { } else {
@@ -175,6 +178,19 @@ impl SoftwareServoHost {
self.webview(webview_id)?.delegate.mark_frame_presented(); self.webview(webview_id)?.delegate.mark_frame_presented();
Ok(rendered_frame) 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 { impl ServoHost for SoftwareServoHost {
@@ -383,11 +399,7 @@ impl ServoHost for SoftwareServoHost {
} }
fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> { fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> {
let Some(rendered_frame) = self.paint_webview(webview_id, true)? else { self.paint_with_readback(webview_id, true)
return Err(ServoHostError::RenderedFrameUnavailable);
};
self.last_rendered_frame = Some(rendered_frame);
Ok(())
} }
fn last_rendered_frame(&self) -> Result<RenderedFrame, ServoHostError> { fn last_rendered_frame(&self) -> Result<RenderedFrame, ServoHostError> {