#[cfg(not(unix))]
fn main() {
println!("this bench drives a unix socket, so there is nothing to measure here");
}
#[cfg(unix)]
fn main() {
unix::main();
}
#[cfg(unix)]
mod unix {
use std::io::{ErrorKind, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Barrier};
use std::time::{Duration, Instant};
const THREADS: &[usize] = &[1, 2, 4, 8];
const PIPELINE: &[usize] = &[1, 50];
const CLIENT_THREADS: usize = 4;
const CONNS_PER_CLIENT: usize = 16;
const MEASURE: Duration = Duration::from_secs(3);
const WARMUP: Duration = Duration::from_secs(1);
const KEYS: usize = 100_000;
const REPEATS: usize = 3;
const NOISY: f64 = 0.05;
pub fn main() {
let smoke = std::env::var_os("YO_BENCH_SMOKE").is_some();
let threads = counts("YO_BENCH_THREADS", if smoke { &[1] } else { THREADS });
let pipelines = counts("YO_BENCH_PIPELINE", if smoke { &[50] } else { PIPELINE });
let clients = one("YO_BENCH_CLIENTS", CLIENT_THREADS);
let conns = one("YO_BENCH_CONNS", CONNS_PER_CLIENT);
let repeats = one("YO_BENCH_REPEATS", if smoke { 1 } else { REPEATS });
let (warmup, measure) = if smoke {
(Duration::from_millis(100), Duration::from_millis(400))
} else {
(WARMUP, MEASURE)
};
let mine = PathBuf::from(env!("CARGO_BIN_EXE_yodb"));
let against = std::env::var_os("YO_BENCH_AGAINST").map(PathBuf::from);
let debug = text("YO_BENCH_DEBUG");
let debug_against = std::env::var_os("YO_BENCH_DEBUG_AGAINST")
.map(|t| t.to_string_lossy().into_owned())
.unwrap_or_else(|| debug.clone());
if let Some(that) = &against {
println!("this {}", mine.display());
println!("that {}", that.display());
if debug != debug_against {
println!("this DEBUG {debug:?}");
println!("that DEBUG {debug_against:?}");
}
}
let mut bad = 0;
for pipeline in pipelines {
println!("\npipeline {pipeline}, {clients} client threads of {conns} connections");
if against.is_some() {
println!(
"{:>8} {:>12} {:>12} {:>9} {:>6} {:>5}",
"threads", "Kops/sec", "that", "this/that", "cv", "agree"
);
} else {
println!(
"{:>8} {:>12} {:>8} {:>6}",
"threads", "Kops/sec", "vs 1", "cv"
);
}
let mut first = 0.0_f64;
for (at, &count) in threads.iter().enumerate() {
let spec = Cell {
threads: count,
pipeline,
clients,
conns,
warmup,
measure,
};
let mut rates = Vec::with_capacity(repeats);
let mut theirs = Vec::with_capacity(repeats);
let mut ratios = Vec::with_capacity(repeats);
for round in 0..repeats {
let Some(that) = &against else {
rates.push(cell(&spec, &mine, 0, &debug));
continue;
};
let (a, b) = if round % 2 == 0 {
let a = cell(&spec, &mine, 0, &debug);
(a, cell(&spec, that, 1, &debug_against))
} else {
let b = cell(&spec, that, 1, &debug_against);
(cell(&spec, &mine, 0, &debug), b)
};
rates.push(a);
theirs.push(b);
ratios.push(if b > 0.0 { a / b } else { 0.0 });
}
let (rate, spread) = middle(&mut rates);
if against.is_some() {
let that = middle(&mut theirs).0;
let (ratio, cv) = middle(&mut ratios);
let with = agreeing(&ratios, ratio);
let flag = if with * 4 < repeats * 3 {
bad += 1;
" toss"
} else {
""
};
println!(
"{count:>8} {:>12.0} {:>12.0} {ratio:>8.2}x {cv:>6.2} {:>5}{flag}",
rate / 1000.0,
that / 1000.0,
format!("{with}/{repeats}")
);
continue;
}
if at == 0 {
first = rate;
}
let ratio = if first > 0.0 { rate / first } else { 0.0 };
let flag = if spread > NOISY {
bad += 1;
" noisy"
} else {
""
};
println!(
"{count:>8} {:>12.0} {ratio:>7.2}x {spread:>6.2}{flag}",
rate / 1000.0
);
}
}
if bad > 0 && against.is_some() {
println!(
"\n{bad} cells had fewer than three quarters of their pairs on the same side of \
1.00 as the median, so what those cells report is which run won. Turn \
YO_BENCH_REPEATS up, because the median of enough pairs converges where a single \
pair does not: on a laptop at a load average of ten, this binary against a copy \
of itself read 1.16x over three pairs and 1.02x over eleven, and the true answer \
is 1.00x."
);
} else if bad > 0 {
println!(
"\n{bad} cells came out above a coefficient of variation of {NOISY:.2}, so this \
machine is not quiet enough to be measuring anything. Nothing here is worth \
quoting until it runs clean. If what you need is whether one build beats \
another rather than what either is worth, point YO_BENCH_AGAINST at the other \
one, which is the question a busy box can still answer."
);
}
}
fn agreeing(ratios: &[f64], median: f64) -> usize {
if median > 1.0 {
return ratios.iter().filter(|r| **r > 1.0).count();
}
if median < 1.0 {
return ratios.iter().filter(|r| **r < 1.0).count();
}
ratios.len()
}
fn middle(rates: &mut [f64]) -> (f64, f64) {
rates.sort_by(f64::total_cmp);
let median = rates[rates.len() / 2];
if rates.len() < 2 {
return (median, 0.0);
}
let mean = rates.iter().sum::<f64>() / rates.len() as f64;
let spread = rates.iter().map(|r| (r - mean) * (r - mean)).sum::<f64>();
let sd = (spread / (rates.len() - 1) as f64).sqrt();
let cv = if mean > 0.0 { sd / mean } else { 0.0 };
(median, cv)
}
struct Cell {
threads: usize,
pipeline: usize,
clients: usize,
conns: usize,
warmup: Duration,
measure: Duration,
}
fn cell(cell: &Cell, bin: &Path, side: usize, debug: &str) -> f64 {
let Cell {
threads,
pipeline,
clients,
conns: per_client,
warmup,
measure,
} = *cell;
let socket = socket_path(threads, pipeline, side);
let mut server = Server::start(bin, &socket, threads);
server.wait_until_listening();
let go = Arc::new(AtomicBool::new(false));
let stop = Arc::new(AtomicBool::new(false));
let done = Arc::new(AtomicU64::new(0));
let gate = Arc::new(Barrier::new(clients + 1));
let mut hands = Vec::with_capacity(clients);
for id in 0..clients {
let socket = socket.clone();
let go = Arc::clone(&go);
let stop = Arc::clone(&stop);
let done = Arc::clone(&done);
let gate = Arc::clone(&gate);
let debug = debug.to_owned();
hands.push(std::thread::spawn(move || {
let mut conns: Vec<UnixStream> =
(0..per_client).map(|_| connect(&socket)).collect();
let batches = Batches::new(id, pipeline);
if id == 0 {
debug_setup(&mut conns[0], &debug);
}
fill(&mut conns[0], id);
gate.wait();
let mut at = 0;
while !go.load(Ordering::Relaxed) {
at = round(&mut conns, &batches, at);
}
let mut count = 0_u64;
while !stop.load(Ordering::Relaxed) {
at = round(&mut conns, &batches, at);
count += (per_client * pipeline) as u64;
}
done.fetch_add(count, Ordering::Relaxed);
}));
}
gate.wait();
std::thread::sleep(warmup);
go.store(true, Ordering::Relaxed);
let counted_from = Instant::now();
std::thread::sleep(measure);
stop.store(true, Ordering::Relaxed);
let elapsed = counted_from.elapsed().as_secs_f64();
for hand in hands {
let _ = hand.join();
}
let total = done.load(Ordering::Relaxed);
server.stop();
let _ = std::fs::remove_file(&socket);
if elapsed > 0.0 {
total as f64 / elapsed
} else {
0.0
}
}
fn round(conns: &mut [UnixStream], batches: &Batches, mut at: usize) -> usize {
for conn in conns {
let batch = batches.at(at);
at = batches.next(at);
if conn.write_all(batch).is_err() {
continue;
}
drain(conn, batches.pipeline * 2);
}
at
}
struct Batches {
buf: Vec<u8>,
marks: Vec<usize>,
pipeline: usize,
}
impl Batches {
fn new(client: usize, pipeline: usize) -> Batches {
let count = (KEYS / pipeline).max(1);
let mut buf = Vec::with_capacity(count * pipeline * 32);
let mut marks = Vec::with_capacity(count + 1);
let mut k = 0;
for _ in 0..count {
marks.push(buf.len());
for _ in 0..pipeline {
encode(&mut buf, &[b"GET", key_of(client, k).as_bytes()]);
k += 1;
}
}
marks.push(buf.len());
Batches {
buf,
marks,
pipeline,
}
}
fn at(&self, batch: usize) -> &[u8] {
&self.buf[self.marks[batch]..self.marks[batch + 1]]
}
fn next(&self, batch: usize) -> usize {
if batch + 2 == self.marks.len() {
0
} else {
batch + 1
}
}
}
struct Server {
child: Child,
socket: PathBuf,
}
impl Server {
fn start(bin: &Path, socket: &Path, threads: usize) -> Server {
let _ = std::fs::remove_file(socket);
let child = Command::new(bin)
.arg("serve")
.arg("--no-port")
.arg("--unixsocket")
.arg(socket)
.arg("--threads")
.arg(threads.to_string())
.stdout(Stdio::null())
.spawn()
.unwrap_or_else(|e| panic!("{} would not start: {e}", bin.display()));
Server {
child,
socket: socket.to_path_buf(),
}
}
fn wait_until_listening(&mut self) {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if UnixStream::connect(&self.socket).is_ok() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("yodb did not start listening on {}", self.socket.display());
}
fn stop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
impl Drop for Server {
fn drop(&mut self) {
self.stop();
}
}
fn connect(socket: &Path) -> UnixStream {
let stream = UnixStream::connect(socket).expect("the server is listening");
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.expect("a timeout the platform accepts");
stream
}
fn debug_setup(conn: &mut UnixStream, text: &str) {
for one in text.split(';').map(str::trim).filter(|s| !s.is_empty()) {
let parts: Vec<&[u8]> = std::iter::once(b"DEBUG".as_slice())
.chain(one.split_whitespace().map(str::as_bytes))
.collect();
let mut out = Vec::new();
encode(&mut out, &parts);
let _ = conn.write_all(&out);
drain(conn, 1);
}
}
fn fill(conn: &mut UnixStream, client: usize) {
const AT_ONCE: usize = 256;
let mut out = Vec::with_capacity(AT_ONCE * 48);
let mut owed = 0;
for k in 0..KEYS {
encode(&mut out, &[b"SET", key_of(client, k).as_bytes(), b"v"]);
owed += 1;
if owed == AT_ONCE || k + 1 == KEYS {
let _ = conn.write_all(&out);
drain(conn, owed);
out.clear();
owed = 0;
}
}
}
fn drain(conn: &mut UnixStream, owed: usize) -> bool {
let mut buf = [0_u8; 64 * 1024];
let mut seen = 0;
let mut last = 0_u8;
while seen < owed {
match conn.read(&mut buf) {
Ok(0) => return false,
Ok(n) => {
for &b in &buf[..n] {
if last == b'\r' && b == b'\n' {
seen += 1;
}
last = b;
}
}
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(_) => return false,
}
}
true
}
fn encode(out: &mut Vec<u8>, parts: &[&[u8]]) {
out.extend_from_slice(format!("*{}\r\n", parts.len()).as_bytes());
for part in parts {
out.extend_from_slice(format!("${}\r\n", part.len()).as_bytes());
out.extend_from_slice(part);
out.extend_from_slice(b"\r\n");
}
}
fn key_of(client: usize, k: usize) -> String {
format!("bench:{client}:{k}")
}
fn socket_path(threads: usize, pipeline: usize, side: usize) -> PathBuf {
let mut path = std::env::temp_dir();
path.push(format!(
"yodb-bench-{}-t{threads}-p{pipeline}-s{side}.sock",
std::process::id()
));
path
}
fn text(name: &str) -> String {
std::env::var_os(name)
.map(|t| t.to_string_lossy().into_owned())
.unwrap_or_default()
}
fn one(name: &str, fallback: usize) -> usize {
std::env::var_os(name)
.and_then(|text| text.to_string_lossy().trim().parse().ok())
.filter(|n| *n > 0)
.unwrap_or(fallback)
}
fn counts(name: &str, fallback: &[usize]) -> Vec<usize> {
let Some(text) = std::env::var_os(name) else {
return fallback.to_vec();
};
let parsed: Vec<usize> = text
.to_string_lossy()
.split(',')
.filter_map(|part| part.trim().parse().ok())
.filter(|n| *n > 0)
.collect();
if parsed.is_empty() {
fallback.to_vec()
} else {
parsed
}
}
}