use zenpixels::PixelDescriptor;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SimdTier {
Unknown,
CurrentHost,
Wasm,
Wasm128,
Neon,
X86V1,
X86V2,
X86V3,
X86V4,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ComputeEnvironment {
available_cores: usize,
available_ram_bytes: Option<u64>,
simd_tier: Option<SimdTier>,
}
impl ComputeEnvironment {
#[must_use]
pub fn new() -> Self {
Self {
available_cores: 1,
available_ram_bytes: None,
simd_tier: None,
}
}
#[must_use]
pub fn with_cores(mut self, cores: usize) -> Self {
self.available_cores = cores.max(1);
self
}
#[must_use]
pub fn with_available_ram_bytes(mut self, bytes: u64) -> Self {
self.available_ram_bytes = Some(bytes);
self
}
#[must_use]
pub fn with_simd_tier(mut self, tier: SimdTier) -> Self {
self.simd_tier = Some(tier);
self
}
#[must_use]
pub fn cores(&self) -> usize {
self.available_cores
}
#[must_use]
pub fn available_ram_bytes(&self) -> Option<u64> {
self.available_ram_bytes
}
#[must_use]
pub fn simd_tier(&self) -> Option<SimdTier> {
self.simd_tier
}
}
impl Default for ComputeEnvironment {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ImageCharacteristics {
width: u32,
height: u32,
descriptor: PixelDescriptor,
frame_count: u32,
}
impl ImageCharacteristics {
#[must_use]
pub fn new(width: u32, height: u32, descriptor: PixelDescriptor) -> Self {
Self {
width,
height,
descriptor,
frame_count: 1,
}
}
#[must_use]
pub fn with_frame_count(mut self, frames: u32) -> Self {
self.frame_count = frames.max(1);
self
}
#[must_use]
pub fn width(&self) -> u32 {
self.width
}
#[must_use]
pub fn height(&self) -> u32 {
self.height
}
#[must_use]
pub fn descriptor(&self) -> &PixelDescriptor {
&self.descriptor
}
#[must_use]
pub fn frame_count(&self) -> u32 {
self.frame_count
}
#[must_use]
pub fn pixels(&self) -> u64 {
self.width as u64 * self.height as u64
}
#[must_use]
pub fn input_bytes(&self) -> u64 {
self.pixels() * self.descriptor.bytes_per_pixel() as u64
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct ThreadingInformation {
parallel: bool,
max_efficient_threads: Option<u32>,
}
impl ThreadingInformation {
pub const SERIAL: Self = Self {
parallel: false,
max_efficient_threads: Some(1),
};
#[must_use]
pub fn parallel(max_efficient_threads: u32) -> Self {
Self {
parallel: true,
max_efficient_threads: Some(max_efficient_threads.max(1)),
}
}
#[must_use]
pub fn parallel_unknown_knee() -> Self {
Self {
parallel: true,
max_efficient_threads: None,
}
}
#[must_use]
pub fn is_parallel(&self) -> bool {
self.parallel
}
#[must_use]
pub fn max_efficient_threads(&self) -> Option<u32> {
self.max_efficient_threads
}
#[must_use]
pub fn effective_threads(&self, cores: usize) -> u64 {
let cores = cores.max(1) as u64;
match self.max_efficient_threads {
Some(knee) => cores.min(knee.max(1) as u64),
None => cores,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct ResourceEstimate {
peak_memory_bytes_est: Option<u64>,
peak_memory_bytes_max: Option<u64>,
wall_ms: Option<u64>,
cpu_ms: Option<u64>,
threading: Option<ThreadingInformation>,
}
impl ResourceEstimate {
#[must_use]
pub fn unknown() -> Self {
Self {
peak_memory_bytes_est: None,
peak_memory_bytes_max: None,
wall_ms: None,
cpu_ms: None,
threading: None,
}
}
#[must_use]
pub fn new(peak_memory_bytes_est: u64, wall_ms: u64) -> Self {
Self {
peak_memory_bytes_est: Some(peak_memory_bytes_est),
peak_memory_bytes_max: None,
wall_ms: Some(wall_ms),
cpu_ms: None,
threading: None,
}
}
#[must_use]
pub fn with_peak_max(mut self, max: u64) -> Self {
self.peak_memory_bytes_max = Some(max);
self
}
#[must_use]
pub fn with_cpu_ms(mut self, cpu_ms: u64) -> Self {
self.cpu_ms = Some(cpu_ms);
self
}
#[must_use]
pub fn with_threading(mut self, threading: ThreadingInformation) -> Self {
self.threading = Some(threading);
self
}
#[must_use]
pub fn peak_memory_bytes_est(&self) -> Option<u64> {
self.peak_memory_bytes_est
}
#[must_use]
pub fn peak_memory_bytes_max(&self) -> Option<u64> {
self.peak_memory_bytes_max
}
#[must_use]
pub fn wall_ms(&self) -> Option<u64> {
self.wall_ms
}
#[must_use]
pub fn cpu_ms(&self) -> Option<u64> {
self.cpu_ms
}
#[must_use]
pub fn threading(&self) -> Option<ThreadingInformation> {
self.threading
}
#[must_use]
pub fn at_cores(&self, cores: usize) -> Self {
let mut out = *self;
if let (Some(wall), Some(threading)) = (self.wall_ms, self.threading) {
let n = threading.effective_threads(cores).max(1);
out.wall_ms = Some(wall / n);
}
out
}
#[must_use]
pub fn conservative(image: &ImageCharacteristics) -> Self {
let input = image
.input_bytes()
.saturating_mul(image.frame_count() as u64);
let fixed: u64 = 16 << 20;
let typical = fixed.saturating_add(input.saturating_mul(3));
let wall_ms = image.pixels().saturating_mul(image.frame_count() as u64) / 50_000;
Self::new(typical, wall_ms)
.with_peak_max(fixed.saturating_add(input.saturating_mul(8)))
.with_cpu_ms(wall_ms)
.with_threading(ThreadingInformation::SERIAL)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn desc() -> PixelDescriptor {
PixelDescriptor::RGB8_SRGB
}
#[test]
fn compute_environment_builder_clamps_and_defaults() {
assert_eq!(ComputeEnvironment::new().cores(), 1);
assert_eq!(ComputeEnvironment::new().with_cores(0).cores(), 1);
assert_eq!(ComputeEnvironment::default().with_cores(16).cores(), 16);
assert_eq!(
ComputeEnvironment::new()
.with_available_ram_bytes(1 << 30)
.available_ram_bytes(),
Some(1 << 30)
);
assert_eq!(ComputeEnvironment::new().simd_tier(), None);
assert_eq!(
ComputeEnvironment::new()
.with_simd_tier(SimdTier::X86V3)
.simd_tier(),
Some(SimdTier::X86V3)
);
}
#[test]
fn image_characteristics_sizes() {
let im = ImageCharacteristics::new(1024, 768, desc());
assert_eq!(im.pixels(), 1024 * 768);
assert_eq!(im.input_bytes(), 1024 * 768 * 3);
assert_eq!(im.with_frame_count(0).frame_count(), 1);
}
#[test]
fn serial_threading_is_one_thread() {
let ti = ThreadingInformation::SERIAL;
assert_eq!(ti.effective_threads(28), 1);
assert_eq!(ti.max_efficient_threads(), Some(1));
assert!(!ti.is_parallel());
}
#[test]
fn parallel_effective_threads_saturate_at_the_knee() {
let ti = ThreadingInformation::parallel(8);
assert!(ti.is_parallel());
assert_eq!(ti.max_efficient_threads(), Some(8));
assert_eq!(ti.effective_threads(4), 4); assert_eq!(ti.effective_threads(28), 8); assert_eq!(
ThreadingInformation::parallel(0).max_efficient_threads(),
Some(1)
); }
#[test]
fn parallel_unknown_knee_scales_to_all_cores() {
let ti = ThreadingInformation::parallel_unknown_knee();
assert!(ti.is_parallel());
assert_eq!(ti.max_efficient_threads(), None);
assert_eq!(ti.effective_threads(28), 28); assert_eq!(ti.effective_threads(1), 1);
}
#[test]
fn at_cores_scales_wall_to_the_knee_and_leaves_peak_and_cpu() {
let base = ResourceEstimate::new(200, 1000)
.with_peak_max(400)
.with_cpu_ms(1000)
.with_threading(ThreadingInformation::parallel(8));
assert_eq!(base.at_cores(4).wall_ms(), Some(250));
assert_eq!(base.at_cores(28).wall_ms(), Some(125));
assert_eq!(base.at_cores(4).peak_memory_bytes_est(), Some(200));
assert_eq!(base.at_cores(4).peak_memory_bytes_max(), Some(400));
assert_eq!(base.at_cores(4).cpu_ms(), Some(1000));
}
#[test]
fn new_sets_only_peak_est_and_wall() {
let est = ResourceEstimate::new(200, 1000);
assert_eq!(est.peak_memory_bytes_est(), Some(200));
assert_eq!(est.wall_ms(), Some(1000));
assert_eq!(est.peak_memory_bytes_max(), None);
assert_eq!(est.cpu_ms(), None);
assert_eq!(est.threading(), None);
assert_eq!(est.at_cores(8).wall_ms(), Some(1000));
}
#[test]
fn unknown_is_all_none_and_at_cores_is_a_noop() {
let est = ResourceEstimate::unknown();
assert_eq!(est.peak_memory_bytes_est(), None);
assert_eq!(est.peak_memory_bytes_max(), None);
assert_eq!(est.wall_ms(), None);
assert_eq!(est.cpu_ms(), None);
assert_eq!(est.threading(), None);
assert_eq!(est.at_cores(28), est);
}
#[test]
fn conservative_is_serial_and_input_scaled() {
let est = ResourceEstimate::conservative(&ImageCharacteristics::new(1000, 1000, desc()));
assert_eq!(est.threading().map(|t| t.is_parallel()), Some(false));
assert!(est.peak_memory_bytes_est().unwrap() >= 1000 * 1000 * 3);
assert_eq!(est.at_cores(28).wall_ms(), est.wall_ms());
}
}