update
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
//! Smoke test for the vendored hardware [`RenderingContext`].
|
||||
//!
|
||||
//! Runs only when the `hardware-render` feature is enabled. The test
|
||||
//! degrades gracefully when the host machine lacks a hardware GL
|
||||
//! adapter (CI sandboxes, no-GPU containers): construction returns
|
||||
//! `Err`, the test logs the cause, and reports `ok` — proving the
|
||||
//! vendored constructor is wired up correctly without falsely
|
||||
//! marking the suite green when a GPU is actually expected and
|
||||
//! missing. Inverting that check (turning a GPU-missing host into a
|
||||
//! hard failure) is left for downstream CI configuration once the
|
||||
//! hardware path is wired into the sidecar binary.
|
||||
|
||||
#![cfg(feature = "hardware-render")]
|
||||
|
||||
use dpi::PhysicalSize;
|
||||
use ely_servo_host::HardwareOffscreenContext;
|
||||
use servo::RenderingContext;
|
||||
|
||||
#[test]
|
||||
fn constructs_or_explains_why_not() {
|
||||
let size = PhysicalSize::new(640, 480);
|
||||
match HardwareOffscreenContext::new(size) {
|
||||
Ok(context) => {
|
||||
// We don't drive Servo here — just confirm the vendored
|
||||
// glue produced a live context. Construction is the
|
||||
// failure mode this smoke test guards against; once a
|
||||
// context exists the real Servo paint path exercises the
|
||||
// rest of the surface.
|
||||
drop(context);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"hardware GL adapter not available on this host \
|
||||
(acceptable in headless / no-GPU environments): {error:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn extracts_iosurface_mach_port_from_current_surface() -> Result<(), String> {
|
||||
let width = 256;
|
||||
let height = 192;
|
||||
let context = match HardwareOffscreenContext::new(PhysicalSize::new(width, height)) {
|
||||
Ok(context) => context,
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"hardware GL adapter not available on this host \
|
||||
(acceptable in headless / no-GPU environments): {error:?}"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
context.prepare_for_rendering();
|
||||
context.present();
|
||||
let first = context
|
||||
.current_iosurface_mach_port()
|
||||
.map_err(|error| format!("first IOSurface mach port extraction failed: {error:?}"))?;
|
||||
assert!(
|
||||
first.mach_port_name != 0,
|
||||
"IOSurfaceCreateMachPort must return a non-null mach_port_t (got 0)"
|
||||
);
|
||||
assert_eq!(first.width, width, "reported width must match surface width");
|
||||
assert_eq!(first.height, height, "reported height must match surface height");
|
||||
|
||||
// The unbind/rebind cycle must leave the context usable: a second
|
||||
// call should still produce a valid mach port without panicking on
|
||||
// a stale `Framebuffer::None`.
|
||||
let second = context
|
||||
.current_iosurface_mach_port()
|
||||
.map_err(|error| format!("repeated mach port extraction failed: {error:?}"))?;
|
||||
assert!(second.mach_port_name != 0);
|
||||
assert_eq!(second.width, width);
|
||||
assert_eq!(second.height, height);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,494 +0,0 @@
|
||||
//! Manual sidecar perf bench; ignored by normal CI.
|
||||
#![cfg(feature = "servo-engine")]
|
||||
|
||||
#[path = "live_perf_bench/pixels.rs"]
|
||||
mod pixels;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
fs,
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
path::PathBuf,
|
||||
process::{Child, ChildStdin, ChildStdout, Command, Stdio},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ely_domain::{ProfileId, TabId};
|
||||
use serde::Deserialize;
|
||||
|
||||
const DEFAULT_FRAMES: u32 = 240;
|
||||
const VIEWPORT_WIDTH: u32 = 1024;
|
||||
const VIEWPORT_HEIGHT: u32 = 768;
|
||||
const SCROLL_STEP_PX: i32 = 4;
|
||||
const RESPONSE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
const SCROLL_PAGE_DATA_URL: &str = "data:text/html,\
|
||||
<!doctype html><meta charset=utf-8><title>perf</title>\
|
||||
<style>html,body{margin:0;padding:0}\
|
||||
body{height:8000px;background:linear-gradient(180deg,red,teal,navy,white,crimson)}\
|
||||
div.row{height:80px;border-bottom:2px solid rgba(0,0,0,.5);color:white;font:24px/80px sans-serif;padding-left:24px}\
|
||||
</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>";
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct LiveResponse {
|
||||
error: Option<String>,
|
||||
frame: Option<LiveFrameReport>,
|
||||
#[serde(default)]
|
||||
perf: Option<FramePerfSummary>,
|
||||
#[serde(default)]
|
||||
surface_handle: Option<BenchSurfaceHandle>,
|
||||
#[serde(default)]
|
||||
current_surface_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy)]
|
||||
struct BenchSurfaceHandle {
|
||||
mach_port_name: u32,
|
||||
surface_id: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct LiveFrameReport {
|
||||
rgba_byte_count: usize,
|
||||
#[serde(default)]
|
||||
width: u32,
|
||||
#[serde(default)]
|
||||
height: u32,
|
||||
#[serde(default)]
|
||||
device_pixel_ratio: f32,
|
||||
#[serde(default)]
|
||||
css_viewport_width: u32,
|
||||
#[serde(default)]
|
||||
css_viewport_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct FramePerfSummary {
|
||||
window: u32,
|
||||
context: String,
|
||||
paint_p50_us: u64,
|
||||
paint_p95_us: u64,
|
||||
paint_p99_us: u64,
|
||||
encode_p50_us: u64,
|
||||
encode_p95_us: u64,
|
||||
encode_p99_us: u64,
|
||||
write_p50_us: u64,
|
||||
write_p95_us: u64,
|
||||
write_p99_us: u64,
|
||||
total_p50_us: u64,
|
||||
total_p95_us: u64,
|
||||
total_p99_us: u64,
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual bench: spawns sidecar, scrolls a data: URL for N frames"]
|
||||
fn run_live_bench() -> Result<(), Box<dyn Error>> {
|
||||
let kind = env::var("ELY_PERF_KIND").unwrap_or_else(|_| "software".to_string());
|
||||
let frames: u32 = env::var("ELY_PERF_FRAMES")
|
||||
.ok()
|
||||
.and_then(|value| value.parse().ok())
|
||||
.unwrap_or(DEFAULT_FRAMES);
|
||||
let url = env::var("ELY_PERF_URL").unwrap_or_else(|_| SCROLL_PAGE_DATA_URL.to_string());
|
||||
|
||||
let profile_id = ProfileId::new();
|
||||
let tab = TabId::new();
|
||||
let profile_data_dir = env::temp_dir().join(format!(
|
||||
"ely-perf-bench-{}-{}-{}",
|
||||
std::process::id(),
|
||||
kind,
|
||||
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 = match drive_bench(&mut stdin, &mut reader, &kind, &tab, &profile_id, &url, frames)
|
||||
{
|
||||
Ok(outcome) => outcome,
|
||||
Err(error) => {
|
||||
drop(stdin);
|
||||
let _ = child.kill();
|
||||
cleanup(&profile_data_dir)?;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
drop(stdin);
|
||||
let _ = child.wait();
|
||||
cleanup(&profile_data_dir)?;
|
||||
|
||||
print_summaries(&kind, frames, &outcome.summaries);
|
||||
print_surface_handles(&kind, &outcome.surface_handles);
|
||||
print_current_surface_summary(&kind, &outcome.current_surface_ids);
|
||||
eprintln!(
|
||||
"\n=== ELY_PERF_KIND={kind} readback_rgba_bytes={} surface_rgba_bytes={} ===",
|
||||
outcome.readback_rgba_bytes, outcome.surface_rgba_bytes,
|
||||
);
|
||||
assert!(
|
||||
!outcome.summaries.is_empty(),
|
||||
"expected at least one FramePerfSummary across {frames} frames"
|
||||
);
|
||||
if kind == "hardware" {
|
||||
assert!(
|
||||
!outcome.surface_handles.is_empty(),
|
||||
"hardware live path must publish IOSurface handles"
|
||||
);
|
||||
assert!(
|
||||
!outcome.current_surface_ids.is_empty(),
|
||||
"hardware live path must report current_surface_id selectors"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
outcome.surface_handles.is_empty(),
|
||||
"software path must never publish an IOSurface handle"
|
||||
);
|
||||
assert!(
|
||||
outcome.current_surface_ids.is_empty(),
|
||||
"software path must never report current_surface_id"
|
||||
);
|
||||
}
|
||||
let viewport_bytes = (1024u64) * (768u64) * 4;
|
||||
let total_rgba_bytes = outcome.readback_rgba_bytes + outcome.surface_rgba_bytes;
|
||||
assert!(
|
||||
total_rgba_bytes >= viewport_bytes,
|
||||
"{kind} path delivered only {total_rgba_bytes} bytes — expected at least one full frame ({})",
|
||||
viewport_bytes,
|
||||
);
|
||||
if kind == "hardware" {
|
||||
let full_readback_budget = viewport_bytes * u64::from(frames);
|
||||
assert_eq!(
|
||||
outcome.readback_rgba_bytes, viewport_bytes,
|
||||
"hardware path should read back only the initial visible frame"
|
||||
);
|
||||
assert!(
|
||||
total_rgba_bytes < full_readback_budget,
|
||||
"hardware path stayed on full readback: {total_rgba_bytes} >= {full_readback_budget}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct BenchOutcome {
|
||||
summaries: Vec<FramePerfSummary>,
|
||||
surface_handles: Vec<BenchSurfaceHandle>,
|
||||
current_surface_ids: Vec<u64>,
|
||||
readback_rgba_bytes: u64,
|
||||
surface_rgba_bytes: u64,
|
||||
}
|
||||
|
||||
fn spawn_sidecar(kind: &str, profile_data_dir: &PathBuf) -> Result<Child, Box<dyn Error>> {
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_ely_servo_sidecar"));
|
||||
command
|
||||
.arg("live")
|
||||
.arg("--profile-data-dir")
|
||||
.arg(profile_data_dir)
|
||||
.arg("--rendering-context")
|
||||
.arg(kind)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit());
|
||||
Ok(command.spawn()?)
|
||||
}
|
||||
|
||||
fn drive_bench(
|
||||
stdin: &mut ChildStdin,
|
||||
reader: &mut BufReader<ChildStdout>,
|
||||
kind: &str,
|
||||
tab: &TabId,
|
||||
profile_id: &ProfileId,
|
||||
url: &str,
|
||||
frames: u32,
|
||||
) -> Result<BenchOutcome, Box<dyn Error>> {
|
||||
let mut summaries = Vec::new();
|
||||
let mut surface_handles = Vec::new();
|
||||
let mut current_surface_ids = Vec::new();
|
||||
let mut readback_rgba_bytes: u64 = 0;
|
||||
let mut surface_rgba_bytes: u64 = 0;
|
||||
|
||||
let navigate = build_ensure(tab, profile_id, url, 0, 0, false);
|
||||
write_request(stdin, &navigate)?;
|
||||
let response = read_response(reader, RESPONSE_TIMEOUT)?;
|
||||
assert_frame_viewport_report(&response);
|
||||
record_summary(&response, kind, &mut summaries);
|
||||
record_surface_handle(&response, kind, &mut surface_handles);
|
||||
record_current_surface_id(&response, &mut current_surface_ids);
|
||||
record_rgba_bytes(&response, &mut readback_rgba_bytes, &mut surface_rgba_bytes);
|
||||
|
||||
let mut accumulated_scroll = 0;
|
||||
let mut painted_frames = 0;
|
||||
let max_attempts = frames.saturating_mul(4).max(frames + 10);
|
||||
for attempt in 0..max_attempts {
|
||||
if painted_frames >= frames {
|
||||
break;
|
||||
}
|
||||
let scroll_delta_y =
|
||||
if painted_frames % 80 == 79 { -SCROLL_STEP_PX * 60 } else { SCROLL_STEP_PX };
|
||||
accumulated_scroll += scroll_delta_y;
|
||||
let request = build_ensure(tab, profile_id, url, 0, scroll_delta_y, true);
|
||||
write_request(stdin, &request)?;
|
||||
let response = read_response(reader, RESPONSE_TIMEOUT)?;
|
||||
if let Some(error) = response.error.as_ref() {
|
||||
return Err(format!("sidecar error at attempt {attempt}: {error}").into());
|
||||
}
|
||||
if response.frame.is_some() {
|
||||
painted_frames += 1;
|
||||
}
|
||||
assert_frame_viewport_report(&response);
|
||||
record_summary(&response, kind, &mut summaries);
|
||||
record_surface_handle(&response, kind, &mut surface_handles);
|
||||
record_current_surface_id(&response, &mut current_surface_ids);
|
||||
record_rgba_bytes(&response, &mut readback_rgba_bytes, &mut surface_rgba_bytes);
|
||||
}
|
||||
assert_eq!(painted_frames, frames, "bench did not receive the requested painted frame count");
|
||||
let _ = accumulated_scroll;
|
||||
for _ in 0..5 {
|
||||
if !summaries.is_empty() {
|
||||
break;
|
||||
}
|
||||
let poll = build_poll(tab);
|
||||
write_request(stdin, &poll)?;
|
||||
let response = read_response(reader, RESPONSE_TIMEOUT)?;
|
||||
record_summary(&response, kind, &mut summaries);
|
||||
}
|
||||
|
||||
Ok(BenchOutcome {
|
||||
summaries,
|
||||
surface_handles,
|
||||
current_surface_ids,
|
||||
readback_rgba_bytes,
|
||||
surface_rgba_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn record_rgba_bytes(
|
||||
response: &LiveResponse,
|
||||
readback_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);
|
||||
if rgba_byte_count > 0 {
|
||||
*readback_rgba_bytes += rgba_byte_count;
|
||||
} else if response.current_surface_id.is_some() {
|
||||
*surface_rgba_bytes += rgba_byte_count;
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_frame_viewport_report(response: &LiveResponse) {
|
||||
let Some(frame) = response.frame.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let dpr = if frame.device_pixel_ratio.is_finite() && frame.device_pixel_ratio > 0.0 {
|
||||
frame.device_pixel_ratio
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let expected_width = ((frame.width as f32) / dpr).round().max(1.0) as u32;
|
||||
let expected_height = ((frame.height as f32) / dpr).round().max(1.0) as u32;
|
||||
|
||||
assert_eq!(
|
||||
frame.css_viewport_width, expected_width,
|
||||
"CSS viewport width must match physical width divided by DPR",
|
||||
);
|
||||
assert_eq!(
|
||||
frame.css_viewport_height, expected_height,
|
||||
"CSS viewport height must match physical height divided by DPR",
|
||||
);
|
||||
}
|
||||
|
||||
fn record_surface_handle(
|
||||
response: &LiveResponse,
|
||||
kind: &str,
|
||||
surface_handles: &mut Vec<BenchSurfaceHandle>,
|
||||
) {
|
||||
if let Some(handle) = response.surface_handle {
|
||||
eprintln!(
|
||||
"[iosurface {kind}] new surface_id=0x{:x} mach_port=0x{:x} {}x{}",
|
||||
handle.surface_id, handle.mach_port_name, handle.width, handle.height,
|
||||
);
|
||||
surface_handles.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
fn record_current_surface_id(response: &LiveResponse, current_surface_ids: &mut Vec<u64>) {
|
||||
if let Some(surface_id) = response.current_surface_id {
|
||||
current_surface_ids.push(surface_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_surface_handles(kind: &str, surface_handles: &[BenchSurfaceHandle]) {
|
||||
eprintln!(
|
||||
"\n=== ELY_PERF_KIND={kind} iosurface_imports={} (one per unique surface) ===",
|
||||
surface_handles.len()
|
||||
);
|
||||
for (index, handle) in surface_handles.iter().enumerate() {
|
||||
eprintln!(
|
||||
"{:<4} surface_id=0x{:x} mach_port=0x{:x} {}x{}",
|
||||
index, handle.surface_id, handle.mach_port_name, handle.width, handle.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_current_surface_summary(kind: &str, current_surface_ids: &[u64]) {
|
||||
use std::collections::BTreeMap;
|
||||
let mut counts: BTreeMap<u64, u32> = BTreeMap::new();
|
||||
for id in current_surface_ids {
|
||||
*counts.entry(*id).or_default() += 1;
|
||||
}
|
||||
eprintln!("\n=== ELY_PERF_KIND={kind} current_surface_id histogram (per-frame selector) ===",);
|
||||
for (surface_id, count) in counts.iter() {
|
||||
eprintln!("surface_id=0x{:x} frames={}", surface_id, count);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ensure(
|
||||
tab: &TabId,
|
||||
profile_id: &ProfileId,
|
||||
url: &str,
|
||||
scroll_dx: i32,
|
||||
scroll_dy: i32,
|
||||
include_hover: bool,
|
||||
) -> String {
|
||||
let hover_x = if include_hover { Some(256u32) } else { None };
|
||||
let hover_y = if include_hover { Some(256u32) } else { None };
|
||||
let scroll_point = if scroll_dx != 0 || scroll_dy != 0 { Some((256u32, 256u32)) } else { None };
|
||||
let hover_x_json = match hover_x {
|
||||
Some(value) => format!("{value}"),
|
||||
None => "null".to_string(),
|
||||
};
|
||||
let hover_y_json = match hover_y {
|
||||
Some(value) => format!("{value}"),
|
||||
None => "null".to_string(),
|
||||
};
|
||||
let scroll_point_x_json = match scroll_point {
|
||||
Some((x, _)) => format!("{x}"),
|
||||
None => "null".to_string(),
|
||||
};
|
||||
let scroll_point_y_json = match scroll_point {
|
||||
Some((_, y)) => format!("{y}"),
|
||||
None => "null".to_string(),
|
||||
};
|
||||
format!(
|
||||
r#"{{"type":"ensure","tab_id":"{tab}","profile_id":"{profile}","url":{url},"width":{w},"height":{h},"page_zoom_percent":100,"scroll_delta_x":{dx},"scroll_delta_y":{dy},"scroll_point_x":{sx},"scroll_point_y":{sy},"click_x":null,"click_y":null,"hover_x":{hx},"hover_y":{hy},"typed_text":null,"site_permissions":[]}}"#,
|
||||
tab = tab.as_str(),
|
||||
profile = profile_id.as_str(),
|
||||
url = serde_json::to_string(url).unwrap_or_else(|_| "\"\"".to_string()),
|
||||
w = VIEWPORT_WIDTH,
|
||||
h = VIEWPORT_HEIGHT,
|
||||
dx = scroll_dx,
|
||||
dy = scroll_dy,
|
||||
sx = scroll_point_x_json,
|
||||
sy = scroll_point_y_json,
|
||||
hx = hover_x_json,
|
||||
hy = hover_y_json,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_poll(tab: &TabId) -> String {
|
||||
format!(r#"{{"type":"poll","tab_id":"{}"}}"#, tab.as_str())
|
||||
}
|
||||
|
||||
fn write_request(stdin: &mut ChildStdin, request: &str) -> Result<(), Box<dyn Error>> {
|
||||
stdin.write_all(request.as_bytes())?;
|
||||
stdin.write_all(b"\n")?;
|
||||
stdin.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_response(
|
||||
reader: &mut BufReader<ChildStdout>,
|
||||
timeout: Duration,
|
||||
) -> Result<LiveResponse, Box<dyn Error>> {
|
||||
Ok(read_response_with_bytes(reader, timeout)?.0)
|
||||
}
|
||||
|
||||
fn read_response_with_bytes(
|
||||
reader: &mut BufReader<ChildStdout>,
|
||||
timeout: Duration,
|
||||
) -> Result<(LiveResponse, Vec<u8>), Box<dyn Error>> {
|
||||
let started_at = Instant::now();
|
||||
let mut json_line = String::new();
|
||||
loop {
|
||||
json_line.clear();
|
||||
let read_bytes = reader.read_line(&mut json_line)?;
|
||||
if read_bytes == 0 {
|
||||
return Err("sidecar closed stdout".into());
|
||||
}
|
||||
if json_line.trim().is_empty() {
|
||||
if started_at.elapsed() >= timeout {
|
||||
return Err("sidecar response timeout".into());
|
||||
}
|
||||
thread::sleep(Duration::from_millis(2));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let response: LiveResponse = serde_json::from_str(json_line.trim_end())?;
|
||||
let mut rgba = Vec::new();
|
||||
if let Some(frame) = response.frame.as_ref()
|
||||
&& frame.rgba_byte_count > 0
|
||||
{
|
||||
rgba.resize(frame.rgba_byte_count, 0);
|
||||
reader.read_exact(&mut rgba)?;
|
||||
}
|
||||
Ok((response, rgba))
|
||||
}
|
||||
|
||||
fn record_summary(response: &LiveResponse, kind: &str, summaries: &mut Vec<FramePerfSummary>) {
|
||||
if let Some(perf) = response.perf.as_ref() {
|
||||
assert_eq!(perf.context, kind, "sidecar context label must match requested kind");
|
||||
eprintln!(
|
||||
"[perf {kind}] window={} paint p50/p95/p99={}/{}/{} encode {}/{}/{} write {}/{}/{} total {}/{}/{} (µs)",
|
||||
perf.window,
|
||||
perf.paint_p50_us,
|
||||
perf.paint_p95_us,
|
||||
perf.paint_p99_us,
|
||||
perf.encode_p50_us,
|
||||
perf.encode_p95_us,
|
||||
perf.encode_p99_us,
|
||||
perf.write_p50_us,
|
||||
perf.write_p95_us,
|
||||
perf.write_p99_us,
|
||||
perf.total_p50_us,
|
||||
perf.total_p95_us,
|
||||
perf.total_p99_us,
|
||||
);
|
||||
summaries.push(perf.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn print_summaries(kind: &str, frames: u32, summaries: &[FramePerfSummary]) {
|
||||
eprintln!("\n=== ELY_PERF_KIND={kind} frames={frames} windows={} ===", summaries.len());
|
||||
for summary in summaries {
|
||||
eprintln!(
|
||||
"win={} paint={}/{}/{} encode={}/{}/{} write={}/{}/{} total={}/{}/{}",
|
||||
summary.window,
|
||||
summary.paint_p50_us,
|
||||
summary.paint_p95_us,
|
||||
summary.paint_p99_us,
|
||||
summary.encode_p50_us,
|
||||
summary.encode_p95_us,
|
||||
summary.encode_p99_us,
|
||||
summary.write_p50_us,
|
||||
summary.write_p95_us,
|
||||
summary.write_p99_us,
|
||||
summary.total_p50_us,
|
||||
summary.total_p95_us,
|
||||
summary.total_p99_us,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(profile_data_dir: &PathBuf) -> Result<(), Box<dyn Error>> {
|
||||
match fs::remove_dir_all(profile_data_dir) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
#![cfg(feature = "servo-engine")]
|
||||
|
||||
use std::{collections::BTreeSet, error::Error, fs, path::PathBuf, process::Command};
|
||||
|
||||
use ely_domain::ProfileId;
|
||||
|
||||
#[path = "sidecar/site_cases.rs"]
|
||||
mod site_cases;
|
||||
#[path = "sidecar/support.rs"]
|
||||
mod support;
|
||||
|
||||
use support::*;
|
||||
|
||||
#[test]
|
||||
fn sidecar_prd_reference_cases_cover_prd_urls() -> Result<(), Box<dyn Error>> {
|
||||
let prd = fs::read_to_string(prd_path())?;
|
||||
let prd_urls = prd_reference_urls(&prd);
|
||||
let covered_urls = PRD_REFERENCE_SITE_COMPATIBILITY_CASES
|
||||
.iter()
|
||||
.map(|case| normalized_url(case.url))
|
||||
.collect::<BTreeSet<_>>();
|
||||
let missing_urls = prd_urls
|
||||
.iter()
|
||||
.filter(|url| !covered_urls.contains(url.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(missing_urls.is_empty(), "missing PRD sidecar smoke cases: {missing_urls:?}");
|
||||
assert_eq!(prd_urls.len(), PRD_REFERENCE_SITE_COMPATIBILITY_CASES.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_opens_and_renders_prd_sites_to_rgba_files() -> Result<(), Box<dyn Error>> {
|
||||
for case in PRD_SITE_COMPATIBILITY_CASES {
|
||||
for size in PRD_SITE_COMPATIBILITY_SIZES {
|
||||
snapshot_prd_site(case, *size, ScrollOffset::ZERO)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_report_uses_requested_profile_id() -> Result<(), Box<dyn Error>> {
|
||||
let profile_id = ProfileId::new();
|
||||
let profile_data_dir = std::env::temp_dir().join(format!(
|
||||
"ely-servo-sidecar-profile-test-{}-{}",
|
||||
std::process::id(),
|
||||
profile_id.as_str()
|
||||
));
|
||||
let rgba_path = std::env::temp_dir().join(format!(
|
||||
"ely-servo-sidecar-profile-test-{}-{}.rgba",
|
||||
std::process::id(),
|
||||
profile_id.as_str()
|
||||
));
|
||||
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_ely_servo_sidecar"))
|
||||
.arg("snapshot")
|
||||
.arg("--url")
|
||||
.arg("data:text/html,%3Ctitle%3EProfile%20Probe%3C%2Ftitle%3EProfile%20Probe")
|
||||
.arg("--profile-id")
|
||||
.arg(profile_id.as_str())
|
||||
.arg("--profile-data-dir")
|
||||
.arg(&profile_data_dir)
|
||||
.arg("--rgba-out")
|
||||
.arg(&rgba_path)
|
||||
.arg("--width")
|
||||
.arg("64")
|
||||
.arg("--height")
|
||||
.arg("64")
|
||||
.output()?;
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let report: serde_json::Value = serde_json::from_slice(&output.stdout)?;
|
||||
assert_eq!(
|
||||
report.get("profile_id").and_then(serde_json::Value::as_str),
|
||||
Some(profile_id.as_str())
|
||||
);
|
||||
|
||||
remove_file_if_present(rgba_path)?;
|
||||
remove_dir_if_present(profile_data_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prd_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("PRD.md")
|
||||
}
|
||||
|
||||
fn remove_file_if_present(path: PathBuf) -> Result<(), Box<dyn Error>> {
|
||||
match fs::remove_file(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_dir_if_present(path: PathBuf) -> Result<(), Box<dyn Error>> {
|
||||
match fs::remove_dir_all(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn prd_reference_urls(prd: &str) -> Vec<String> {
|
||||
prd.lines()
|
||||
.filter(|line| line.starts_with("[R"))
|
||||
.filter_map(|line| {
|
||||
let start = line.find("https://")?;
|
||||
let url = line[start..].split_whitespace().next()?;
|
||||
Some(normalized_url(url))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalized_url(url: &str) -> String {
|
||||
url.trim().trim_end_matches('/').to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_opens_and_renders_prd_reference_sites_to_rgba_files() -> Result<(), Box<dyn Error>> {
|
||||
for case in PRD_REFERENCE_SITE_COMPATIBILITY_CASES {
|
||||
snapshot_prd_site(case, PRD_REFERENCE_SITE_SIZE, ScrollOffset::ZERO)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_scrolls_prd_site_with_servo_input() -> Result<(), Box<dyn Error>> {
|
||||
let scrolled_report =
|
||||
snapshot_prd_site(&SERVO_SCROLL_SITE, SERVO_SCROLL_SIZE, SERVO_SCROLL_OFFSET)?;
|
||||
|
||||
assert_eq!(report_field_as_i64(&scrolled_report, "scroll_x")?, SERVO_SCROLL_OFFSET.x);
|
||||
assert_eq!(report_field_as_i64(&scrolled_report, "scroll_y")?, SERVO_SCROLL_OFFSET.y);
|
||||
assert_eq!(report_field_as_u64(&scrolled_report, "width")?, SERVO_SCROLL_SIZE.width);
|
||||
assert!(report_field_as_bool(&scrolled_report, "scroll_changed_frame")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_clicks_page_with_servo_mouse_input() -> Result<(), Box<dyn Error>> {
|
||||
let initial_report = snapshot_click_probe(None)?;
|
||||
let clicked_report = snapshot_click_probe(Some(SERVO_CLICK_POINT))?;
|
||||
|
||||
assert_eq!(report_field_as_u64(&clicked_report, "click_x")?, SERVO_CLICK_POINT.x);
|
||||
assert_eq!(report_field_as_u64(&clicked_report, "click_y")?, SERVO_CLICK_POINT.y);
|
||||
assert!(report_field_as_bool(&clicked_report, "click_changed_frame")?);
|
||||
assert_ne!(
|
||||
report_field_as_u64(&initial_report, "sample_hash")?,
|
||||
report_field_as_u64(&clicked_report, "sample_hash")?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_drags_page_with_servo_mouse_input() -> Result<(), Box<dyn Error>> {
|
||||
let initial_report = snapshot_drag_probe(None)?;
|
||||
let drag_points = DragPoints { from: SERVO_DRAG_FROM, to: SERVO_DRAG_TO };
|
||||
let dragged_report = snapshot_drag_probe(Some(drag_points))?;
|
||||
|
||||
assert_eq!(report_field_as_u64(&dragged_report, "drag_from_x")?, SERVO_DRAG_FROM.x);
|
||||
assert_eq!(report_field_as_u64(&dragged_report, "drag_from_y")?, SERVO_DRAG_FROM.y);
|
||||
assert_eq!(report_field_as_u64(&dragged_report, "drag_to_x")?, SERVO_DRAG_TO.x);
|
||||
assert_eq!(report_field_as_u64(&dragged_report, "drag_to_y")?, SERVO_DRAG_TO.y);
|
||||
assert!(report_field_as_bool(&dragged_report, "drag_changed_frame")?);
|
||||
assert_ne!(
|
||||
report_field_as_u64(&initial_report, "sample_hash")?,
|
||||
report_field_as_u64(&dragged_report, "sample_hash")?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_touches_page_with_servo_touch_input() -> Result<(), Box<dyn Error>> {
|
||||
let initial_report = snapshot_touch_probe(None)?;
|
||||
let touched_report = snapshot_touch_probe(Some(SERVO_TOUCH_POINT))?;
|
||||
|
||||
assert_eq!(report_field_as_u64(&touched_report, "touch_x")?, SERVO_TOUCH_POINT.x);
|
||||
assert_eq!(report_field_as_u64(&touched_report, "touch_y")?, SERVO_TOUCH_POINT.y);
|
||||
assert!(report_field_as_bool(&touched_report, "touch_changed_frame")?);
|
||||
assert_ne!(
|
||||
report_field_as_u64(&initial_report, "sample_hash")?,
|
||||
report_field_as_u64(&touched_report, "sample_hash")?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidecar_types_text_with_servo_keyboard_input() -> Result<(), Box<dyn Error>> {
|
||||
let initial_report = snapshot_text_probe(None)?;
|
||||
let typed_report = snapshot_text_probe(Some(SERVO_TEXT_VALUE))?;
|
||||
|
||||
assert_eq!(
|
||||
report_field_as_u64(&typed_report, "typed_text_byte_count")?,
|
||||
SERVO_TEXT_VALUE.len() as u64
|
||||
);
|
||||
assert!(report_field_as_bool(&typed_report, "text_changed_frame")?);
|
||||
assert_ne!(
|
||||
report_field_as_u64(&initial_report, "sample_hash")?,
|
||||
report_field_as_u64(&typed_report, "sample_hash")?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
pub(super) const PRD_SITE_COMPATIBILITY_CASES: &[PrdSiteCompatibilityCase] = &[
|
||||
PrdSiteCompatibilityCase { url: "https://github.com", title_fragment: "GitHub" },
|
||||
PrdSiteCompatibilityCase { url: "https://example.com", title_fragment: "Example Domain" },
|
||||
PrdSiteCompatibilityCase { url: "https://servo.org/", title_fragment: "Servo" },
|
||||
];
|
||||
|
||||
pub(super) const PRD_REFERENCE_SITE_COMPATIBILITY_CASES: &[PrdSiteCompatibilityCase] = &[
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://blog.google/products-and-platforms/products/chrome/new-chrome-productivity-features/",
|
||||
title_fragment: "Chrome",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://www.microsoft.com/en-us/edge/features/vertical-tabs",
|
||||
title_fragment: "Microsoft Edge",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://resources.arc.net/hc/en-us/articles/19230755904151-Favorites-Top-Tabs-Across-Every-Space",
|
||||
title_fragment: "Favorites",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://resources.arc.net/hc/en-us/articles/19228855311127-Auto-Archive-Clean-as-you-go",
|
||||
title_fragment: "Auto Archive",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://vivaldi.com/features/workspaces/",
|
||||
title_fragment: "Workspaces",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://help.vivaldi.com/desktop/tabs/tab-tiling/",
|
||||
title_fragment: "Tab Tiling",
|
||||
},
|
||||
PrdSiteCompatibilityCase { url: "https://www.gpui.rs/", title_fragment: "gpui" },
|
||||
PrdSiteCompatibilityCase { url: "https://docs.rs/gpui", title_fragment: "gpui" },
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://zed.dev/blog/videogame",
|
||||
title_fragment: "Leveraging Rust",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://github.com/longbridge/gpui-component/",
|
||||
title_fragment: "gpui-component",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://github.com/zed-industries/awesome-gpui/",
|
||||
title_fragment: "awesome-gpui",
|
||||
},
|
||||
PrdSiteCompatibilityCase { url: "https://servo.org/", title_fragment: "Servo" },
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://servo.org/blog/2026/04/13/servo-0.1.0-release/",
|
||||
title_fragment: "Servo",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://developers.cloudflare.com/d1/",
|
||||
title_fragment: "Cloudflare",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://developers.cloudflare.com/workers/platform/storage-options/",
|
||||
title_fragment: "Cloudflare",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://developers.cloudflare.com/kv/concepts/how-kv-works/",
|
||||
title_fragment: "Cloudflare",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://better-auth.com/blog/1-5",
|
||||
title_fragment: "Better Auth",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://developers.cloudflare.com/d1/platform/limits/",
|
||||
title_fragment: "Cloudflare",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://component-model.bytecodealliance.org/",
|
||||
title_fragment: "WebAssembly Component Model",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://docs.wasmtime.dev/api/wasmtime/component/index.html",
|
||||
title_fragment: "wasmtime",
|
||||
},
|
||||
PrdSiteCompatibilityCase {
|
||||
url: "https://docs.wasmtime.dev/security.html",
|
||||
title_fragment: "Wasmtime",
|
||||
},
|
||||
];
|
||||
|
||||
pub(super) const PRD_SITE_COMPATIBILITY_SIZES: &[FrameSize] = &[
|
||||
FrameSize { width: 640, height: 480 },
|
||||
FrameSize { width: 934, height: 657 },
|
||||
FrameSize { width: 1614, height: 980 },
|
||||
];
|
||||
pub(super) const PRD_REFERENCE_SITE_SIZE: FrameSize = FrameSize { width: 934, height: 657 };
|
||||
pub(super) const SERVO_SCROLL_SITE: PrdSiteCompatibilityCase =
|
||||
PrdSiteCompatibilityCase { url: "https://servo.org/", title_fragment: "Servo" };
|
||||
pub(super) const SERVO_SCROLL_SIZE: FrameSize = FrameSize { width: 934, height: 657 };
|
||||
pub(super) const SERVO_SCROLL_OFFSET: ScrollOffset = ScrollOffset { x: 0, y: 480 };
|
||||
pub(super) const SERVO_CLICK_URL: &str = "data:text/html,%3C!doctype%20html%3E%3Ctitle%3EClick%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f7f7f7%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3B%7D%3C%2Fstyle%3E%3Cbutton%20onclick%3D%22document.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Clicked%27%3Bthis.textContent%3D%27Clicked%27%3B%22%3ETap%3C%2Fbutton%3E";
|
||||
pub(super) const SERVO_CLICK_SIZE: FrameSize = FrameSize { width: 640, height: 480 };
|
||||
pub(super) const SERVO_CLICK_POINT: ClickPoint = ClickPoint { x: 160, y: 120 };
|
||||
pub(super) const SERVO_DRAG_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3EDrag%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f7f7f7%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3B%7D%3C%2Fstyle%3E%3Cbutton%20id%3Dbox%3EDrag%3C%2Fbutton%3E%3Cscript%3Elet%20dragging%3Dfalse%3Bconst%20box%3Ddocument.getElementById%28%27box%27%29%3BaddEventListener%28%27mousedown%27%2Cevent%3D%3E%7Bif%28event.target%3D%3D%3Dbox%29%7Bdragging%3Dtrue%3B%7D%7D%29%3BaddEventListener%28%27mousemove%27%2Cevent%3D%3E%7Bif%28dragging%26%26event.clientX%3E280%29%7Bdocument.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Dragged%27%3Bbox.textContent%3D%27Dragged%27%3B%7D%7D%29%3BaddEventListener%28%27mouseup%27%2C%28%29%3D%3E%7Bdragging%3Dfalse%3B%7D%29%3B%3C%2Fscript%3E";
|
||||
pub(super) const SERVO_DRAG_SIZE: FrameSize = FrameSize { width: 640, height: 480 };
|
||||
pub(super) const SERVO_DRAG_FROM: ClickPoint = ClickPoint { x: 160, y: 120 };
|
||||
pub(super) const SERVO_DRAG_TO: ClickPoint = ClickPoint { x: 320, y: 120 };
|
||||
pub(super) const SERVO_TOUCH_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3ETouch%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f7f7f7%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3Btouch-action%3Amanipulation%3B%7D%3C%2Fstyle%3E%3Cbutton%20ontouchstart%3D%22document.body.dataset.touch%3D%27start%27%3B%22%20onclick%3D%22document.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Touched%27%3Bthis.textContent%3D%27Touched%27%3B%22%3ETap%3C%2Fbutton%3E";
|
||||
pub(super) const SERVO_TOUCH_SIZE: FrameSize = FrameSize { width: 640, height: 480 };
|
||||
pub(super) const SERVO_TOUCH_POINT: ClickPoint = ClickPoint { x: 160, y: 120 };
|
||||
pub(super) const SERVO_TEXT_URL: &str = "data:text/html,%3C!doctype%20html%3E%3Ctitle%3EText%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f7f7f7%3Bfont%3A28px%20sans-serif%3B%7Dinput%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A260px%3Bheight%3A70px%3Bfont%3A28px%20sans-serif%3B%7Doutput%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A180px%3Bfont%3A32px%20sans-serif%3B%7D%3C%2Fstyle%3E%3Cinput%20id%3Dq%20autofocus%20oninput%3D%22document.body.style.background%3D%27%230039ff%27%3Bdocument.getElementById%28%27out%27%29.textContent%3Dthis.value%3B%22%3E%3Coutput%20id%3Dout%3Eempty%3C%2Foutput%3E";
|
||||
pub(super) const SERVO_TEXT_SIZE: FrameSize = FrameSize { width: 640, height: 480 };
|
||||
pub(super) const SERVO_TEXT_POINT: ClickPoint = ClickPoint { x: 160, y: 120 };
|
||||
pub(super) const SERVO_TEXT_VALUE: &str = "ely42";
|
||||
|
||||
pub(super) struct PrdSiteCompatibilityCase {
|
||||
pub(super) url: &'static str,
|
||||
pub(super) title_fragment: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct FrameSize {
|
||||
pub(super) width: u64,
|
||||
pub(super) height: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct ScrollOffset {
|
||||
pub(super) x: i64,
|
||||
pub(super) y: i64,
|
||||
}
|
||||
|
||||
impl ScrollOffset {
|
||||
pub(super) const ZERO: Self = Self { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct ClickPoint {
|
||||
pub(super) x: u64,
|
||||
pub(super) y: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct DragPoints {
|
||||
pub(super) from: ClickPoint,
|
||||
pub(super) to: ClickPoint,
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command, Output, Stdio},
|
||||
sync::Mutex,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ely_domain::ProfileId;
|
||||
|
||||
pub(super) use super::site_cases::{
|
||||
ClickPoint, DragPoints, FrameSize, PRD_REFERENCE_SITE_COMPATIBILITY_CASES,
|
||||
PRD_REFERENCE_SITE_SIZE, PRD_SITE_COMPATIBILITY_CASES, PRD_SITE_COMPATIBILITY_SIZES,
|
||||
PrdSiteCompatibilityCase, SERVO_CLICK_POINT, SERVO_DRAG_FROM, SERVO_DRAG_TO,
|
||||
SERVO_SCROLL_OFFSET, SERVO_SCROLL_SITE, SERVO_SCROLL_SIZE, SERVO_TEXT_VALUE, SERVO_TOUCH_POINT,
|
||||
ScrollOffset,
|
||||
};
|
||||
use super::site_cases::{
|
||||
SERVO_CLICK_SIZE, SERVO_CLICK_URL, SERVO_DRAG_SIZE, SERVO_DRAG_URL, SERVO_TEXT_POINT,
|
||||
SERVO_TEXT_SIZE, SERVO_TEXT_URL, SERVO_TOUCH_SIZE, SERVO_TOUCH_URL,
|
||||
};
|
||||
|
||||
pub(super) const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
|
||||
const SIDECAR_TIMEOUT: Duration = Duration::from_secs(45);
|
||||
const SIDECAR_POLL_INTERVAL: Duration = Duration::from_millis(20);
|
||||
const SIDECAR_COMMAND_COOLDOWN: Duration = Duration::from_millis(750);
|
||||
const SIDECAR_RETRY_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const SIDECAR_MAX_ATTEMPTS: usize = 3;
|
||||
static SIDECAR_COMMAND_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct SnapshotInput<'a> {
|
||||
click_point: Option<ClickPoint>,
|
||||
drag_points: Option<DragPoints>,
|
||||
touch_point: Option<ClickPoint>,
|
||||
typed_text: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_prd_site(
|
||||
case: &PrdSiteCompatibilityCase,
|
||||
size: FrameSize,
|
||||
scroll_offset: ScrollOffset,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
let site_name = case
|
||||
.url
|
||||
.chars()
|
||||
.map(|character| if character.is_ascii_alphanumeric() { character } else { '-' })
|
||||
.collect::<String>();
|
||||
let output_path = std::env::temp_dir().join(format!(
|
||||
"ely-servo-sidecar-{}-{site_name}-{}x{}-{}-{}.rgba",
|
||||
std::process::id(),
|
||||
size.width,
|
||||
size.height,
|
||||
scroll_offset.x,
|
||||
scroll_offset.y
|
||||
));
|
||||
|
||||
snapshot_prd_site_with_retry(case, &output_path, size, scroll_offset)
|
||||
}
|
||||
|
||||
fn snapshot_prd_site_with_retry(
|
||||
case: &PrdSiteCompatibilityCase,
|
||||
output_path: &Path,
|
||||
size: FrameSize,
|
||||
scroll_offset: ScrollOffset,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
for attempt in 0..SIDECAR_MAX_ATTEMPTS {
|
||||
match snapshot_prd_site_once(case, output_path, size, scroll_offset) {
|
||||
Ok(report) => return Ok(report),
|
||||
Err(error) if attempt + 1 == SIDECAR_MAX_ATTEMPTS => return Err(error),
|
||||
Err(_) => remove_file_if_present(output_path)?,
|
||||
}
|
||||
|
||||
thread::sleep(SIDECAR_RETRY_INTERVAL);
|
||||
}
|
||||
|
||||
Err("sidecar PRD snapshot retry did not produce output".into())
|
||||
}
|
||||
|
||||
fn snapshot_prd_site_once(
|
||||
case: &PrdSiteCompatibilityCase,
|
||||
output_path: &Path,
|
||||
size: FrameSize,
|
||||
scroll_offset: ScrollOffset,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
let output = run_sidecar_snapshot_with_retry(
|
||||
case.url,
|
||||
output_path,
|
||||
size,
|
||||
scroll_offset,
|
||||
SnapshotInput::default(),
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {}x{}\nstatus: {:?}\nstdout: {}\nstderr: {}",
|
||||
case.url,
|
||||
size.width,
|
||||
size.height,
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let report: serde_json::Value = serde_json::from_slice(&output.stdout)?;
|
||||
assert_report_state_is_renderable(&report)?;
|
||||
assert_eq!(report_field_as_u64(&report, "width")?, size.width, "{}", case.url);
|
||||
assert_eq!(report_field_as_u64(&report, "height")?, size.height, "{}", case.url);
|
||||
assert_eq!(
|
||||
report_field_as_u64(&report, "rgba_byte_count")?,
|
||||
size.width * size.height * 4,
|
||||
"{}",
|
||||
case.url
|
||||
);
|
||||
assert_report_text_contains(&report, "loaded_url", case.url)?;
|
||||
assert_report_text_equals(&report, "requested_url", case.url)?;
|
||||
assert_report_text_contains(&report, "title", case.title_fragment)?;
|
||||
assert!(report_field_as_u64(&report, "non_white_pixel_count")? > 0, "{}", case.url);
|
||||
assert!(
|
||||
report_field_as_u64(&report, "content_pixel_count")? >= MINIMUM_CONTENT_PIXELS,
|
||||
"{}",
|
||||
case.url
|
||||
);
|
||||
assert!(report_field_as_u64(&report, "sample_hash")? > 0, "{}", case.url);
|
||||
assert_eq!(std::fs::metadata(output_path)?.len(), size.width * size.height * 4);
|
||||
|
||||
log_prd_report(&report, case, size)?;
|
||||
|
||||
std::fs::remove_file(output_path)?;
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
fn remove_file_if_present(path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_click_probe(
|
||||
click_point: Option<ClickPoint>,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
snapshot_probe(
|
||||
SERVO_CLICK_URL,
|
||||
SERVO_CLICK_SIZE,
|
||||
"click",
|
||||
SnapshotInput { click_point, ..SnapshotInput::default() },
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_drag_probe(
|
||||
drag_points: Option<DragPoints>,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
snapshot_probe(
|
||||
SERVO_DRAG_URL,
|
||||
SERVO_DRAG_SIZE,
|
||||
"drag",
|
||||
SnapshotInput { drag_points, ..SnapshotInput::default() },
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_touch_probe(
|
||||
touch_point: Option<ClickPoint>,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
snapshot_probe(
|
||||
SERVO_TOUCH_URL,
|
||||
SERVO_TOUCH_SIZE,
|
||||
"touch",
|
||||
SnapshotInput { touch_point, ..SnapshotInput::default() },
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_text_probe(
|
||||
typed_text: Option<&str>,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
snapshot_probe(
|
||||
SERVO_TEXT_URL,
|
||||
SERVO_TEXT_SIZE,
|
||||
"text",
|
||||
SnapshotInput {
|
||||
click_point: typed_text.map(|_| SERVO_TEXT_POINT),
|
||||
typed_text,
|
||||
..SnapshotInput::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn snapshot_probe(
|
||||
url: &str,
|
||||
size: FrameSize,
|
||||
label: &'static str,
|
||||
input: SnapshotInput<'_>,
|
||||
) -> Result<serde_json::Value, Box<dyn Error>> {
|
||||
let output_path = std::env::temp_dir().join(format!(
|
||||
"ely-servo-sidecar-{}-{label}-{}x{}.rgba",
|
||||
std::process::id(),
|
||||
size.width,
|
||||
size.height
|
||||
));
|
||||
|
||||
if output_path.exists() {
|
||||
std::fs::remove_file(&output_path)?;
|
||||
}
|
||||
|
||||
let output =
|
||||
run_sidecar_snapshot_with_retry(url, &output_path, size, ScrollOffset::ZERO, input)?;
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{label} probe\nstatus: {:?}\nstdout: {}\nstderr: {}",
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let report: serde_json::Value = serde_json::from_slice(&output.stdout)?;
|
||||
assert_eq!(report_field_as_u64(&report, "width")?, size.width);
|
||||
assert_eq!(report_field_as_u64(&report, "height")?, size.height);
|
||||
assert!(report_field_as_u64(&report, "content_pixel_count")? > 0);
|
||||
assert_eq!(std::fs::metadata(&output_path)?.len(), size.width * size.height * 4);
|
||||
|
||||
std::fs::remove_file(&output_path)?;
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
fn run_sidecar_snapshot(
|
||||
site_url: &str,
|
||||
output_path: &Path,
|
||||
size: FrameSize,
|
||||
scroll_offset: ScrollOffset,
|
||||
input: SnapshotInput<'_>,
|
||||
) -> Result<Output, Box<dyn Error>> {
|
||||
let _guard = SIDECAR_COMMAND_LOCK
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("sidecar command lock poisoned"))?;
|
||||
let profile_id = ProfileId::new();
|
||||
let profile_data_dir = temporary_profile_data_dir(&profile_id);
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_ely_servo_sidecar"));
|
||||
command
|
||||
.arg("snapshot")
|
||||
.arg("--url")
|
||||
.arg(site_url)
|
||||
.arg("--profile-id")
|
||||
.arg(profile_id.as_str())
|
||||
.arg("--profile-data-dir")
|
||||
.arg(&profile_data_dir)
|
||||
.arg("--rgba-out")
|
||||
.arg(output_path)
|
||||
.arg("--width")
|
||||
.arg(size.width.to_string())
|
||||
.arg("--height")
|
||||
.arg(size.height.to_string());
|
||||
if scroll_offset.x != 0 {
|
||||
command.arg("--scroll-x").arg(scroll_offset.x.to_string());
|
||||
}
|
||||
if scroll_offset.y != 0 {
|
||||
command.arg("--scroll-y").arg(scroll_offset.y.to_string());
|
||||
}
|
||||
if let Some(click_point) = input.click_point {
|
||||
command.arg("--click-x").arg(click_point.x.to_string());
|
||||
command.arg("--click-y").arg(click_point.y.to_string());
|
||||
}
|
||||
if let Some(drag_points) = input.drag_points {
|
||||
command.arg("--drag-from-x").arg(drag_points.from.x.to_string());
|
||||
command.arg("--drag-from-y").arg(drag_points.from.y.to_string());
|
||||
command.arg("--drag-to-x").arg(drag_points.to.x.to_string());
|
||||
command.arg("--drag-to-y").arg(drag_points.to.y.to_string());
|
||||
}
|
||||
if let Some(touch_point) = input.touch_point {
|
||||
command.arg("--touch-x").arg(touch_point.x.to_string());
|
||||
command.arg("--touch-y").arg(touch_point.y.to_string());
|
||||
}
|
||||
if let Some(typed_text) = input.typed_text {
|
||||
command.arg("--type-text").arg(typed_text);
|
||||
}
|
||||
|
||||
let mut child = command.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
|
||||
let started_at = Instant::now();
|
||||
loop {
|
||||
if child.try_wait()?.is_some() {
|
||||
let output = child.wait_with_output()?;
|
||||
remove_temporary_dir(&profile_data_dir)?;
|
||||
thread::sleep(SIDECAR_COMMAND_COOLDOWN);
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
if started_at.elapsed() >= SIDECAR_TIMEOUT {
|
||||
terminate_child(child)?;
|
||||
remove_temporary_dir(&profile_data_dir)?;
|
||||
thread::sleep(SIDECAR_COMMAND_COOLDOWN);
|
||||
return Err(format!(
|
||||
"timed out rendering {site_url} at {}x{}",
|
||||
size.width, size.height
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
thread::sleep(SIDECAR_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn temporary_profile_data_dir(profile_id: &ProfileId) -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"ely-servo-sidecar-profile-{}-{}",
|
||||
std::process::id(),
|
||||
profile_id.as_str()
|
||||
))
|
||||
}
|
||||
|
||||
fn remove_temporary_dir(path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
match std::fs::remove_dir_all(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_sidecar_snapshot_with_retry(
|
||||
site_url: &str,
|
||||
output_path: &std::path::Path,
|
||||
size: FrameSize,
|
||||
scroll_offset: ScrollOffset,
|
||||
input: SnapshotInput<'_>,
|
||||
) -> Result<Output, Box<dyn Error>> {
|
||||
for attempt in 0..SIDECAR_MAX_ATTEMPTS {
|
||||
if output_path.exists() {
|
||||
std::fs::remove_file(output_path)?;
|
||||
}
|
||||
|
||||
match run_sidecar_snapshot(site_url, output_path, size, scroll_offset, input) {
|
||||
Ok(output) if output.status.success() => return Ok(output),
|
||||
Ok(output) if attempt + 1 == SIDECAR_MAX_ATTEMPTS => return Ok(output),
|
||||
Ok(_output) => {}
|
||||
Err(error) if attempt + 1 == SIDECAR_MAX_ATTEMPTS => return Err(error),
|
||||
Err(_error) => {}
|
||||
}
|
||||
|
||||
thread::sleep(SIDECAR_RETRY_INTERVAL);
|
||||
}
|
||||
|
||||
Err("sidecar snapshot retry did not produce output".into())
|
||||
}
|
||||
|
||||
fn terminate_child(mut child: Child) -> Result<(), Box<dyn Error>> {
|
||||
match child.kill() {
|
||||
Ok(()) => {
|
||||
let _output = child.wait_with_output()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::InvalidInput => Ok(()),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_report_text_equals(
|
||||
report: &serde_json::Value,
|
||||
field: &'static str,
|
||||
expected: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let value = report_field_as_text(report, field)?;
|
||||
if value == expected { Ok(()) } else { Err(format!("{field}: {value}").into()) }
|
||||
}
|
||||
|
||||
fn assert_report_text_contains(
|
||||
report: &serde_json::Value,
|
||||
field: &'static str,
|
||||
fragment: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let value = report_field_as_text(report, field)?;
|
||||
if value.contains(fragment) { Ok(()) } else { Err(format!("{field}: {value}").into()) }
|
||||
}
|
||||
|
||||
fn assert_report_state_is_renderable(report: &serde_json::Value) -> Result<(), Box<dyn Error>> {
|
||||
let state = report_field_as_text(report, "state")?;
|
||||
if matches!(state, "complete" | "loading") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("state: {state}").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn log_prd_report(
|
||||
report: &serde_json::Value,
|
||||
case: &PrdSiteCompatibilityCase,
|
||||
size: FrameSize,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
eprintln!(
|
||||
"prd-live-site servo-sidecar url={} loaded={} title={} state={} size={}x{} content_pixels={} non_white_pixels={} sample_hash={}",
|
||||
case.url,
|
||||
report_field_as_text(report, "loaded_url")?,
|
||||
report_field_as_text(report, "title")?,
|
||||
report_field_as_text(report, "state")?,
|
||||
size.width,
|
||||
size.height,
|
||||
report_field_as_u64(report, "content_pixel_count")?,
|
||||
report_field_as_u64(report, "non_white_pixel_count")?,
|
||||
report_field_as_u64(report, "sample_hash")?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_field_as_text<'a>(
|
||||
report: &'a serde_json::Value,
|
||||
field: &'static str,
|
||||
) -> Result<&'a str, Box<dyn Error>> {
|
||||
report
|
||||
.get(field)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.ok_or_else(|| format!("missing text report field: {field}").into())
|
||||
}
|
||||
|
||||
pub(super) fn report_field_as_bool(
|
||||
report: &serde_json::Value,
|
||||
field: &'static str,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
report
|
||||
.get(field)
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.ok_or_else(|| format!("missing boolean report field: {field}").into())
|
||||
}
|
||||
|
||||
pub(super) fn report_field_as_i64(
|
||||
report: &serde_json::Value,
|
||||
field: &'static str,
|
||||
) -> Result<i64, Box<dyn Error>> {
|
||||
report
|
||||
.get(field)
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.ok_or_else(|| format!("missing signed report field: {field}").into())
|
||||
}
|
||||
|
||||
pub(super) fn report_field_as_u64(
|
||||
report: &serde_json::Value,
|
||||
field: &'static str,
|
||||
) -> Result<u64, Box<dyn Error>> {
|
||||
report
|
||||
.get(field)
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.ok_or_else(|| format!("missing numeric report field: {field}").into())
|
||||
}
|
||||
@@ -11,9 +11,8 @@ use std::{
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText};
|
||||
use ely_servo_host::{
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
||||
PageZoomRequest, PermissionDecision, PermissionRequest, ResizeRequest, ScreenshotRequest,
|
||||
ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest,
|
||||
WebViewState,
|
||||
PageZoomRequest, PermissionDecision, PermissionRequest, ResizeRequest, ScrollRequest,
|
||||
ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, WebViewState,
|
||||
};
|
||||
|
||||
const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
|
||||
@@ -336,17 +335,6 @@ fn exercise_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
||||
site.url
|
||||
);
|
||||
assert_rendered_frame_has_content(&host, site.url, MINIMUM_CONTENT_PIXELS)?;
|
||||
if site.url == "https://example.com" {
|
||||
let screenshot =
|
||||
host.capture_screenshot(ScreenshotRequest { webview_id: webview_id.clone() })?;
|
||||
assert_frame_has_dimensions_and_content(
|
||||
&screenshot,
|
||||
"https://example.com screenshot",
|
||||
INITIAL_WIDTH,
|
||||
INITIAL_HEIGHT,
|
||||
MINIMUM_CONTENT_PIXELS,
|
||||
);
|
||||
}
|
||||
previous_frame_hash = Some(host.last_rendered_frame()?.sample_hash());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user