Skip to content

Commit

Permalink
feat: customizable block size via --block-size argument (#38)
Browse files Browse the repository at this point in the history
Relates to issue #15 

- adding the `--block-size` arg
- with `--block-size 4k` the IOPS metric would somehow realistic
- adding the IOPS metric to the generated metrics
- remove average throuphput and std. deviation throuphput, because those
where not providing enough value
- refines also the output and adds some clarity on units and formulas
  • Loading branch information
sassman authored Jan 8, 2025
1 parent c8d5c8b commit 2ce989b
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 132 deletions.
42 changes: 42 additions & 0 deletions src/fmt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#[macro_export]
macro_rules! println_stats {
($label:expr, $value:expr, $unit:expr) => {
println!("{:<36} {:>10.2} {}", $label, $value, $unit);
};
}

#[macro_export]
macro_rules! println_metric {
($label:expr, $value:expr) => {
let (value, unit) = ($value.as_value(), $value.as_unit());
println_stats!($label, value, unit);
};
}

#[macro_export]
macro_rules! println_time_ms {
($label:expr, $value:expr) => {
println!(
"{:<36} {}",
$label,
Duration::from_millis($value as u64).as_human_readable()
);
};
}

#[macro_export]
macro_rules! println_duration {
($label:expr, $value:expr) => {
println!("{:<36} {}", $label, $value.as_human_readable());
};
}

#[macro_export]
macro_rules! shout {
($label:expr) => {
let standard_font = FIGfont::standard().unwrap();
let figure = standard_font.convert($label);
assert!(figure.is_some());
println!("{}", figure.unwrap());
};
}
184 changes: 111 additions & 73 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,74 @@
use std::path::PathBuf;
use std::time::{Duration, Instant};
use std::time::Duration;

use clap::Parser;
use figlet_rs::FIGfont;
use throughput::Throughput;
use utils::Bytes;

use crate::statistics::{mean, std_deviation};
use crate::utils::{write_once, HumanReadable, Throughput, BUF_SIZE_MB, MAX_CYCLES, TOTAL_SIZE_MB};
use crate::utils::{HumanReadable, MetricWithUnit, MAX_CYCLES};

mod fmt;
mod statistics;
mod throughput;
mod timer;
mod utils;

const MAX_BLOCK_SIZE: Bytes = Bytes::from_mb(256);

/// SSD - Benchmark
#[derive(Parser, Debug)]
#[command(version, about, long_about = None, author, name = "ssd-benchmark")]
struct Args {
/// Directory to meassure, default is the current directory.
#[arg(short, long)]
directory: Option<PathBuf>,

/// Block size in bytes, default is 8m. Note `4k` will be parsed as 4 * 1024 byte.
#[arg(short, long, default_value = "8m")]
block_size: String,

/// Verbose output
#[arg(short, long, default_value_t = false)]
verbose: bool,
}

fn parse_block_size(args: &Args) -> Bytes {
// parse the sector size from the command line, and validate
let factor = match args.block_size.to_lowercase().chars().last().unwrap() {
'k' => 1024,
'm' => 1024 * 1024,
x if !x.is_ascii_digit() => {
eprintln!("The provided block size unit is not valid, allowed values are k, m");
std::process::exit(1);
}
_ => 1,
};

let block_size = args
.block_size
.chars()
.filter(|c| c.is_ascii_digit())
.collect::<String>()
.parse::<u64>()
.map(|block_size| Bytes::from_b(block_size * factor))
.unwrap();

if block_size > MAX_BLOCK_SIZE {
eprintln!("The provided block size exeeds the allowed maximum of {MAX_BLOCK_SIZE}");
std::process::exit(1);
}

block_size
}

fn main() -> std::io::Result<()> {
let verbose = false;
const BUF_SIZE: usize = BUF_SIZE_MB * 1024 * 1024;
let mut buffer = vec![0_u8; BUF_SIZE].into_boxed_slice();
let args = Args::parse();
let verbose = args.verbose;

shout!("SSD - Benchmark");
println!("Version {}", env!("CARGO_PKG_VERSION"));

// let's validate the directory if present
if let Some(dir) = args.directory.as_ref() {
Expand All @@ -33,95 +78,88 @@ fn main() -> std::io::Result<()> {
}
}

shout!("SSD - Benchmark");
println!("Version {}", env!("CARGO_PKG_VERSION"));

println!("Filling buffer with {} MB random data... ", BUF_SIZE_MB);
let buffer_time = prof! {
for i in 0..BUF_SIZE {
buffer[i] = fastrand::u8(..);
};
};
let buf_size = parse_block_size(&args);
let n = buf_size.sequentials();

println_time_ms!("Buffer filled", buffer_time.as_millis());

println!("\nStart benchmarking your disk writing performance...");
println!();
println!(
"Perform sequential writing of total {} MB in {} MB chunks",
TOTAL_SIZE_MB, BUF_SIZE_MB
);
println!("## Sequential Writes");
println!();
println!("Performing {n} sequential writes of {buf_size} blocks",);

let write_time = write_once(buffer.as_ref(), &args.directory)?;
let write_time = buf_size.meassure_sequenqually_writes(&args.directory)?;
let total_bytes = buf_size.total_bytes();

if !verbose {
println!();
}
println!();
println_time_ms!("Total time", write_time.as_millis());
println_stats!("Throughput", write_time.throughput(TOTAL_SIZE_MB), "MB/s");
println_duration!("Total time", write_time);
let tp = Throughput::new(total_bytes, write_time);
println_metric!("Write Throughput", tp);
println_stats!("Write Performance", tp.as_iops(buf_size), "IOPS");
println!();

println!("## Cycled Sequential Writes");
println!();
println!(
"Perform {} write cycles of {} MB",
MAX_CYCLES, TOTAL_SIZE_MB,
);

let mut write_time = Duration::new(0, 0);
let mut min_w_time = None;
let mut max_w_time = None;
let mut write_timings: Vec<f64> = Vec::new();
for i in 0..MAX_CYCLES {
let duration = write_once(buffer.as_ref(), &args.directory)?;
write_timings.push(duration.as_millis() as f64);
if max_w_time.is_none() || duration > max_w_time.unwrap() {
max_w_time = Some(duration);
println!("Performing {MAX_CYCLES} cycles of {n} sequential writes of {buf_size} blocks");

let mut write_time = Duration::default();
let mut min_w_time = Duration::MAX;
let mut max_w_time = Duration::default();
let mut write_timings: Vec<Duration> = Vec::with_capacity(MAX_CYCLES);
for i in 1..=MAX_CYCLES {
print!("[{i}/{MAX_CYCLES}] ");
let duration = buf_size.meassure_sequenqually_writes(&args.directory)?;
write_timings.push(duration);
if duration > max_w_time {
max_w_time = duration;
}
if min_w_time.is_none() || duration < min_w_time.unwrap() {
min_w_time = Some(duration);
if duration < min_w_time {
min_w_time = duration;
}
write_time += duration;
println!();
if verbose {
println_time_ms!(format!("Cycle {} time", i + 1), duration.as_millis());
println_stats!("Throughput", duration.throughput(TOTAL_SIZE_MB), "MB/s");
println_duration!("Time", duration);
println_metric!("Throughput", Throughput::new(total_bytes, duration));
}
}
let deviation_time = std_deviation(write_timings.as_slice()).unwrap_or(0 as f64) as u64;
let mean_time_ms = mean(write_timings.as_slice()).unwrap_or(0 as f64) as u64;
let write_values: Vec<f32> = write_timings
.as_slice()
let write_micros = write_timings
.iter()
.map(|t| (*t as u64).throughput(TOTAL_SIZE_MB))
.map(|d| d.as_micros() as f64)
.collect::<Vec<_>>();
let deviation_time =
Duration::from_micros(std_deviation(write_micros.as_slice()).unwrap_or(0 as f64) as u64);
let mean_time = Duration::from_micros(mean(write_micros.as_slice()).unwrap_or(0.0) as u64);

println!();
println_duration!("Total time", write_time);
println_duration!("Min write time", min_w_time);
println_duration!("Max write time", max_w_time);
println_duration!("Range write time", max_w_time - min_w_time);
println_duration!("Mean write time Ø", mean_time);
println_duration!("Standard deviation σ", deviation_time);
println!();

let max_tp = Throughput::new(total_bytes, min_w_time);
let min_tp = Throughput::new(total_bytes, max_w_time);
let mean_iops: Vec<_> = write_timings
.iter()
.map(|d| Throughput::new(total_bytes, *d).as_iops(buf_size) as f64)
.collect();
let mean_throughput = std_deviation(write_values.as_slice());
let mean_iops = mean(mean_iops.as_slice()).unwrap() as u64;

println_metric!("Min write throughput", min_tp);
println_metric!("Max write throughput", max_tp);
println_stats!("Max write performance", max_tp.as_iops(buf_size), "IOPS");
println_stats!("Min write performance", min_tp.as_iops(buf_size), "IOPS");
println_stats!("Mean write performance", mean_iops, "IOPS");

println!();
println_time_ms!("Total time", write_time.as_millis());
println_time_ms!("Min write time", min_w_time.unwrap().as_millis());
println_time_ms!("Max write time", max_w_time.unwrap().as_millis());
println_time_ms!(
"Range write time",
(max_w_time.unwrap() - min_w_time.unwrap()).as_millis()
);
println_time_ms!("Average write time Ø", mean_time_ms);
println_time_ms!("Standard deviation σ", deviation_time);
println!("## Notes");
println!();
println_stats!(
"Min throughput",
max_w_time.unwrap().throughput(TOTAL_SIZE_MB),
"MB/s"
);
println_stats!(
"Max throughput",
min_w_time.unwrap().throughput(TOTAL_SIZE_MB),
"MB/s"
);
println_stats!(
"Average throughput Ø",
mean_time_ms.throughput(TOTAL_SIZE_MB),
"MB/s"
);
println_stats!("Standard deviation σ", mean_throughput.unwrap(), "MB/s");
println!("1 MB = 1024 KB and 1 KB = 1024 B");
println!("IOPS = Throughput [B/s] / Block Size [B]");

Ok(())
}
96 changes: 96 additions & 0 deletions src/throughput.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::{fmt::Display, time::Duration};

use crate::utils::{Bytes, MetricWithUnit};

pub struct Throughput {
pub write: Bytes,
pub duration: Duration,
}

impl Throughput {
pub const fn new(write: Bytes, duration: Duration) -> Self {
Self { write, duration }
}

pub fn as_bps(&self) -> f64 {
self.write.as_byte() as f64 / self.duration.as_secs_f64()
}

fn display(&self) -> (f64, &'static str) {
let bps = self.as_bps();
let kbps = bps / 1024.0;
let mbps = kbps / 1024.0;
let gbps = mbps / 1024.0;

if gbps >= 1.0 {
(gbps, "GB/s")
} else if mbps >= 1.0 {
(mbps, "MB/s")
} else if kbps >= 1.0 {
(kbps, "KB/s")
} else {
(bps, "B/s")
}
}

pub fn as_iops(&self, block_size: Bytes) -> u64 {
(self.as_bps() / block_size.as_byte() as f64) as u64
}
}

impl MetricWithUnit<f64> for Throughput {
fn as_unit(&self) -> &'static str {
self.display().1
}

fn as_value(&self) -> f64 {
self.display().0
}
}

impl Display for Throughput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (value, unit) = self.display();
if unit == "B/s" {
// there is no smaller than bytes, hence no need for decimal places
write!(f, "{value:.0} {unit}", value = value, unit = unit)
} else {
write!(f, "{value:.2} {unit}")
}
}
}

#[cfg(test)]
mod tests {
use super::*;

const TP_50MBPS: Throughput = Throughput::new(Bytes::from_mb(100), Duration::from_secs(2));

#[test]
fn test_display() {
let throughput = Throughput::new(Bytes::from_mb(100), Duration::from_secs(1));
assert_eq!(format!("{}", throughput), "100.00 MB/s");

// small values should be displayed as KB/s
let throughput = Throughput::new(Bytes::from_b(1024), Duration::from_secs(1));
assert_eq!(format!("{}", throughput), "1.00 KB/s");

// very small values should be displayed as B/s
let throughput = Throughput::new(Bytes::from_b(512), Duration::from_secs(1));
assert_eq!(format!("{}", throughput), "512 B/s");
}

#[test]
fn test_unit_value() {
assert_eq!(TP_50MBPS.as_unit(), "MB/s");
assert_eq!(TP_50MBPS.as_value(), 50.0);
}

#[test]
fn test_iops() {
let block_size = Bytes::from_kb(4);

let iops = TP_50MBPS.as_iops(block_size);
assert_eq!(iops, 12_800);
}
}
17 changes: 17 additions & 0 deletions src/timer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::time::{Duration, Instant};

pub struct Timer {
pub start: Instant,
}

impl Timer {
pub fn start() -> Self {
Self {
start: Instant::now(),
}
}

pub fn stop(&self) -> Duration {
self.start.elapsed()
}
}
Loading

0 comments on commit 2ce989b

Please sign in to comment.