perf(sidecar): use payloadless hardware frames after warmup
This commit is contained in:
@@ -133,17 +133,17 @@ impl ServoLiveClient {
|
|||||||
if let Some(error) = response.error {
|
if let Some(error) = response.error {
|
||||||
return Err(ServoLiveError::SidecarFailed { message: error });
|
return Err(ServoLiveError::SidecarFailed { message: error });
|
||||||
}
|
}
|
||||||
|
let surface_handle = response.surface_handle;
|
||||||
|
let current_surface_id = response.current_surface_id;
|
||||||
|
|
||||||
if let Some(perf) = response.perf.as_ref() {
|
if let Some(perf) = response.perf.as_ref() {
|
||||||
log_frame_perf(perf);
|
log_frame_perf(perf);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(handle) = response.surface_handle.as_ref() {
|
if let Some(handle) = surface_handle.as_ref() {
|
||||||
log_iosurface_handle(handle);
|
log_iosurface_handle(handle);
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
self.import_iosurface_handle(handle)?;
|
|
||||||
}
|
}
|
||||||
if let Some(surface_id) = response.current_surface_id {
|
if let Some(surface_id) = current_surface_id {
|
||||||
log_iosurface_current(surface_id);
|
log_iosurface_current(surface_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,15 @@ impl ServoLiveClient {
|
|||||||
let mut frame = ServoLiveFrame::from_parts(report, rgba_bytes);
|
let mut frame = ServoLiveFrame::from_parts(report, rgba_bytes);
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(surface_id) = response.current_surface_id {
|
if let Some(handle) = surface_handle.as_ref() {
|
||||||
|
// Drain the stdout payload before IOSurface import so the
|
||||||
|
// sidecar cannot block writing RGBA bytes while this worker
|
||||||
|
// is inside IOSurfaceLookupFromMachPort.
|
||||||
|
self.import_iosurface_handle(handle)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Some(surface_id) = current_surface_id {
|
||||||
let pixel_buffer = self.iosurface_cache.pixel_buffer_for(surface_id);
|
let pixel_buffer = self.iosurface_cache.pixel_buffer_for(surface_id);
|
||||||
if pixel_buffer.is_none() && !has_software_payload {
|
if pixel_buffer.is_none() && !has_software_payload {
|
||||||
return Err(ServoLiveError::IOSurfacePixelBufferMissing { surface_id });
|
return Err(ServoLiveError::IOSurfacePixelBufferMissing { surface_id });
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
|
|
||||||
use ely_domain::{ProfileId, TabId, UrlText};
|
use ely_domain::{ProfileId, TabId, UrlText};
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
use ely_servo_host::WebViewState;
|
use ely_servo_host::ServoHostError;
|
||||||
use ely_servo_host::{
|
use ely_servo_host::{
|
||||||
IOSurfaceIdentity, NavigationRequest, RenderingContextKind, ServoHost, ServoSurfaceSize,
|
IOSurfaceIdentity, NavigationRequest, RenderingContextKind, ServoHost, ServoSurfaceSize,
|
||||||
SoftwareServoHost,
|
SoftwareServoHost,
|
||||||
@@ -26,6 +26,7 @@ use super::perf::{FramePerfAggregator, FramePerfSummary, elapsed_ns};
|
|||||||
|
|
||||||
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||||
let LiveArgs { profile_data_dir, iosurface_mach_service, rendering_context_kind } = args;
|
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)?;
|
fs::create_dir_all(&profile_data_dir)?;
|
||||||
let context_label = rendering_context_label(rendering_context_kind);
|
let context_label = rendering_context_label(rendering_context_kind);
|
||||||
let mut host = SoftwareServoHost::new_with_config_dir_and_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 sessions,
|
||||||
&mut published_surface_ids,
|
&mut published_surface_ids,
|
||||||
rendering_context_kind,
|
rendering_context_kind,
|
||||||
|
publish_readback_surface_fields,
|
||||||
request,
|
request,
|
||||||
),
|
),
|
||||||
Err(error) => Err(LiveSidecarError::Json(error)),
|
Err(error) => Err(LiveSidecarError::Json(error)),
|
||||||
@@ -92,6 +94,7 @@ fn handle_request(
|
|||||||
sessions: &mut HashMap<String, LiveSession>,
|
sessions: &mut HashMap<String, LiveSession>,
|
||||||
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
rendering_context_kind: RenderingContextKind,
|
rendering_context_kind: RenderingContextKind,
|
||||||
|
publish_readback_surface_fields: bool,
|
||||||
request: LiveRequest,
|
request: LiveRequest,
|
||||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||||
match request {
|
match request {
|
||||||
@@ -159,12 +162,14 @@ fn handle_request(
|
|||||||
session.awaiting_visible_frame = true;
|
session.awaiting_visible_frame = true;
|
||||||
}
|
}
|
||||||
let webview_id = session.webview_id.clone();
|
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(
|
populate_surface_fields(
|
||||||
host,
|
host,
|
||||||
&webview_id,
|
&webview_id,
|
||||||
&tab_id,
|
&tab_id,
|
||||||
published_surface_ids,
|
published_surface_ids,
|
||||||
|
publish_readback_surface_fields,
|
||||||
&mut outcome,
|
&mut outcome,
|
||||||
);
|
);
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
@@ -174,12 +179,14 @@ fn handle_request(
|
|||||||
return Ok(LiveOutcome::empty());
|
return Ok(LiveOutcome::empty());
|
||||||
};
|
};
|
||||||
let webview_id = session.webview_id.clone();
|
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(
|
populate_surface_fields(
|
||||||
host,
|
host,
|
||||||
&webview_id,
|
&webview_id,
|
||||||
&tab_id,
|
&tab_id,
|
||||||
published_surface_ids,
|
published_surface_ids,
|
||||||
|
publish_readback_surface_fields,
|
||||||
&mut outcome,
|
&mut outcome,
|
||||||
);
|
);
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
@@ -198,6 +205,8 @@ fn poll_frame(
|
|||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &mut LiveSession,
|
session: &mut LiveSession,
|
||||||
rendering_context_kind: RenderingContextKind,
|
rendering_context_kind: RenderingContextKind,
|
||||||
|
tab_id: &str,
|
||||||
|
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||||
host.tick();
|
host.tick();
|
||||||
let snapshot = host.snapshot(&session.webview_id)?;
|
let snapshot = host.snapshot(&session.webview_id)?;
|
||||||
@@ -206,8 +215,14 @@ fn poll_frame(
|
|||||||
return Ok(LiveOutcome::empty());
|
return Ok(LiveOutcome::empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (outcome, has_visible_content) =
|
let (outcome, has_visible_content) = paint_pending_frame(
|
||||||
paint_pending_frame(host, session, rendering_context_kind, has_pending_frame)?;
|
host,
|
||||||
|
session,
|
||||||
|
rendering_context_kind,
|
||||||
|
tab_id,
|
||||||
|
published_surface_ids,
|
||||||
|
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;
|
||||||
@@ -228,14 +243,23 @@ fn paint_pending_frame(
|
|||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &mut LiveSession,
|
session: &mut LiveSession,
|
||||||
rendering_context_kind: RenderingContextKind,
|
rendering_context_kind: RenderingContextKind,
|
||||||
|
tab_id: &str,
|
||||||
|
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
has_pending_frame: bool,
|
has_pending_frame: bool,
|
||||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||||
|
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||||
|
let _ = (tab_id, published_surface_ids);
|
||||||
|
|
||||||
match rendering_context_kind {
|
match rendering_context_kind {
|
||||||
RenderingContextKind::Software => paint_readback_frame(host, session, !has_pending_frame),
|
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 => {
|
RenderingContextKind::Hardware => paint_hardware_surface_frame(
|
||||||
paint_hardware_surface_frame(host, session, has_pending_frame)
|
host,
|
||||||
}
|
session,
|
||||||
|
tab_id,
|
||||||
|
published_surface_ids,
|
||||||
|
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, !has_pending_frame),
|
RenderingContextKind::Hardware => paint_readback_frame(host, session, !has_pending_frame),
|
||||||
}
|
}
|
||||||
@@ -264,16 +288,22 @@ fn paint_readback_frame(
|
|||||||
fn paint_hardware_surface_frame(
|
fn paint_hardware_surface_frame(
|
||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &LiveSession,
|
session: &LiveSession,
|
||||||
|
tab_id: &str,
|
||||||
|
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
has_pending_frame: bool,
|
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, !has_pending_frame);
|
return paint_initial_hardware_surface_frame(host, session, !has_pending_frame);
|
||||||
}
|
}
|
||||||
// Cross-process IOSurface lookup can block the app-side worker for
|
if !payloadless_surface_pool_ready(published_surface_ids, tab_id, session.width, session.height)
|
||||||
// seconds on macOS. Live app frames use readback so scroll/click
|
{
|
||||||
// input stays bounded by the paint barrier instead of the surface
|
return paint_readback_frame(host, session, !has_pending_frame);
|
||||||
// import path.
|
}
|
||||||
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"))]
|
#[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 paint_ns = elapsed_ns(paint_started_at);
|
||||||
let encode_started_at = Instant::now();
|
let encode_started_at = Instant::now();
|
||||||
let report = LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio());
|
let report = LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio());
|
||||||
let has_visible_content = frame.non_white_pixel_count() > 0
|
let has_visible_content = frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0;
|
||||||
&& frame.content_pixel_count() > 0
|
|
||||||
&& hardware_snapshot_has_visible_document(&snapshot);
|
|
||||||
let encode_ns = elapsed_ns(encode_started_at);
|
let encode_ns = elapsed_ns(encode_started_at);
|
||||||
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
||||||
Ok((LiveOutcome::from_frame(report, frame, timings), has_visible_content))
|
Ok((LiveOutcome::from_frame(report, frame, timings), has_visible_content))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
fn hardware_snapshot_has_visible_document(snapshot: &ely_servo_host::WebViewSnapshot) -> bool {
|
fn paint_hardware_surface_report(
|
||||||
snapshot.title().is_some() || matches!(snapshot.state(), WebViewState::Complete)
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ use ely_servo_host::{IOSurfaceIdentity, SoftwareServoHost};
|
|||||||
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
|
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
|
||||||
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
||||||
|
|
||||||
/// Populate the hardware surface protocol fields on `outcome`. Two
|
/// Populate the hardware surface protocol fields on `outcome`. Mach
|
||||||
/// pieces of state ride out together:
|
/// 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
|
/// * `current_surface_id` — set on every payload-bearing hardware
|
||||||
/// frame so the receiver knows which previously-imported
|
/// frame so the receiver knows which previously-imported
|
||||||
@@ -30,12 +34,13 @@ pub(super) fn populate_surface_fields(
|
|||||||
webview_id: &ely_domain::WebViewId,
|
webview_id: &ely_domain::WebViewId,
|
||||||
tab_id: &str,
|
tab_id: &str,
|
||||||
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
|
publish_readback_surface_fields: bool,
|
||||||
outcome: &mut LiveOutcome,
|
outcome: &mut LiveOutcome,
|
||||||
) {
|
) {
|
||||||
if outcome.response.frame.is_none() {
|
if outcome.response.frame.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if outcome.frame.is_some() {
|
if outcome.frame.is_some() && !publish_readback_surface_fields {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[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")))]
|
#[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() {
|
if let Some(summary) = pending_summary.take() {
|
||||||
outcome.response.perf = Some(summary);
|
outcome.response.perf = Some(summary);
|
||||||
}
|
}
|
||||||
// Hardware path: receiver samples the IOSurface directly through
|
// Payloadless hardware frames carry only the IOSurface selector.
|
||||||
// its CVPixelBuffer cache, so the raw RGBA payload is dead
|
let drop_rgba_payload =
|
||||||
// weight. Drop it from the wire (and zero the byte count in the
|
outcome.response.current_surface_id.is_some() && outcome.frame.is_none();
|
||||||
// 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();
|
|
||||||
if drop_rgba_payload && let Some(report) = outcome.response.frame.as_mut() {
|
if drop_rgba_payload && let Some(report) = outcome.response.frame.as_mut() {
|
||||||
report.rgba_byte_count = 0;
|
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 {
|
pub fn from_report(report: LiveFrameReport, partial_timings: PartialFrameTimings) -> Self {
|
||||||
Self {
|
Self {
|
||||||
response: LiveResponse::frame(report),
|
response: LiveResponse::frame(report),
|
||||||
@@ -204,6 +204,31 @@ impl LiveFrameReport {
|
|||||||
sample_hash: frame.sample_hash(),
|
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) {
|
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")]
|
#[error("servo rendered frame is unavailable")]
|
||||||
RenderedFrameUnavailable,
|
RenderedFrameUnavailable,
|
||||||
|
|
||||||
|
#[error("servo hardware surface is unavailable for {id}")]
|
||||||
|
HardwareSurfaceUnavailable { id: WebViewId },
|
||||||
|
|
||||||
#[error("servo screenshot capture timed out for {id}")]
|
#[error("servo screenshot capture timed out for {id}")]
|
||||||
ScreenshotTimedOut { id: WebViewId },
|
ScreenshotTimedOut { id: WebViewId },
|
||||||
|
|
||||||
|
|||||||
@@ -65,13 +65,6 @@ impl SoftwareServoHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construct the host with an explicit [`RenderingContextKind`].
|
/// 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(
|
pub fn new_with_config_dir_and_kind(
|
||||||
size: ServoSurfaceSize,
|
size: ServoSurfaceSize,
|
||||||
config_dir: Option<PathBuf>,
|
config_dir: Option<PathBuf>,
|
||||||
@@ -106,12 +99,17 @@ impl SoftwareServoHost {
|
|||||||
self.create_webview_in_context(tab_id, profile_id, size)
|
self.create_webview_in_context(tab_id, profile_id, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint and present the webview's current surface while leaving
|
/// Paint and present the current surface without RGBA readback.
|
||||||
/// framebuffer readback to callers that explicitly need RGBA
|
|
||||||
/// 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> {
|
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 {
|
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();
|
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)?;
|
||||||
rendering_context.prepare_for_rendering();
|
rendering_context.prepare_for_rendering();
|
||||||
// `webview.paint()` dispatches a render command to Servo's paint
|
// Clear the pending-frame flag before `paint()` so barrier callers observe
|
||||||
// thread — it does NOT block until the framebuffer is consistent.
|
// the next Servo frame-ready notification for this paint.
|
||||||
// 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.
|
|
||||||
{
|
{
|
||||||
let webview = self.webview(webview_id)?;
|
let webview = self.webview(webview_id)?;
|
||||||
webview.delegate.mark_frame_presented();
|
webview.delegate.mark_frame_presented();
|
||||||
@@ -228,12 +218,7 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
// replacing the about:blank WebView so CSS viewport = physical surface / DPR.
|
// replacing the about:blank WebView so CSS viewport = physical surface / DPR.
|
||||||
.hidpi_scale_factor(hidpi_scale_factor)
|
.hidpi_scale_factor(hidpi_scale_factor)
|
||||||
.build();
|
.build();
|
||||||
// Cosmetic: makes the freshly built WebView paint its
|
// The input-accepting invariant lives in `webview_for_input`.
|
||||||
// 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.
|
|
||||||
webview.webview.show();
|
webview.webview.show();
|
||||||
webview.webview.focus();
|
webview.webview.focus();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
//! Manually-invoked sidecar perf bench. Runs the live loop, scrolls a
|
//! Manual sidecar perf bench; ignored by normal CI.
|
||||||
//! tall page over N frames, and harvests the [`FramePerfSummary`]
|
|
||||||
//! entries the sidecar emits every window. Invoked once per kind:
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! ELY_PERF_KIND=software ELY_PERF_FRAMES=240 \
|
|
||||||
//! cargo test -p ely_servo_host --release --test live_perf_bench \
|
|
||||||
//! --features servo-engine -- --ignored --nocapture run_live_bench
|
|
||||||
//!
|
|
||||||
//! ELY_PERF_KIND=hardware ELY_PERF_FRAMES=240 \
|
|
||||||
//! cargo test -p ely_servo_host --release --test live_perf_bench \
|
|
||||||
//! --features servo-engine,hardware-render -- --ignored --nocapture run_live_bench
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Marked `#[ignore]` so normal CI skips it; it shells out to the
|
|
||||||
//! sidecar binary and runs for tens of seconds.
|
|
||||||
#![cfg(feature = "servo-engine")]
|
#![cfg(feature = "servo-engine")]
|
||||||
|
|
||||||
|
#[path = "live_perf_bench/pixels.rs"]
|
||||||
|
mod pixels;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
error::Error,
|
error::Error,
|
||||||
@@ -43,18 +31,6 @@ div.row{height:80px;border-bottom:2px solid rgba(0,0,0,.5);color:white;font:24px
|
|||||||
</style>\
|
</style>\
|
||||||
<script>for(let i=0;i<100;i++){let d=document.createElement('div');d.className='row';d.textContent='row '+i;document.body.appendChild(d)}</script>";
|
<script>for(let i=0;i<100;i++){let d=document.createElement('div');d.className='row';d.textContent='row '+i;document.body.appendChild(d)}</script>";
|
||||||
|
|
||||||
/// Solid red page used by the pixel-content sanity test. If the
|
|
||||||
/// sidecar's paint barrier (T15) does its job, every pixel of the
|
|
||||||
/// viewport reads back as approximately (255, 0, 0, 255) in Servo's
|
|
||||||
/// gl::RGBA byte order. If the framebuffer is still being read before
|
|
||||||
/// Servo paints, every byte is 255 (the initial clear-to-white state)
|
|
||||||
/// and the assertion catches it.
|
|
||||||
const SOLID_RED_DATA_URL: &str =
|
|
||||||
"data:text/html,<body style=\"margin:0;background:%23ff0000;height:4000px\">";
|
|
||||||
|
|
||||||
const SOLID_BLUE_DATA_URL: &str =
|
|
||||||
"data:text/html,<body style=\"margin:0;background:%230000ff;height:4000px\">";
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct LiveResponse {
|
struct LiveResponse {
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
@@ -161,8 +137,12 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
|||||||
);
|
);
|
||||||
if kind == "hardware" {
|
if kind == "hardware" {
|
||||||
assert!(
|
assert!(
|
||||||
outcome.surface_handles.is_empty() && outcome.current_surface_ids.is_empty(),
|
!outcome.surface_handles.is_empty(),
|
||||||
"hardware live path uses bounded readback and must skip IOSurface selection"
|
"hardware live path must publish IOSurface handles"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!outcome.current_surface_ids.is_empty(),
|
||||||
|
"hardware live path must report current_surface_id selectors"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -175,12 +155,19 @@ fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let viewport_bytes = (1024u64) * (768u64) * 4;
|
let viewport_bytes = (1024u64) * (768u64) * 4;
|
||||||
|
let total_rgba_bytes = outcome.readback_rgba_bytes + outcome.surface_rgba_bytes;
|
||||||
assert!(
|
assert!(
|
||||||
outcome.readback_rgba_bytes >= viewport_bytes,
|
total_rgba_bytes >= viewport_bytes,
|
||||||
"{kind} path delivered only {} bytes — expected at least one full frame ({})",
|
"{kind} path delivered only {total_rgba_bytes} bytes — expected at least one full frame ({})",
|
||||||
outcome.readback_rgba_bytes,
|
|
||||||
viewport_bytes,
|
viewport_bytes,
|
||||||
);
|
);
|
||||||
|
if kind == "hardware" {
|
||||||
|
let full_readback_budget = viewport_bytes * u64::from(frames);
|
||||||
|
assert!(
|
||||||
|
total_rgba_bytes < full_readback_budget,
|
||||||
|
"hardware path stayed on full readback: {total_rgba_bytes} >= {full_readback_budget}"
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,10 +269,10 @@ fn record_rgba_bytes(
|
|||||||
surface_rgba_bytes: &mut u64,
|
surface_rgba_bytes: &mut u64,
|
||||||
) {
|
) {
|
||||||
let rgba_byte_count = response.frame.as_ref().map_or(0, |frame| frame.rgba_byte_count as u64);
|
let rgba_byte_count = response.frame.as_ref().map_or(0, |frame| frame.rgba_byte_count as u64);
|
||||||
if response.current_surface_id.is_some() {
|
if rgba_byte_count > 0 {
|
||||||
*surface_rgba_bytes += rgba_byte_count;
|
|
||||||
} else {
|
|
||||||
*readback_rgba_bytes += rgba_byte_count;
|
*readback_rgba_bytes += rgba_byte_count;
|
||||||
|
} else if response.current_surface_id.is_some() {
|
||||||
|
*surface_rgba_bytes += rgba_byte_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,25 +461,9 @@ fn record_summary(response: &LiveResponse, kind: &str, summaries: &mut Vec<Frame
|
|||||||
|
|
||||||
fn print_summaries(kind: &str, frames: u32, summaries: &[FramePerfSummary]) {
|
fn print_summaries(kind: &str, frames: u32, summaries: &[FramePerfSummary]) {
|
||||||
eprintln!("\n=== ELY_PERF_KIND={kind} frames={frames} windows={} ===", summaries.len());
|
eprintln!("\n=== ELY_PERF_KIND={kind} frames={frames} windows={} ===", summaries.len());
|
||||||
eprintln!(
|
|
||||||
"{:<8} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
|
|
||||||
"win",
|
|
||||||
"paint50",
|
|
||||||
"paint95",
|
|
||||||
"paint99",
|
|
||||||
"enc50",
|
|
||||||
"enc95",
|
|
||||||
"enc99",
|
|
||||||
"wr50",
|
|
||||||
"wr95",
|
|
||||||
"wr99",
|
|
||||||
"tot50",
|
|
||||||
"tot95",
|
|
||||||
"tot99",
|
|
||||||
);
|
|
||||||
for summary in summaries {
|
for summary in summaries {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{:<8} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}",
|
"win={} paint={}/{}/{} encode={}/{}/{} write={}/{}/{} total={}/{}/{}",
|
||||||
summary.window,
|
summary.window,
|
||||||
summary.paint_p50_us,
|
summary.paint_p50_us,
|
||||||
summary.paint_p95_us,
|
summary.paint_p95_us,
|
||||||
@@ -517,178 +488,3 @@ fn cleanup(profile_data_dir: &PathBuf) -> Result<(), Box<dyn Error>> {
|
|||||||
Err(error) => Err(error.into()),
|
Err(error) => Err(error.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// End-to-end pixel-content test. Drives the sidecar with a solid-red
|
|
||||||
/// HTML page, reads the frame off the wire, samples a handful of
|
|
||||||
/// pixels from the centre of the viewport, and asserts the RGBA
|
|
||||||
/// matches red. Catches three regressions in one shot:
|
|
||||||
/// * T15 paint barrier — if `read_to_image` runs before Servo
|
|
||||||
/// paints, every pixel is the framebuffer's clear-to-white state
|
|
||||||
/// `(255, 255, 255, 255)` and the red assertion fires.
|
|
||||||
/// * T13 hidpi — if the viewport is mis-scaled, the body might not
|
|
||||||
/// fill the canvas and the centre pixel would sample whatever's
|
|
||||||
/// outside.
|
|
||||||
/// * General pipeline rot — confirms `build_ensure` + JSON wire +
|
|
||||||
/// RGBA byte stream still delivers the bytes Servo painted.
|
|
||||||
#[test]
|
|
||||||
#[ignore = "drives a real sidecar via stdin/stdout; takes a few seconds"]
|
|
||||||
fn red_data_url_yields_red_rgba() -> Result<(), Box<dyn Error>> {
|
|
||||||
assert_solid_color_renders("software", SOLID_RED_DATA_URL, ColorTarget::Red)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "drives a real sidecar via stdin/stdout; takes a few seconds"]
|
|
||||||
fn blue_data_url_yields_blue_rgba() -> Result<(), Box<dyn Error>> {
|
|
||||||
assert_solid_color_renders("software", SOLID_BLUE_DATA_URL, ColorTarget::Blue)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
enum ColorTarget {
|
|
||||||
Red,
|
|
||||||
Blue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorTarget {
|
|
||||||
fn label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ColorTarget::Red => "red",
|
|
||||||
ColorTarget::Blue => "blue",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_solid_color_renders(
|
|
||||||
kind: &str,
|
|
||||||
url: &str,
|
|
||||||
target: ColorTarget,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
let profile_id = ProfileId::new();
|
|
||||||
let tab = TabId::new();
|
|
||||||
let profile_data_dir = env::temp_dir().join(format!(
|
|
||||||
"ely-pixel-{}-{}-{}",
|
|
||||||
std::process::id(),
|
|
||||||
target.label(),
|
|
||||||
profile_id.as_str(),
|
|
||||||
));
|
|
||||||
fs::create_dir_all(&profile_data_dir)?;
|
|
||||||
|
|
||||||
let mut child = spawn_sidecar(kind, &profile_data_dir)?;
|
|
||||||
let mut stdin = child.stdin.take().ok_or("sidecar stdin missing")?;
|
|
||||||
let stdout = child.stdout.take().ok_or("sidecar stdout missing")?;
|
|
||||||
let mut reader = BufReader::new(stdout);
|
|
||||||
|
|
||||||
let outcome = drive_solid_color_render(&mut stdin, &mut reader, &tab, &profile_id, url, target);
|
|
||||||
|
|
||||||
drop(stdin);
|
|
||||||
let _ = child.wait();
|
|
||||||
cleanup(&profile_data_dir)?;
|
|
||||||
|
|
||||||
outcome
|
|
||||||
}
|
|
||||||
|
|
||||||
fn drive_solid_color_render(
|
|
||||||
stdin: &mut ChildStdin,
|
|
||||||
reader: &mut BufReader<ChildStdout>,
|
|
||||||
tab: &TabId,
|
|
||||||
profile_id: &ProfileId,
|
|
||||||
url: &str,
|
|
||||||
target: ColorTarget,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
// The navigate response itself is the one most likely to carry
|
|
||||||
// real pixels — the sidecar's `awaiting_visible_frame` is armed
|
|
||||||
// on a new URL and `poll_frame` will wait inside its own budget
|
|
||||||
// for Servo to paint. Send navigate, capture the bytes, then
|
|
||||||
// drive scroll iterations to give Servo additional repaint
|
|
||||||
// opportunities. The first matching frame wins.
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
let mut report = None;
|
|
||||||
for iteration in 0..30 {
|
|
||||||
let scroll_y = if iteration == 0 {
|
|
||||||
0
|
|
||||||
} else if iteration % 2 == 1 {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
};
|
|
||||||
let request = build_ensure(tab, profile_id, url, 0, scroll_y, false);
|
|
||||||
write_request(stdin, &request)?;
|
|
||||||
let (response, response_bytes) = read_response_with_bytes(reader, RESPONSE_TIMEOUT)?;
|
|
||||||
if let Some(error) = response.error.as_ref() {
|
|
||||||
return Err(format!("sidecar error: {error}").into());
|
|
||||||
}
|
|
||||||
if let Some(frame_report) = response.frame {
|
|
||||||
if !response_bytes.is_empty()
|
|
||||||
&& sample_matches_target(
|
|
||||||
&response_bytes,
|
|
||||||
frame_report.width,
|
|
||||||
frame_report.height,
|
|
||||||
target,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
report = Some(frame_report);
|
|
||||||
bytes = response_bytes;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if !response_bytes.is_empty() {
|
|
||||||
bytes = response_bytes;
|
|
||||||
report = Some(frame_report);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let report = report.ok_or("never received a frame with bytes")?;
|
|
||||||
let width = report.width as usize;
|
|
||||||
let height = report.height as usize;
|
|
||||||
assert_eq!(bytes.len(), width * height * 4, "rgba byte count must match width × height × 4",);
|
|
||||||
|
|
||||||
// Sample 9 evenly-spaced points in the inner quartile of the
|
|
||||||
// viewport. Solid backgrounds should pass every sample; if Servo
|
|
||||||
// is still painting initial-white we'll see (255, 255, 255, 255)
|
|
||||||
// across the grid and the per-pixel asserts will explain.
|
|
||||||
let mut samples = Vec::new();
|
|
||||||
for fy in [1, 2, 3] {
|
|
||||||
for fx in [1, 2, 3] {
|
|
||||||
let x = width * fx / 4;
|
|
||||||
let y = height * fy / 4;
|
|
||||||
let idx = (y * width + x) * 4;
|
|
||||||
samples.push((x, y, bytes[idx], bytes[idx + 1], bytes[idx + 2], bytes[idx + 3]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eprintln!("[pixel sample {}] {:?}", target.label(), samples);
|
|
||||||
|
|
||||||
let mut hits = 0;
|
|
||||||
for (_x, _y, r, g, b, _a) in &samples {
|
|
||||||
if matches_color(*r, *g, *b, target) {
|
|
||||||
hits += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
hits >= 5,
|
|
||||||
"expected ≥5/9 centre-quadrant pixels to be {} after rendering {}; got samples {:?}",
|
|
||||||
target.label(),
|
|
||||||
url,
|
|
||||||
samples,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_matches_target(bytes: &[u8], width: u32, height: u32, target: ColorTarget) -> bool {
|
|
||||||
let w = width as usize;
|
|
||||||
let h = height as usize;
|
|
||||||
if bytes.len() < w * h * 4 || w == 0 || h == 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let cx = w / 2;
|
|
||||||
let cy = h / 2;
|
|
||||||
let idx = (cy * w + cx) * 4;
|
|
||||||
matches_color(bytes[idx], bytes[idx + 1], bytes[idx + 2], target)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn matches_color(r: u8, g: u8, b: u8, target: ColorTarget) -> bool {
|
|
||||||
match target {
|
|
||||||
ColorTarget::Red => r >= 200 && g <= 60 && b <= 60,
|
|
||||||
ColorTarget::Blue => r <= 60 && g <= 60 && b >= 200,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
error::Error,
|
||||||
|
fs,
|
||||||
|
io::BufReader,
|
||||||
|
process::{ChildStdin, ChildStdout},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ely_domain::{ProfileId, TabId};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
RESPONSE_TIMEOUT, build_ensure, cleanup, read_response_with_bytes, spawn_sidecar, write_request,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOLID_RED_DATA_URL: &str =
|
||||||
|
"data:text/html,<body style=\"margin:0;background:%23ff0000;height:4000px\">";
|
||||||
|
const SOLID_BLUE_DATA_URL: &str =
|
||||||
|
"data:text/html,<body style=\"margin:0;background:%230000ff;height:4000px\">";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "drives a real sidecar via stdin/stdout; takes a few seconds"]
|
||||||
|
fn red_data_url_yields_red_rgba() -> Result<(), Box<dyn Error>> {
|
||||||
|
assert_solid_color_renders("software", SOLID_RED_DATA_URL, ColorTarget::Red)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "drives a real sidecar via stdin/stdout; takes a few seconds"]
|
||||||
|
fn blue_data_url_yields_blue_rgba() -> Result<(), Box<dyn Error>> {
|
||||||
|
assert_solid_color_renders("software", SOLID_BLUE_DATA_URL, ColorTarget::Blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum ColorTarget {
|
||||||
|
Red,
|
||||||
|
Blue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorTarget {
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ColorTarget::Red => "red",
|
||||||
|
ColorTarget::Blue => "blue",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_solid_color_renders(
|
||||||
|
kind: &str,
|
||||||
|
url: &str,
|
||||||
|
target: ColorTarget,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let profile_id = ProfileId::new();
|
||||||
|
let tab = TabId::new();
|
||||||
|
let profile_data_dir = env::temp_dir().join(format!(
|
||||||
|
"ely-pixel-{}-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
target.label(),
|
||||||
|
profile_id.as_str(),
|
||||||
|
));
|
||||||
|
fs::create_dir_all(&profile_data_dir)?;
|
||||||
|
|
||||||
|
let mut child = spawn_sidecar(kind, &profile_data_dir)?;
|
||||||
|
let mut stdin = child.stdin.take().ok_or("sidecar stdin missing")?;
|
||||||
|
let stdout = child.stdout.take().ok_or("sidecar stdout missing")?;
|
||||||
|
let mut reader = BufReader::new(stdout);
|
||||||
|
let outcome = drive_solid_color_render(&mut stdin, &mut reader, &tab, &profile_id, url, target);
|
||||||
|
|
||||||
|
drop(stdin);
|
||||||
|
let _ = child.wait();
|
||||||
|
cleanup(&profile_data_dir)?;
|
||||||
|
outcome
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drive_solid_color_render(
|
||||||
|
stdin: &mut ChildStdin,
|
||||||
|
reader: &mut BufReader<ChildStdout>,
|
||||||
|
tab: &TabId,
|
||||||
|
profile_id: &ProfileId,
|
||||||
|
url: &str,
|
||||||
|
target: ColorTarget,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
let mut report = None;
|
||||||
|
for iteration in 0..30 {
|
||||||
|
let scroll_y = if iteration == 0 {
|
||||||
|
0
|
||||||
|
} else if iteration % 2 == 1 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
};
|
||||||
|
let request = build_ensure(tab, profile_id, url, 0, scroll_y, false);
|
||||||
|
write_request(stdin, &request)?;
|
||||||
|
let (response, response_bytes) = read_response_with_bytes(reader, RESPONSE_TIMEOUT)?;
|
||||||
|
if let Some(error) = response.error.as_ref() {
|
||||||
|
return Err(format!("sidecar error: {error}").into());
|
||||||
|
}
|
||||||
|
if let Some(frame_report) = response.frame {
|
||||||
|
if !response_bytes.is_empty()
|
||||||
|
&& sample_matches_target(
|
||||||
|
&response_bytes,
|
||||||
|
frame_report.width,
|
||||||
|
frame_report.height,
|
||||||
|
target,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
report = Some(frame_report);
|
||||||
|
bytes = response_bytes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !response_bytes.is_empty() {
|
||||||
|
bytes = response_bytes;
|
||||||
|
report = Some(frame_report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let report = report.ok_or("never received a frame with bytes")?;
|
||||||
|
let width = report.width as usize;
|
||||||
|
let height = report.height as usize;
|
||||||
|
assert_eq!(bytes.len(), width * height * 4, "rgba byte count must match width * height * 4",);
|
||||||
|
|
||||||
|
let mut samples = Vec::new();
|
||||||
|
for fy in [1, 2, 3] {
|
||||||
|
for fx in [1, 2, 3] {
|
||||||
|
let x = width * fx / 4;
|
||||||
|
let y = height * fy / 4;
|
||||||
|
let idx = (y * width + x) * 4;
|
||||||
|
samples.push((x, y, bytes[idx], bytes[idx + 1], bytes[idx + 2], bytes[idx + 3]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!("[pixel sample {}] {:?}", target.label(), samples);
|
||||||
|
|
||||||
|
let hits =
|
||||||
|
samples.iter().filter(|(_x, _y, r, g, b, _a)| matches_color(*r, *g, *b, target)).count();
|
||||||
|
assert!(
|
||||||
|
hits >= 5,
|
||||||
|
"expected at least 5/9 center-quadrant pixels to be {} after rendering {}; got samples {:?}",
|
||||||
|
target.label(),
|
||||||
|
url,
|
||||||
|
samples,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_matches_target(bytes: &[u8], width: u32, height: u32, target: ColorTarget) -> bool {
|
||||||
|
let w = width as usize;
|
||||||
|
let h = height as usize;
|
||||||
|
if bytes.len() < w * h * 4 || w == 0 || h == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let cx = w / 2;
|
||||||
|
let cy = h / 2;
|
||||||
|
let idx = (cy * w + cx) * 4;
|
||||||
|
matches_color(bytes[idx], bytes[idx + 1], bytes[idx + 2], target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_color(r: u8, g: u8, b: u8, target: ColorTarget) -> bool {
|
||||||
|
match target {
|
||||||
|
ColorTarget::Red => r >= 200 && g <= 60 && b <= 60,
|
||||||
|
ColorTarget::Blue => r <= 60 && g <= 60 && b >= 200,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user