statumen 0.2.0

Statumen whole-slide image reader
Documentation
#![cfg_attr(
    not(feature = "metal"),
    allow(dead_code, unreachable_code, unused_variables)
)]

use std::env;
use std::path::PathBuf;
use std::time::{Duration, Instant};

use statumen::{PlaneSelection, Slide, TileLayout, TileOutputPreference, TilePixels, TileRequest};

#[cfg(feature = "metal")]
fn metal_sessions() -> Result<statumen::output::metal::MetalBackendSessions, String> {
    let device = metal::Device::system_default().ok_or("no system Metal device")?;
    Ok(statumen::output::metal::MetalBackendSessions::new(
        signinum_jpeg_metal::MetalBackendSession::new(device.clone()),
        signinum_j2k_metal::MetalBackendSession::new(device),
    ))
}

#[cfg(not(feature = "metal"))]
fn metal_sessions() -> Result<(), String> {
    Err("bench_dicom_tile_batch requires --features metal".to_string())
}

fn elapsed_ms(duration: Duration) -> f64 {
    duration.as_secs_f64() * 1000.0
}

fn summarize(samples: &[Duration]) -> (f64, f64, f64) {
    let mut ms = samples.iter().copied().map(elapsed_ms).collect::<Vec<_>>();
    ms.sort_by(|a, b| a.partial_cmp(b).unwrap());
    let p50 = ms[ms.len() / 2];
    let p95 = ms[((ms.len() - 1) as f64 * 0.95).round() as usize];
    let mean = ms.iter().sum::<f64>() / ms.len() as f64;
    (p50, p95, mean)
}

fn tile_requests(slide: &Slide, max_tiles: usize) -> Result<Vec<TileRequest>, String> {
    let series = &slide.dataset().scenes[0].series[0];
    for (level_index, level) in series.levels.iter().enumerate() {
        match &level.tile_layout {
            TileLayout::Regular {
                tiles_across,
                tiles_down,
                ..
            } => {
                let usable_cols = tiles_across.saturating_sub(1).max(1);
                let usable_rows = tiles_down.saturating_sub(1).max(1);
                let cols = usable_cols.min(max_tiles as u64).max(1);
                let rows = (usable_rows.min((max_tiles as u64).div_ceil(cols))).max(1);
                let mut reqs = Vec::new();
                for row in 0..rows {
                    for col in 0..cols {
                        if reqs.len() == max_tiles {
                            return Ok(reqs);
                        }
                        reqs.push(TileRequest {
                            scene: 0,
                            series: 0,
                            level: level_index as u32,
                            plane: PlaneSelection::default(),
                            col: col as i64,
                            row: row as i64,
                        });
                    }
                }
                if !reqs.is_empty() {
                    return Ok(reqs);
                }
            }
            TileLayout::Irregular { tiles, .. } => {
                let mut reqs = tiles
                    .keys()
                    .take(max_tiles)
                    .map(|&(col, row)| TileRequest {
                        scene: 0,
                        series: 0,
                        level: level_index as u32,
                        plane: PlaneSelection::default(),
                        col,
                        row,
                    })
                    .collect::<Vec<_>>();
                reqs.sort_by_key(|req| (req.level, req.row, req.col));
                if !reqs.is_empty() {
                    return Ok(reqs);
                }
            }
            TileLayout::WholeLevel { .. } => {}
        }
    }
    Err("slide has no tile-addressable level".to_string())
}

fn count_device(tiles: &[TilePixels]) -> usize {
    tiles
        .iter()
        .filter(|tile| matches!(tile, TilePixels::Device(_)))
        .count()
}

fn run_batch(
    slide: &Slide,
    reqs: &[TileRequest],
    output: &TileOutputPreference,
) -> Result<(Duration, usize), String> {
    let started = Instant::now();
    let tiles = slide
        .source()
        .read_tiles(reqs, output.clone())
        .map_err(|err| err.to_string())?;
    if tiles.len() != reqs.len() {
        return Err(format!(
            "read_tiles returned {} tiles for {} requests",
            tiles.len(),
            reqs.len()
        ));
    }
    Ok((started.elapsed(), count_device(&tiles)))
}

fn run_loop(
    slide: &Slide,
    reqs: &[TileRequest],
    output: &TileOutputPreference,
) -> Result<(Duration, usize), String> {
    let started = Instant::now();
    let mut device = 0;
    for req in reqs {
        let tile = slide
            .source()
            .read_tile(req, output.clone())
            .map_err(|err| err.to_string())?;
        if matches!(tile, TilePixels::Device(_)) {
            device += 1;
        }
    }
    Ok((started.elapsed(), device))
}

fn main() {
    let path = env::args_os()
        .nth(1)
        .map(PathBuf::from)
        .unwrap_or_else(|| {
            eprintln!("usage: cargo run --example bench_dicom_tile_batch --features metal -- <dicom-path> [tile-count] [repeats]");
            std::process::exit(2);
        });
    let max_tiles = env::args()
        .nth(2)
        .and_then(|value| value.parse::<usize>().ok())
        .filter(|value| *value > 0)
        .unwrap_or(16);
    let repeats = env::args()
        .nth(3)
        .and_then(|value| value.parse::<usize>().ok())
        .filter(|value| *value > 0)
        .unwrap_or(5);

    let slide = Slide::open(&path).unwrap_or_else(|err| {
        eprintln!("open {}: {err}", path.display());
        std::process::exit(1);
    });
    let reqs = tile_requests(&slide, max_tiles).unwrap_or_else(|err| {
        eprintln!("{err}");
        std::process::exit(1);
    });

    #[cfg(feature = "metal")]
    let output = TileOutputPreference::require_device_auto_with_metal_and_compressed_decode(
        metal_sessions().unwrap_or_else(|err| {
            eprintln!("{err}");
            std::process::exit(1);
        }),
    );

    #[cfg(not(feature = "metal"))]
    let output = {
        eprintln!("{}", metal_sessions().unwrap_err());
        std::process::exit(1);
    };

    let codec = slide.source().tile_codec_kind(&reqs[0]);
    println!("slide={}", path.display());
    println!("codec={codec:?}");
    println!("tile_count={}", reqs.len());
    println!("repeats={repeats}");

    let mut batch_samples = Vec::with_capacity(repeats);
    let mut loop_samples = Vec::with_capacity(repeats);
    let mut batch_device = 0;
    let mut loop_device = 0;

    for _ in 0..repeats {
        let (elapsed, device) = run_batch(&slide, &reqs, &output).unwrap_or_else(|err| {
            eprintln!("batch read failed: {err}");
            std::process::exit(1);
        });
        batch_samples.push(elapsed);
        batch_device = device;

        let (elapsed, device) = run_loop(&slide, &reqs, &output).unwrap_or_else(|err| {
            eprintln!("loop read failed: {err}");
            std::process::exit(1);
        });
        loop_samples.push(elapsed);
        loop_device = device;
    }

    let (batch_p50, batch_p95, batch_mean) = summarize(&batch_samples);
    let (loop_p50, loop_p95, loop_mean) = summarize(&loop_samples);
    println!("batch_device_tiles={batch_device}");
    println!("loop_device_tiles={loop_device}");
    println!("read_tiles_batch p50={batch_p50:.3}ms p95={batch_p95:.3}ms mean={batch_mean:.3}ms");
    println!("read_tile_loop p50={loop_p50:.3}ms p95={loop_p95:.3}ms mean={loop_mean:.3}ms");
    println!("loop_over_batch_mean_ratio={:.3}x", loop_mean / batch_mean);
}