Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/instant-reliability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,11 @@ jobs:
run: |
set -euo pipefail
sudo apt-get update -qq
# v4l2loopback needs videodev, which is in the extra kernel modules.
sudo apt-get install -y -qq "linux-modules-extra-$(uname -r)" v4l2loopback-dkms xvfb x11-utils xdotool \
if ! sudo modprobe videodev 2>/dev/null; then
sudo apt-get install -y -qq "linux-modules-extra-$(uname -r)"
sudo modprobe videodev
fi
sudo apt-get install -y -qq "linux-headers-$(uname -r)" v4l2loopback-dkms xvfb x11-utils xdotool \
dbus ffmpeg pulseaudio pulseaudio-utils libasound2-plugins >/dev/null
chmod +x "$RUNNER_TEMP/build/cap-gpui" "$RUNNER_TEMP/build/real-app-tests"
sudo modprobe v4l2loopback video_nr=42 card_label="Cap Test Camera" exclusive_caps=1
Expand Down
169 changes: 134 additions & 35 deletions crates/recording/examples/x11-capture-benchmark.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
//! Compares the X11 grabber's MIT-SHM (`ShmGetImage`) and core `GetImage`
//! backends on root-window grabs. Each sample is a full `X11Grabber::grab`,
//! including the row copy into a freshly allocated ffmpeg BGRZ frame.
//!
//! scripts/bench-x11.sh # runs under xvfb-run at 3840x2160x24
//! scripts/bench-x11.sh --json
Expand All @@ -20,7 +17,7 @@ mod linux {
use std::time::Instant;

use anyhow::{Context, anyhow, bail};
use cap_recording::screen_capture::{X11Grabber, X11InputConfig};
use cap_recording::screen_capture::{X11Grabber, X11InputConfig, copy_x11_image_rows};
use clap::Parser;
use x11rb::connection::{Connection, RequestConnection};
use x11rb::protocol::shm::{self, ConnectionExt as _};
Expand All @@ -46,19 +43,23 @@ mod linux {
enum Backend {
GetImage,
ShmGetImage,
ShmRowCopy,
}

impl Backend {
fn name(self) -> &'static str {
match self {
Backend::GetImage => "GetImage",
Backend::ShmGetImage => "ShmGetImage",
Backend::ShmGetImage => "ShmPublish",
Backend::ShmRowCopy => "ShmRowCopy",
}
}
}

struct Stats {
samples: usize,
mean: f64,
median: f64,
p50: f64,
p95: f64,
p99: f64,
Expand All @@ -73,8 +74,16 @@ mod linux {
let rank = ((p / 100.0) * samples_ms.len() as f64).ceil() as usize;
samples_ms[rank.clamp(1, samples_ms.len()) - 1]
};
let mid = samples_ms.len() / 2;
let median = if samples_ms.len().is_multiple_of(2) {
(samples_ms[mid - 1] + samples_ms[mid]) / 2.0
} else {
samples_ms[mid]
};
Self {
samples: samples_ms.len(),
mean,
median,
p50: percentile(50.0),
p95: percentile(95.0),
p99: percentile(99.0),
Expand All @@ -84,7 +93,9 @@ mod linux {

fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"samples": self.samples,
"mean_ms": self.mean,
"median_ms": self.median,
"p50_ms": self.p50,
"p95_ms": self.p95,
"p99_ms": self.p99,
Expand All @@ -98,16 +109,23 @@ mod linux {
height: u16,
get_image: Stats,
shm: Stats,
row_copy: Stats,
publish_only: Stats,
row_copy_only: Stats,
}

impl SizeResult {
fn speedup(&self) -> f64 {
self.get_image.mean / self.shm.mean
}

fn row_copy_savings_ms(&self) -> f64 {
self.row_copy.median - self.shm.median
}
}

fn grabber(width: u16, height: u16, backend: Backend) -> anyhow::Result<X11Grabber> {
let grabber = X11Grabber::with_shm(
let mut grabber = X11Grabber::with_shm(
&X11InputConfig {
display_name: std::env::var("DISPLAY").context("DISPLAY is not set")?,
window_id: None,
Expand All @@ -118,9 +136,12 @@ mod linux {
fps: 30,
show_cursor: false,
},
backend == Backend::ShmGetImage,
backend != Backend::GetImage,
)?;
if (backend == Backend::ShmGetImage) != grabber.uses_shm() {
if backend == Backend::ShmRowCopy {
grabber.force_shm_row_copy(true);
}
if (backend != Backend::GetImage) != grabber.uses_shm() {
bail!(
"{width}x{height}: X11Grabber did not select {}",
backend.name()
Expand Down Expand Up @@ -213,6 +234,60 @@ mod linux {
Ok(samples)
}

fn time_publish(grabber: &mut X11Grabber, args: &Args) -> anyhow::Result<Vec<f64>> {
grabber.grab()?;
for _ in 0..args.warmup {
drop(grabber.publish_idle_shm_frame()?);
}
let mut samples = Vec::with_capacity(args.iters);
for _ in 0..args.iters {
let start = Instant::now();
let frame = grabber.publish_idle_shm_frame()?;
samples.push(start.elapsed().as_secs_f64() * 1000.0);
drop(frame);
}
Ok(samples)
}

fn time_row_copy(width: u16, height: u16, args: &Args) -> anyhow::Result<Vec<f64>> {
let width = usize::from(width);
let height = usize::from(height);
let src = vec![7u8; width * height * 4];
let mut dst =
ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRZ, width as u32, height as u32);
let once = |dst: &mut ffmpeg::frame::Video| {
copy_x11_image_rows(&src, width, height, dst).context("row copy")
};
for _ in 0..args.warmup {
once(&mut dst)?;
}
let mut samples = Vec::with_capacity(args.iters);
for _ in 0..args.iters {
let start = Instant::now();
once(&mut dst)?;
samples.push(start.elapsed().as_secs_f64() * 1000.0);
}
Ok(samples)
}

fn profile_name() -> &'static str {
if cfg!(debug_assertions) {
"dev"
} else {
"release opt-level=s lto=thin codegen-units=1"
}
}

fn rustc_version() -> String {
std::process::Command::new("rustc")
.arg("--version")
.output()
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|version| version.trim().to_string())
.unwrap_or_else(|| "unknown".to_string())
}

pub fn run() -> anyhow::Result<()> {
let args = Args::parse();
if args.iters == 0 {
Expand Down Expand Up @@ -274,28 +349,41 @@ mod linux {

let mut get_image_grabber = grabber(width, height, Backend::GetImage)?;
let mut shm_grabber = grabber(width, height, Backend::ShmGetImage)?;
let mut row_copy_grabber = grabber(width, height, Backend::ShmRowCopy)?;

let reference = frame_rows(&get_image_grabber.grab()?);
if frame_rows(&shm_grabber.grab()?) != reference {
bail!("{width}x{height}: ShmGetImage pixels differ from GetImage");
bail!("{width}x{height}: ShmPublish pixels differ from GetImage");
}
if frame_rows(&row_copy_grabber.grab()?) != reference {
bail!("{width}x{height}: ShmRowCopy pixels differ from GetImage");
}

eprintln!("benchmarking {width}x{height} ...");
let pixels = f64::from(width) * f64::from(height);
let publish_only = Stats::from_samples(time_publish(&mut shm_grabber, &args)?, pixels);
let row_copy_only = Stats::from_samples(time_row_copy(width, height, &args)?, pixels);
let get_image =
Stats::from_samples(time_backend(&mut get_image_grabber, &args)?, pixels);
let shm = Stats::from_samples(time_backend(&mut shm_grabber, &args)?, pixels);
if !shm_grabber.uses_shm() {
let row_copy = Stats::from_samples(time_backend(&mut row_copy_grabber, &args)?, pixels);
if !shm_grabber.uses_shm() || !row_copy_grabber.uses_shm() {
bail!("{width}x{height}: X11Grabber fell back to GetImage mid-run");
}
results.push(SizeResult {
width,
height,
get_image,
shm,
row_copy,
publish_only,
row_copy_only,
});
}

let rustc = rustc_version();
let profile = profile_name();

if args.json {
let rows: Vec<_> = results
.iter()
Expand All @@ -305,12 +393,19 @@ mod linux {
"height": r.height,
Backend::GetImage.name(): r.get_image.to_json(),
Backend::ShmGetImage.name(): r.shm.to_json(),
"speedup": r.speedup(),
Backend::ShmRowCopy.name(): r.row_copy.to_json(),
"publish_only": r.publish_only.to_json(),
"row_copy_only": r.row_copy_only.to_json(),
"full_grab_row_copy_savings_median_ms": r.row_copy_savings_ms(),
"isolated_row_copy_savings_median_ms": r.row_copy_only.median - r.publish_only.median,
"speedup_vs_get_image": r.speedup(),
})
})
.collect();
let report = serde_json::json!({
"cpu": cpu,
"rustc": rustc,
"profile": profile,
"x_server": server,
"screen": { "width": screen_w, "height": screen_h, "depth": screen.root_depth },
"mit_shm_version": shm_version,
Expand All @@ -324,44 +419,48 @@ mod linux {
return Ok(());
}

println!("# X11 capture benchmark: GetImage vs ShmGetImage\n");
println!("# X11 capture benchmark: SHM publish vs row copy\n");
println!("- CPU: {cpu}");
println!("- rustc: {rustc}");
println!("- profile: {profile}");
println!(
"- X server: {server} ({screen_w}x{screen_h}, depth {})",
screen.root_depth
);
println!("- MIT-SHM: {shm_version}");
println!(
"- {} warmup + {} timed grabs per backend, each a full `X11Grabber::grab` including the copy into a new ffmpeg BGRZ frame\n",
"- {} warmup + {} samples. Full grabs time `X11Grabber::grab`. `row_copy_only` times `copy_image_rows` into a reused frame. `publish_only` wraps the idle SHM mapping with no X round trip.\n",
args.warmup, args.iters
);
println!(
"| Resolution | Backend | Mean (ms) | p50 (ms) | p95 (ms) | p99 (ms) | Mpix/s | Speedup |"
);
println!("|---|---|---:|---:|---:|---:|---:|---:|");
println!("| Resolution | Path | Median (ms) | Mean (ms) | p95 (ms) | Samples |");
println!("|---|---|---:|---:|---:|---:|");
for r in &results {
for (backend, stats) in [
(Backend::GetImage, &r.get_image),
(Backend::ShmGetImage, &r.shm),
for (path, stats) in [
("GetImage grab", &r.get_image),
("ShmPublish grab", &r.shm),
("ShmRowCopy grab", &r.row_copy),
("publish_only", &r.publish_only),
("row_copy_only", &r.row_copy_only),
] {
let speedup = if backend == Backend::ShmGetImage {
format!("{:.2}x", r.speedup())
} else {
"1.00x".to_string()
};
println!(
"| {}x{} | {} | {:.3} | {:.3} | {:.3} | {:.3} | {:.1} | {} |",
r.width,
r.height,
backend.name(),
stats.mean,
stats.p50,
stats.p95,
stats.p99,
stats.mpix_per_s,
speedup
"| {}x{} | {} | {:.4} | {:.4} | {:.4} | {} |",
r.width, r.height, path, stats.median, stats.mean, stats.p95, stats.samples
);
}
println!(
"| {}x{} | isolated savings (row copy - publish) | {:.4} | | | {} |",
r.width,
r.height,
r.row_copy_only.median - r.publish_only.median,
r.row_copy_only.samples
);
println!(
"| {}x{} | full grab savings (row copy - publish) | {:.4} | | | {} |",
r.width,
r.height,
r.row_copy_savings_ms(),
r.shm.samples
);
}
Ok(())
}
Expand Down
Loading
Loading