use mysql::Pool;
use mysql::prelude::*;
use std::time::Instant;
fn seek_range(
pool: &Pool,
table: &str,
key: &str,
page: u64,
lo: Option<i64>,
hi: Option<i64>,
) -> u64 {
let mut c = pool.get_conn().expect("conn");
let mut cursor = lo;
let mut n: u64 = 0;
loop {
let where_clause = match (cursor, hi) {
(Some(l), Some(h)) => format!("WHERE {key} > {l} AND {key} <= {h}"),
(Some(l), None) => format!("WHERE {key} > {l}"),
(None, Some(h)) => format!("WHERE {key} <= {h}"),
(None, None) => String::new(),
};
let sql = format!(
"SELECT {key}, payload FROM {table} {where_clause} ORDER BY {key} LIMIT {page}"
);
let rows: Vec<(i64, String)> = c.query(sql).expect("seek page");
if rows.is_empty() {
break;
}
n += rows.len() as u64;
cursor = Some(rows.last().unwrap().0);
if (rows.len() as u64) < page {
break;
}
}
n
}
fn main() {
let a: Vec<String> = std::env::args().collect();
let url = a
.get(1)
.map(String::as_str)
.unwrap_or("mysql://rivet:rivet@127.0.0.1:3310/bench");
let table = a.get(2).map(String::as_str).unwrap_or("big");
let key = a.get(3).map(String::as_str).unwrap_or("id");
let parts: usize = a.get(4).and_then(|s| s.parse().ok()).unwrap_or(4);
let page: u64 = a.get(5).and_then(|s| s.parse().ok()).unwrap_or(10_000);
let pool = Pool::new(url).expect("pool");
let mut c = pool.get_conn().expect("conn");
let total: u64 = c
.query_first::<u64, _>(format!("SELECT COUNT(*) FROM {table}"))
.expect("count")
.expect("count row");
let mut bounds: Vec<i64> = Vec::new();
for i in 1..parts {
let off = total * (i as u64) / (parts as u64);
let b: i64 = c
.query_first::<i64, _>(format!(
"SELECT {key} FROM {table} ORDER BY {key} LIMIT 1 OFFSET {off}"
))
.expect("boundary")
.expect("boundary row");
bounds.push(b);
}
let mut ranges: Vec<(Option<i64>, Option<i64>)> = Vec::new();
let mut prev: Option<i64> = None;
for &b in &bounds {
ranges.push((prev, Some(b)));
prev = Some(b);
}
ranges.push((prev, None));
let t0 = Instant::now();
let seq_rows = seek_range(&pool, table, key, page, None, None);
let seq_ms = t0.elapsed().as_millis().max(1);
let t1 = Instant::now();
let counts: Vec<u64> = std::thread::scope(|s| {
let pool_ref = &pool;
let handles: Vec<_> = ranges
.iter()
.map(|&(lo, hi)| s.spawn(move || seek_range(pool_ref, table, key, page, lo, hi)))
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
let par_ms = t1.elapsed().as_millis().max(1);
let par_rows: u64 = counts.iter().sum();
let max_w = *counts.iter().max().unwrap_or(&0);
let min_w = *counts.iter().filter(|&&c| c > 0).min().unwrap_or(&0);
let skew = if min_w > 0 {
max_w as f64 / min_w as f64
} else {
f64::INFINITY
};
println!("table={table} key={key} total={total} parts={parts} page={page}");
println!("boundaries (row-percentile): {bounds:?}");
println!("per-worker rows: {counts:?} (skew max/min = {skew:.2}x)");
println!("SEQUENTIAL: {seq_rows} rows in {seq_ms} ms");
println!(
"PARALLEL: {par_rows} rows in {par_ms} ms => speedup {:.2}x",
seq_ms as f64 / par_ms as f64
);
assert_eq!(
seq_rows, par_rows,
"row-count parity: parallel must read every row"
);
}