use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::os::unix::fs::{FileExt, OpenOptionsExt};
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Instant;
use rustfs_uring::UringDriver;
const MAX_QD: usize = 4096;
const MIN_ALIGN: usize = 512;
const MAX_ALIGN: usize = 1 << 20;
const MAX_CHUNK: usize = 1 << 30;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Strategy {
StdBuffered,
StdODirect,
UringReadAt,
UringReadAtDirect,
}
impl Strategy {
fn parse(s: &str) -> Result<Self, String> {
match s {
"std_buffered" => Ok(Self::StdBuffered),
"std_odirect" => Ok(Self::StdODirect),
"uring_read_at" => Ok(Self::UringReadAt),
"uring_read_at_direct" => Ok(Self::UringReadAtDirect),
other => Err(format!(
"unknown strategy {other:?}; expected one of std_buffered, std_odirect, uring_read_at, uring_read_at_direct"
)),
}
}
fn name(self) -> &'static str {
match self {
Self::StdBuffered => "std_buffered",
Self::StdODirect => "std_odirect",
Self::UringReadAt => "uring_read_at",
Self::UringReadAtDirect => "uring_read_at_direct",
}
}
fn is_direct(self) -> bool {
matches!(self, Self::StdODirect | Self::UringReadAtDirect)
}
}
#[derive(Clone)]
struct Config {
strategy: Strategy,
file: String,
size: usize,
chunk: usize,
qd: usize,
align: usize,
verify: bool,
}
fn parse_usize(name: &str, raw: &str) -> Result<usize, String> {
raw.parse()
.map_err(|_| format!("{name}: {raw:?} is not a non-negative integer"))
}
fn parse_args() -> Result<Config, String> {
let a: Vec<String> = std::env::args().collect();
if a.len() != 7 {
return Err("usage: streaming_bench <strategy> <file> <size_bytes> <chunk_bytes> <qd> <align>".to_string());
}
let cfg = Config {
strategy: Strategy::parse(&a[1])?,
file: a[2].clone(),
size: parse_usize("size_bytes", &a[3])?,
chunk: parse_usize("chunk_bytes", &a[4])?,
qd: parse_usize("qd", &a[5])?,
align: parse_usize("align", &a[6])?,
verify: matches!(std::env::var("BENCH_VERIFY").as_deref(), Ok("1") | Ok("true")),
};
validate(&cfg)?;
Ok(cfg)
}
fn validate(cfg: &Config) -> Result<(), String> {
if cfg.size == 0 {
return Err("size_bytes must be > 0".to_string());
}
if cfg.size as u64 > i64::MAX as u64 {
return Err("size_bytes exceeds i64::MAX (the kernel's signed loff_t)".to_string());
}
if cfg.chunk == 0 || cfg.chunk > MAX_CHUNK {
return Err(format!("chunk_bytes must be in 1..={MAX_CHUNK}, got {}", cfg.chunk));
}
if cfg.qd == 0 || cfg.qd > MAX_QD {
return Err(format!("qd must be in 1..={MAX_QD}, got {}", cfg.qd));
}
if !cfg.align.is_power_of_two() || cfg.align < MIN_ALIGN || cfg.align > MAX_ALIGN {
return Err(format!("align must be a power of two in {MIN_ALIGN}..={MAX_ALIGN}, got {}", cfg.align));
}
if cfg.strategy.is_direct() && !cfg.chunk.is_multiple_of(cfg.align) {
return Err(format!(
"O_DIRECT strategies need chunk_bytes ({}) to be a multiple of align ({}), so every read offset is block-aligned",
cfg.chunk, cfg.align
));
}
if cfg.chunk.checked_add(cfg.align).is_none() {
return Err("chunk_bytes + align overflows usize".to_string());
}
Ok(())
}
fn splitmix64(seed: u64) -> u64 {
let mut z = seed.wrapping_add(0x9e37_79b9_7f4a_7c15);
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
fn expected_word(idx: u64) -> [u8; 8] {
splitmix64(idx).to_le_bytes()
}
fn verify_range(offset: u64, data: &[u8]) -> Result<(), String> {
let (mut cached_idx, mut word) = (u64::MAX, [0u8; 8]);
for (i, &got) in data.iter().enumerate() {
let pos = offset + i as u64;
let idx = pos / 8;
if idx != cached_idx {
cached_idx = idx;
word = expected_word(idx);
}
let want = word[(pos % 8) as usize];
if got != want {
return Err(format!("content mismatch at byte {pos}: got {got:#04x}, want {want:#04x}"));
}
}
Ok(())
}
fn ensure_file(path: &str, size: usize) -> Result<(), String> {
match std::fs::symlink_metadata(path) {
Ok(meta) if !meta.file_type().is_file() => Err(format!(
"{path} exists and is not a regular file ({:?}); refusing to touch it",
meta.file_type()
)),
Ok(meta) if meta.len() != size as u64 => Err(format!(
"{path} exists with {} bytes but {size} were requested; refusing to truncate it — delete it or pick another path",
meta.len()
)),
Ok(_) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => create_file(path, size).map_err(|e| {
let _ = std::fs::remove_file(path);
format!("create bench file {path}: {e}")
}),
Err(e) => Err(format!("stat {path}: {e}")),
}
}
fn create_file(path: &str, size: usize) -> io::Result<()> {
let mut f = OpenOptions::new()
.write(true)
.create_new(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(path)?;
let mut buf = vec![0u8; 1 << 20];
let mut written = 0usize;
while written < size {
let base = (written / 8) as u64;
for (w, word) in buf.chunks_exact_mut(8).enumerate() {
word.copy_from_slice(&expected_word(base + w as u64));
}
let n = (size - written).min(buf.len());
f.write_all(&buf[..n])?;
written += n;
}
f.sync_all()
}
fn open_read(cfg: &Config, direct: bool) -> io::Result<File> {
let mut opts = OpenOptions::new();
opts.read(true);
let mut flags = libc::O_NOFOLLOW | libc::O_CLOEXEC;
if direct {
flags |= libc::O_DIRECT;
}
opts.custom_flags(flags).open(&cfg.file)
}
fn run_std_buffered(cfg: &Config) -> Result<(usize, usize), String> {
let mut f = open_read(cfg, false).map_err(|e| format!("open: {e}"))?;
let mut buf = vec![0u8; cfg.chunk];
let (mut total, mut ops) = (0usize, 0usize);
loop {
let n = f.read(&mut buf).map_err(|e| format!("read: {e}"))?;
if n == 0 {
break;
}
if cfg.verify {
verify_range(total as u64, &buf[..n])?;
}
total += n;
ops += 1;
}
Ok((total, ops))
}
fn aligned_buf(len: usize, align: usize) -> (Vec<u8>, usize) {
let v = vec![0u8; len + align];
let pad = v.as_ptr().align_offset(align);
assert!(pad + len <= v.len());
(v, pad)
}
fn run_std_odirect(cfg: &Config) -> Result<(usize, usize), String> {
let f = open_read(cfg, true).map_err(|e| format!("open O_DIRECT: {e}"))?;
let chunk = cfg.chunk;
let (mut buf, pad) = aligned_buf(chunk, cfg.align);
let (mut total, mut ops, mut off) = (0usize, 0usize, 0u64);
while (off as usize) < cfg.size {
let n = f
.read_at(&mut buf[pad..pad + chunk], off)
.map_err(|e| format!("O_DIRECT read_at: {e}"))?;
if n == 0 {
break;
}
let logical = n.min(cfg.size - off as usize);
if cfg.verify {
verify_range(off, &buf[pad..pad + logical])?;
}
total += logical;
ops += 1;
off += n as u64;
}
Ok((total, ops))
}
async fn run_uring(cfg: &Config, direct: bool) -> Result<(usize, usize), String> {
let depth = (cfg.qd * 2).next_power_of_two() as u32;
let driver = Arc::new(UringDriver::probe_and_start(depth).map_err(|e| format!("probe io_uring: {e:?}"))?);
let file = Arc::new(open_read(cfg, direct).map_err(|e| format!("open: {e}"))?);
let offsets: Vec<(u64, usize)> = (0..cfg.size)
.step_by(cfg.chunk)
.map(|o| (o as u64, cfg.chunk.min(cfg.size - o)))
.collect();
let (mut total, mut ops) = (0usize, 0usize);
let mut set = tokio::task::JoinSet::new();
let mut idx = 0usize;
let align = cfg.align;
let verify = cfg.verify;
let spawn = |set: &mut tokio::task::JoinSet<io::Result<usize>>, idx: usize| {
let (off, len) = offsets[idx];
let d = driver.clone();
let f = file.clone();
set.spawn(async move {
let bytes = if direct {
d.read_at_direct(f, off, len, align).await?
} else {
d.read_at(f, off, len).await?
};
if verify {
if bytes.len() != len {
return Err(io::Error::other(format!(
"short read at offset {off}: got {} bytes, want {len}",
bytes.len()
)));
}
verify_range(off, &bytes).map_err(io::Error::other)?;
}
Ok(bytes.len())
});
};
while set.len() < cfg.qd && idx < offsets.len() {
spawn(&mut set, idx);
idx += 1;
}
while let Some(res) = set.join_next().await {
let n = res.map_err(|e| format!("join: {e}"))?.map_err(|e| format!("read: {e}"))?;
total += n;
ops += 1;
if idx < offsets.len() {
spawn(&mut set, idx);
idx += 1;
}
}
Ok((total, ops))
}
fn run(cfg: &Config) -> Result<(usize, usize, f64), String> {
let start = Instant::now();
let (total, ops) = match cfg.strategy {
Strategy::StdBuffered => run_std_buffered(cfg)?,
Strategy::StdODirect => run_std_odirect(cfg)?,
Strategy::UringReadAt | Strategy::UringReadAtDirect => {
let direct = cfg.strategy == Strategy::UringReadAtDirect;
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|e| format!("runtime: {e}"))?;
rt.block_on(run_uring(cfg, direct))?
}
};
let secs = start.elapsed().as_secs_f64();
if total != cfg.size {
return Err(format!("strategy {} read {total} of {} bytes", cfg.strategy.name(), cfg.size));
}
Ok((total, ops, secs))
}
pub(super) fn main() -> ExitCode {
let cfg = match parse_args().and_then(|cfg| ensure_file(&cfg.file, cfg.size).map(|()| cfg)) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("streaming_bench: {e}");
return ExitCode::from(2);
}
};
let (total, ops, secs) = match run(&cfg) {
Ok(v) => v,
Err(e) => {
eprintln!("streaming_bench: {e}");
return ExitCode::FAILURE;
}
};
if cfg.verify {
eprintln!(
"streaming_bench: verify=ok strategy={} size={} chunk={} qd={} align={} (timing below is NOT a measurement)",
cfg.strategy.name(),
cfg.size,
cfg.chunk,
cfg.qd,
cfg.align
);
}
let mbps = (total as f64 / (1024.0 * 1024.0)) / secs;
println!(
"{},{},{},{},{},{},{:.6},{:.1},{}",
cfg.strategy.name(),
cfg.size,
cfg.chunk,
cfg.qd,
cfg.align,
total,
secs,
mbps,
ops
);
ExitCode::SUCCESS
}