use std::{
collections::{BTreeMap, VecDeque},
env, fs,
io::{self, ErrorKind, Read, Write},
mem::{size_of, zeroed},
net::{Shutdown, TcpListener, TcpStream},
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
process::Command,
sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering},
},
thread,
time::{Duration, Instant},
};
use super::super::iouring::ring::{IORING_CQE_F_MORE, Ring, RingConfig, Sqe, buf_ring::BufRing};
mod readers;
mod report;
mod workloads;
use readers::*;
use report::*;
use workloads::*;
const SIZES: [usize; 5] = [512, 4 << 10, 16 << 10, 64 << 10, 256 << 10];
const PP_SIZES: [usize; 6] = [512, 1 << 10, 4 << 10, 16 << 10, 64 << 10, 256 << 10];
const RUNS: usize = 5;
const MAX_ATTEMPTS: usize = 3;
const SPREAD_LIMIT: f64 = 0.15;
const STREAM_BYTES: u64 = 2 << 30;
const STREAM_SECS: f64 = 5.0;
const PP_ROUND_TRIPS: usize = 100_000;
const PP_MAX_SECS: f64 = 8.0;
const PP_WARMUP: usize = 1_000;
const WRITE_CHUNK: usize = 64 << 10;
const PATTERN_LEN: usize = (1 << 20) + 13;
const WIN: f64 = 1.05;
const RECV_UD: u64 = 1;
const CANCEL_UD: u64 = 2;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Path {
Pool,
UringPoll,
Epoll,
}
impl Path {
const ALL: [Path; 3] = [Path::Pool, Path::UringPoll, Path::Epoll];
fn tag(self) -> &'static str {
match self {
Path::Pool => "A",
Path::UringPoll => "B",
Path::Epoll => "C",
}
}
}
#[derive(Clone, Copy, Debug, Default)]
struct Counters {
reads: u64,
bytes: u64,
enter: u64,
recv: u64,
epoll_wait: u64,
send: u64,
enobufs: u64,
}
#[derive(Clone, Copy, Debug)]
struct Cpus {
reader: usize,
writer: usize,
}
fn cpus() -> Cpus {
env::var("NUCLEI_BENCH_CPUS")
.ok()
.and_then(|v| {
let (r, w) = v.split_once(',')?;
Some(Cpus {
reader: r.trim().parse().ok()?,
writer: w.trim().parse().ok()?,
})
})
.unwrap_or(Cpus {
reader: 2,
writer: 3,
})
}
fn cvt(r: i32) -> io::Result<i32> {
if r < 0 {
Err(io::Error::last_os_error())
} else {
Ok(r)
}
}
fn pin(cpu: usize) -> io::Result<()> {
unsafe {
let mut set: libc::cpu_set_t = zeroed();
libc::CPU_SET(cpu, &mut set);
cvt(libc::sched_setaffinity(
0,
size_of::<libc::cpu_set_t>(),
&set,
))
.map(drop)
}
}
fn thread_cpu_ns() -> u64 {
let ru = unsafe {
let mut ru: libc::rusage = zeroed();
libc::getrusage(libc::RUSAGE_THREAD, &mut ru);
ru
};
let ns = |t: libc::timeval| t.tv_sec as u64 * 1_000_000_000 + t.tv_usec as u64 * 1_000;
ns(ru.ru_utime) + ns(ru.ru_stime)
}
fn loadavg() -> String {
fs::read_to_string("/proc/loadavg").map_or_else(|_| "unavailable".into(), |s| s.trim().into())
}
#[test]
#[ignore = "measurement: run in release with --ignored --nocapture on a quiet machine"]
fn bench_read_path() {
if !uring_available() {
return;
}
let cpus = cpus();
let (l3, src) = l3_bytes();
println!("load average before: {}", loadavg());
println!(
"reader cpu {}, writer cpu {} (sched_setaffinity, distinct)",
cpus.reader, cpus.writer
);
println!(
"L3 {} from {src}; the cold destination rotates through {}",
human(l3),
human(4 * l3)
);
println!(
"{RUNS} runs per point; streaming ends at {} or {STREAM_SECS} s; a point whose range exceeds {:.0}% of its median is rerun up to {MAX_ATTEMPTS} times",
human(STREAM_BYTES as usize),
SPREAD_LIMIT * 100.0
);
let sized = |s| (s, size_pool(s, cpus, 4 * l3).expect("pool sizing failed"));
let pools: Pools = PP_SIZES.map(sized).into();
println!(
"\n== path A pool per read size (count = 4x the smallest count that never ran dry in a hot and a cold probe) =="
);
let cells: Vec<_> = pools
.iter()
.map(|(&s, c)| {
let bytes = c.buf_size as usize * usize::from(c.count);
let buf = human(c.buf_size as usize);
vec![human(s), buf, c.count.to_string(), human(bytes)]
})
.collect();
table(&["S", "buffer", "count", "pool"], &cells);
let hot = measure_stream("hot", None, cpus, &pools).expect("hot streaming failed");
let cold = measure_stream("cold", Some(4 * l3), cpus, &pools).expect("cold streaming failed");
let pp = measure_pingpong(cpus, &pools).expect("ping-pong failed");
verdict(&hot, &cold, &pp);
println!("\nload average after: {}", loadavg());
}
fn check_stream(path: Path, s: usize, total: u64, cfg: PoolCfg) {
let (conn, peer) = connected_pair().unwrap();
let stop = Arc::new(AtomicBool::new(false));
let writer = thread::spawn(move || feed(peer, total, &stop));
let mut r = Reader::new(path, conn.as_raw_fd(), cfg).unwrap();
let (mut buf, mut cur, mut got) = (vec![0u8; s], Cursor::default(), 0u64);
loop {
let n = r.read(&mut buf).unwrap();
if n == 0 {
break;
}
cur.verify(&buf[..n]);
got += n as u64;
}
assert_eq!(
got, total,
"{path:?} S={s}: bytes received differ from bytes sent"
);
drop(r);
writer.join().unwrap().unwrap();
}
fn correctness(path: Path) {
if path != Path::Epoll && !uring_available() {
return;
}
if path == Path::Pool && !buffer_rings_available() {
return;
}
let pool = |count| PoolCfg {
buf_size: 4096,
count,
};
let cases = [(1000, 64), (4096, 64), (10_000, 64), (10_000, 4), (512, 4)];
for (s, count) in cases {
check_stream(path, s, 3 << 20, pool(count));
}
for s in [1 << 10, 20_000] {
pingpong_run(path, s, 200, pool(64), None, true).unwrap();
}
}
#[test]
fn read_path_pool_is_exact() {
correctness(Path::Pool);
}
#[test]
fn read_path_uring_poll_is_exact() {
correctness(Path::UringPoll);
}
#[test]
fn read_path_epoll_is_exact() {
correctness(Path::Epoll);
}