perf(sidecar): use payloadless hardware frames after warmup
This commit is contained in:
@@ -7,7 +7,7 @@ use std::{
|
||||
|
||||
use ely_domain::{ProfileId, TabId, UrlText};
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
use ely_servo_host::WebViewState;
|
||||
use ely_servo_host::ServoHostError;
|
||||
use ely_servo_host::{
|
||||
IOSurfaceIdentity, NavigationRequest, RenderingContextKind, ServoHost, ServoSurfaceSize,
|
||||
SoftwareServoHost,
|
||||
@@ -26,6 +26,7 @@ use super::perf::{FramePerfAggregator, FramePerfSummary, elapsed_ns};
|
||||
|
||||
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||
let LiveArgs { profile_data_dir, iosurface_mach_service, rendering_context_kind } = args;
|
||||
let publish_readback_surface_fields = iosurface_mach_service.is_none();
|
||||
fs::create_dir_all(&profile_data_dir)?;
|
||||
let context_label = rendering_context_label(rendering_context_kind);
|
||||
let mut host = SoftwareServoHost::new_with_config_dir_and_kind(
|
||||
@@ -64,6 +65,7 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||
&mut sessions,
|
||||
&mut published_surface_ids,
|
||||
rendering_context_kind,
|
||||
publish_readback_surface_fields,
|
||||
request,
|
||||
),
|
||||
Err(error) => Err(LiveSidecarError::Json(error)),
|
||||
@@ -92,6 +94,7 @@ fn handle_request(
|
||||
sessions: &mut HashMap<String, LiveSession>,
|
||||
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||
rendering_context_kind: RenderingContextKind,
|
||||
publish_readback_surface_fields: bool,
|
||||
request: LiveRequest,
|
||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||
match request {
|
||||
@@ -159,12 +162,14 @@ fn handle_request(
|
||||
session.awaiting_visible_frame = true;
|
||||
}
|
||||
let webview_id = session.webview_id.clone();
|
||||
let mut outcome = poll_frame(host, session, rendering_context_kind)?;
|
||||
let mut outcome =
|
||||
poll_frame(host, session, rendering_context_kind, &tab_id, published_surface_ids)?;
|
||||
populate_surface_fields(
|
||||
host,
|
||||
&webview_id,
|
||||
&tab_id,
|
||||
published_surface_ids,
|
||||
publish_readback_surface_fields,
|
||||
&mut outcome,
|
||||
);
|
||||
Ok(outcome)
|
||||
@@ -174,12 +179,14 @@ fn handle_request(
|
||||
return Ok(LiveOutcome::empty());
|
||||
};
|
||||
let webview_id = session.webview_id.clone();
|
||||
let mut outcome = poll_frame(host, session, rendering_context_kind)?;
|
||||
let mut outcome =
|
||||
poll_frame(host, session, rendering_context_kind, &tab_id, published_surface_ids)?;
|
||||
populate_surface_fields(
|
||||
host,
|
||||
&webview_id,
|
||||
&tab_id,
|
||||
published_surface_ids,
|
||||
publish_readback_surface_fields,
|
||||
&mut outcome,
|
||||
);
|
||||
Ok(outcome)
|
||||
@@ -198,6 +205,8 @@ fn poll_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &mut LiveSession,
|
||||
rendering_context_kind: RenderingContextKind,
|
||||
tab_id: &str,
|
||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||
host.tick();
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
@@ -206,8 +215,14 @@ fn poll_frame(
|
||||
return Ok(LiveOutcome::empty());
|
||||
}
|
||||
|
||||
let (outcome, has_visible_content) =
|
||||
paint_pending_frame(host, session, rendering_context_kind, has_pending_frame)?;
|
||||
let (outcome, has_visible_content) = paint_pending_frame(
|
||||
host,
|
||||
session,
|
||||
rendering_context_kind,
|
||||
tab_id,
|
||||
published_surface_ids,
|
||||
has_pending_frame,
|
||||
)?;
|
||||
if has_visible_content {
|
||||
session.awaiting_visible_frame = false;
|
||||
session.ever_visible_frame = true;
|
||||
@@ -228,14 +243,23 @@ fn paint_pending_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &mut LiveSession,
|
||||
rendering_context_kind: RenderingContextKind,
|
||||
tab_id: &str,
|
||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||
has_pending_frame: bool,
|
||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||
let _ = (tab_id, published_surface_ids);
|
||||
|
||||
match rendering_context_kind {
|
||||
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, has_pending_frame)
|
||||
}
|
||||
RenderingContextKind::Hardware => paint_hardware_surface_frame(
|
||||
host,
|
||||
session,
|
||||
tab_id,
|
||||
published_surface_ids,
|
||||
has_pending_frame,
|
||||
),
|
||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||
RenderingContextKind::Hardware => paint_readback_frame(host, session, !has_pending_frame),
|
||||
}
|
||||
@@ -264,16 +288,22 @@ fn paint_readback_frame(
|
||||
fn paint_hardware_surface_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &LiveSession,
|
||||
tab_id: &str,
|
||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||
has_pending_frame: bool,
|
||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||
if !session.ever_visible_frame {
|
||||
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, !has_pending_frame)
|
||||
if !payloadless_surface_pool_ready(published_surface_ids, tab_id, session.width, session.height)
|
||||
{
|
||||
return paint_readback_frame(host, session, !has_pending_frame);
|
||||
}
|
||||
let (outcome, identity) = paint_hardware_surface_report(host, session, !has_pending_frame)?;
|
||||
if !surface_has_been_published(published_surface_ids, tab_id, identity) {
|
||||
return paint_readback_frame(host, session, true);
|
||||
}
|
||||
Ok((outcome, true))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
@@ -289,17 +319,61 @@ fn paint_initial_hardware_surface_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 has_visible_content = frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0;
|
||||
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)
|
||||
fn paint_hardware_surface_report(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &LiveSession,
|
||||
wait_for_completion: bool,
|
||||
) -> Result<(LiveOutcome, IOSurfaceIdentity), LiveSidecarError> {
|
||||
let paint_started_at = Instant::now();
|
||||
host.paint_without_readback_with_completion(&session.webview_id, wait_for_completion)?;
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
let identity = host.peek_iosurface_identity(&session.webview_id)?.ok_or_else(|| {
|
||||
ServoHostError::HardwareSurfaceUnavailable { id: session.webview_id.clone() }
|
||||
})?;
|
||||
let paint_ns = elapsed_ns(paint_started_at);
|
||||
let encode_started_at = Instant::now();
|
||||
let report = LiveFrameReport::from_surface(
|
||||
&snapshot,
|
||||
identity.width,
|
||||
identity.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), identity))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
fn payloadless_surface_pool_ready(
|
||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||
tab_id: &str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> bool {
|
||||
published_surface_ids.get(tab_id).is_some_and(|published| {
|
||||
published
|
||||
.iter()
|
||||
.filter(|identity| identity.width == width && identity.height == height)
|
||||
.take(2)
|
||||
.count()
|
||||
>= 2
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
fn surface_has_been_published(
|
||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||
tab_id: &str,
|
||||
identity: IOSurfaceIdentity,
|
||||
) -> bool {
|
||||
published_surface_ids.get(tab_id).is_some_and(|published| published.contains(&identity))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -11,8 +11,12 @@ use ely_servo_host::{IOSurfaceIdentity, SoftwareServoHost};
|
||||
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
|
||||
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
||||
|
||||
/// Populate the hardware surface protocol fields on `outcome`. Two
|
||||
/// pieces of state ride out together:
|
||||
/// Populate the hardware surface protocol fields on `outcome`. Mach
|
||||
/// app clients keep readback frames free of surface fields because
|
||||
/// synchronous IOSurface import can block the live worker; the no-Mach
|
||||
/// bench path publishes readback warm-up handles so it can validate
|
||||
/// payloadless steady-state frames. Two pieces of state ride out
|
||||
/// together:
|
||||
///
|
||||
/// * `current_surface_id` — set on every payload-bearing hardware
|
||||
/// frame so the receiver knows which previously-imported
|
||||
@@ -30,12 +34,13 @@ pub(super) fn populate_surface_fields(
|
||||
webview_id: &ely_domain::WebViewId,
|
||||
tab_id: &str,
|
||||
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||
publish_readback_surface_fields: bool,
|
||||
outcome: &mut LiveOutcome,
|
||||
) {
|
||||
if outcome.response.frame.is_none() {
|
||||
return;
|
||||
}
|
||||
if outcome.frame.is_some() {
|
||||
if outcome.frame.is_some() && !publish_readback_surface_fields {
|
||||
return;
|
||||
}
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
@@ -58,7 +63,7 @@ pub(super) fn populate_surface_fields(
|
||||
}
|
||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||
{
|
||||
let _ = (host, webview_id, tab_id, published_surface_ids);
|
||||
let _ = (host, webview_id, tab_id, published_surface_ids, publish_readback_surface_fields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,12 +159,9 @@ pub(super) fn write_outcome(
|
||||
if let Some(summary) = pending_summary.take() {
|
||||
outcome.response.perf = Some(summary);
|
||||
}
|
||||
// Hardware path: receiver samples the IOSurface directly through
|
||||
// its CVPixelBuffer cache, so the raw RGBA payload is dead
|
||||
// weight. Drop it from the wire (and zero the byte count in the
|
||||
// header so the client knows nothing follows). At 1080p × 60 fps
|
||||
// that's 8 MB × 60 = ~480 MB/s of pipe traffic eliminated.
|
||||
let drop_rgba_payload = outcome.response.current_surface_id.is_some();
|
||||
// Payloadless hardware frames carry only the IOSurface selector.
|
||||
let drop_rgba_payload =
|
||||
outcome.response.current_surface_id.is_some() && outcome.frame.is_none();
|
||||
if drop_rgba_payload && let Some(report) = outcome.response.frame.as_mut() {
|
||||
report.rgba_byte_count = 0;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ impl LiveOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, all(feature = "hardware-render", target_os = "macos")))]
|
||||
pub fn from_report(report: LiveFrameReport, partial_timings: PartialFrameTimings) -> Self {
|
||||
Self {
|
||||
response: LiveResponse::frame(report),
|
||||
@@ -204,6 +204,31 @@ impl LiveFrameReport {
|
||||
sample_hash: frame.sample_hash(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
pub fn from_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,
|
||||
sample_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn css_viewport_size(width: u32, height: u32, device_pixel_ratio: f32) -> (u32, u32) {
|
||||
|
||||
@@ -33,6 +33,9 @@ pub enum ServoHostError {
|
||||
#[error("servo rendered frame is unavailable")]
|
||||
RenderedFrameUnavailable,
|
||||
|
||||
#[error("servo hardware surface is unavailable for {id}")]
|
||||
HardwareSurfaceUnavailable { id: WebViewId },
|
||||
|
||||
#[error("servo screenshot capture timed out for {id}")]
|
||||
ScreenshotTimedOut { id: WebViewId },
|
||||
|
||||
|
||||
@@ -65,13 +65,6 @@ impl SoftwareServoHost {
|
||||
}
|
||||
|
||||
/// Construct the host with an explicit [`RenderingContextKind`].
|
||||
///
|
||||
/// `Hardware` requires the `hardware-render` feature; the call
|
||||
/// fails with `ServoHostError::HardwareRenderUnavailable` if the
|
||||
/// feature wasn't compiled in. This is the constructor the
|
||||
/// sidecar binary will use once a `--rendering-context` CLI
|
||||
/// flag lands; today the default path through `new` and
|
||||
/// `new_with_config_dir` keeps the software behaviour unchanged.
|
||||
pub fn new_with_config_dir_and_kind(
|
||||
size: ServoSurfaceSize,
|
||||
config_dir: Option<PathBuf>,
|
||||
@@ -106,12 +99,17 @@ impl SoftwareServoHost {
|
||||
self.create_webview_in_context(tab_id, profile_id, size)
|
||||
}
|
||||
|
||||
/// Paint and present the webview's current surface while leaving
|
||||
/// framebuffer readback to callers that explicitly need RGBA
|
||||
/// bytes. The live hardware path exports the just-presented
|
||||
/// IOSurface from the rendering context.
|
||||
/// Paint and present the current surface without RGBA readback.
|
||||
pub fn paint_without_readback(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> {
|
||||
self.paint_webview(webview_id, false, true).map(|_| ())
|
||||
self.paint_without_readback_with_completion(webview_id, true)
|
||||
}
|
||||
|
||||
pub fn paint_without_readback_with_completion(
|
||||
&mut self,
|
||||
webview_id: &WebViewId,
|
||||
wait_for_completion: bool,
|
||||
) -> Result<(), ServoHostError> {
|
||||
self.paint_webview(webview_id, false, wait_for_completion).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn close_webview(&mut self, webview_id: &WebViewId) -> bool {
|
||||
@@ -151,16 +149,8 @@ impl SoftwareServoHost {
|
||||
let rendering_context = self.webview(webview_id)?.rendering_context.clone();
|
||||
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||
rendering_context.prepare_for_rendering();
|
||||
// `webview.paint()` dispatches a render command to Servo's paint
|
||||
// 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 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.
|
||||
// Clear the pending-frame flag before `paint()` so barrier callers observe
|
||||
// the next Servo frame-ready notification for this paint.
|
||||
{
|
||||
let webview = self.webview(webview_id)?;
|
||||
webview.delegate.mark_frame_presented();
|
||||
@@ -228,12 +218,7 @@ impl ServoHost for SoftwareServoHost {
|
||||
// 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
|
||||
// `webview_for_input`; we deliberately do not re-show or
|
||||
// re-focus on the `load()` branch so a background tab
|
||||
// finishing a load cannot steal focus from the foreground
|
||||
// tab between the user's mouse-down and the next render.
|
||||
// The input-accepting invariant lives in `webview_for_input`.
|
||||
webview.webview.show();
|
||||
webview.webview.focus();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user