#![allow(dead_code)]
pub mod gpu;
use std::time::Duration;
pub const MIB: f64 = 1024.0 * 1024.0;
#[cfg(windows)]
pub fn private_bytes() -> u64 {
use windows::Win32::System::{
ProcessStatus::{
GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX,
},
Threading::GetCurrentProcess,
};
let mut counters = PROCESS_MEMORY_COUNTERS_EX::default();
unsafe {
GetProcessMemoryInfo(
GetCurrentProcess(),
std::ptr::from_mut(&mut counters).cast::<PROCESS_MEMORY_COUNTERS>(),
size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
)
.expect("GetProcessMemoryInfo failed");
}
counters.PrivateUsage as u64
}
#[cfg(not(windows))]
pub fn private_bytes() -> u64 {
let statm = std::fs::read_to_string("/proc/self/statm").expect("read /proc/self/statm");
let resident: u64 = statm
.split_whitespace()
.nth(1)
.and_then(|pages| pages.parse().ok())
.expect("/proc/self/statm has a resident-pages field");
resident * 4096
}
#[derive(Clone, Copy)]
pub enum Unit {
Bytes,
Objects,
}
impl Unit {
fn scale(self) -> f64 {
match self {
Self::Bytes => MIB,
Self::Objects => 1.0,
}
}
fn suffix(self) -> &'static str {
match self {
Self::Bytes => "MiB",
Self::Objects => "objects",
}
}
}
pub struct Trend {
label: String,
unit: Unit,
gauge: Box<dyn FnMut() -> u64>,
samples: Vec<u64>,
}
impl Trend {
pub fn new(label: impl Into<String>, unit: Unit, gauge: impl FnMut() -> u64 + 'static) -> Self {
Self {
label: label.into(),
unit,
gauge: Box::new(gauge),
samples: Vec::new(),
}
}
pub fn private_bytes(label: impl Into<String>) -> Self {
Self::new(label, Unit::Bytes, private_bytes)
}
pub fn sample(&mut self) {
let value = (self.gauge)();
self.samples.push(value);
}
pub fn samples(&self) -> &[u64] {
&self.samples
}
pub fn slope(&self) -> f64 {
let n = self.samples.len();
assert!(
n >= 8,
"{}: {n} samples is too few to fit a trend — raise the iteration \
count (MEDIA_PP_SOAK_ITERS / MEDIA_PP_SOAK_SECS)",
self.label
);
let mean_x = (n - 1) as f64 / 2.0;
let mean_y = self.samples.iter().map(|&y| y as f64).sum::<f64>() / n as f64;
let mut covariance = 0.0;
let mut variance = 0.0;
for (i, &y) in self.samples.iter().enumerate() {
let dx = i as f64 - mean_x;
covariance += dx * (y as f64 - mean_y);
variance += dx * dx;
}
covariance / variance
}
pub fn slope_standard_error(&self) -> f64 {
let n = self.samples.len();
let slope = self.slope();
let mean_x = (n - 1) as f64 / 2.0;
let mean_y = self.samples.iter().map(|&y| y as f64).sum::<f64>() / n as f64;
let mut residual_squares = 0.0;
let mut variance = 0.0;
for (i, &y) in self.samples.iter().enumerate() {
let dx = i as f64 - mean_x;
let residual = y as f64 - (mean_y + slope * dx);
residual_squares += residual * residual;
variance += dx * dx;
}
let sigma = (residual_squares / (n - 2) as f64).sqrt();
sigma / variance.sqrt()
}
pub fn resolution(&self, max_slope: f64) -> f64 {
max_slope + 2.0 * self.slope_standard_error()
}
pub fn assert_flat(&self, max_slope: f64) {
let slope = self.slope();
let scale = self.unit.scale();
let standard_error = self.slope_standard_error();
eprintln!(
"{}\n +- {:.3} {}/iter (1 sigma); resolves growth of {:.3}/iter and above",
self.report(),
standard_error / scale,
self.unit.suffix(),
self.resolution(max_slope) / scale,
);
assert!(
2.0 * standard_error <= max_slope.max(f64::MIN_POSITIVE),
"{} is too noisy to judge: slope {:+.3} +- {:.3} {} per iteration against a \
{:.3} limit. Raise MEDIA_PP_SOAK_ITERS (sensitivity improves with n^1.5) or \
the scenario's own threshold.\n{}",
self.label,
slope / scale,
standard_error / scale,
self.unit.suffix(),
max_slope / scale,
self.report()
);
assert!(
slope <= max_slope,
"{} grew {:.3} {} per iteration (limit {:.3})\n{}",
self.label,
slope / scale,
self.unit.suffix(),
max_slope / scale,
self.report()
);
}
pub fn report(&self) -> String {
let first = *self.samples.first().expect("at least one sample");
let last = *self.samples.last().expect("at least one sample");
let scale = self.unit.scale();
let series = self
.samples
.iter()
.map(|&value| format!("{:.1}", value as f64 / scale))
.collect::<Vec<_>>()
.join(" ");
format!(
"{}: {:.1} -> {:.1} {} over {} samples, slope {:+.3}/iter\n {series}",
self.label,
first as f64 / scale,
last as f64 / scale,
self.unit.suffix(),
self.samples.len(),
self.slope() / scale,
)
}
pub fn print(&self) {
eprintln!("{}", self.report());
}
}
pub fn spawn_isolated(name: &str) -> bool {
use std::io::Write;
const MARKER: &str = "MEDIA_PP_SOAK_ISOLATED";
if std::env::var_os(MARKER).is_some() {
return false;
}
let executable = std::env::current_exe().expect("this test binary's own path");
let output = std::process::Command::new(executable)
.args([
name,
"--exact",
"--ignored",
"--nocapture",
"--test-threads=1",
])
.env(MARKER, "1")
.output()
.expect("run this scenario in its own process");
std::io::stdout()
.write_all(&output.stdout)
.expect("forward the child's output");
std::io::stderr()
.write_all(&output.stderr)
.expect("forward the child's output");
let summary = String::from_utf8_lossy(&output.stdout);
assert!(
summary.contains("1 passed") || !output.status.success(),
"{name} matched no test in its own process — the name passed to spawn_isolated has \
drifted from the test's actual path"
);
assert!(
output.status.success(),
"{name} failed in its own process (its output is above)"
);
true
}
pub fn settle() {
const QUIET_BYTES: u64 = 256 * 1024;
const QUIET_SAMPLES: usize = 8;
let deadline = std::time::Instant::now() + Duration::from_secs(20);
let mut previous = private_bytes();
let mut quiet = 0;
while std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(250));
let current = private_bytes();
quiet = if current.abs_diff(previous) < QUIET_BYTES {
quiet + 1
} else {
0
};
previous = current;
if quiet >= QUIET_SAMPLES {
return;
}
}
eprintln!(
"note: private bytes never settled; this scenario's window starts on a moving baseline"
);
}
pub fn exclusive() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn iterations(default: usize) -> usize {
env_parsed("MEDIA_PP_SOAK_ITERS", default)
}
pub fn soak_duration(default_secs: u64) -> Duration {
Duration::from_secs(env_parsed("MEDIA_PP_SOAK_SECS", default_secs))
}
fn env_parsed<T: std::str::FromStr>(name: &str, default: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(default)
}
pub fn try_test_video() -> Option<String> {
let Ok(path) = std::env::var("MEDIA_PP_TEST_VIDEO") else {
eprintln!(
"skipping: set MEDIA_PP_TEST_VIDEO to a video file to run this test \
(no media is checked into this repository)"
);
return None;
};
if !std::path::Path::new(&path).is_file() {
eprintln!("skipping: MEDIA_PP_TEST_VIDEO={path} is not a readable file");
return None;
}
Some(path)
}
pub fn try_restore_token() -> Option<String> {
match std::env::var("MEDIA_PP_SOAK_RESTORE_TOKEN") {
Ok(token) if !token.trim().is_empty() => Some(token),
_ => {
eprintln!(
"skipping: set MEDIA_PP_SOAK_RESTORE_TOKEN to an xdg-desktop-portal restore \
token to run this test (without one the portal would show its picker and \
block); `cargo run -p screen_record_software -- out.mp4 2 monitor` prints one"
);
None
}
}
}
pub struct TempDir {
path: std::path::PathBuf,
}
impl TempDir {
pub fn new(prefix: &str) -> Self {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock is after the epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("media-pp-soak-{prefix}-{unique}"));
std::fs::create_dir_all(&path).expect("create the soak scratch directory");
Self { path }
}
pub fn path(&self) -> &std::path::Path {
&self.path
}
pub fn join(&self, name: &str) -> std::path::PathBuf {
self.path.join(name)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.path)
.expect("every recording must be closed by teardown, so its directory can be removed");
}
}