use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
use std::time::{Duration, Instant};
use mvcc::{Config, Database, Mvcc, Result, Serializable, Snapshot};
#[derive(Mvcc, Clone, Debug)]
#[mvcc(table = "rows")]
struct Row {
#[mvcc(primary_key)]
id: u64,
value: i64,
}
const ROWS: u64 = 10_000;
const HOT_ROWS: u64 = 4;
const RUN: Duration = Duration::from_millis(700);
const SAMPLES: usize = 5;
fn main() -> Result<()> {
let threads: usize = std::env::args()
.skip(1)
.find(|a| !a.starts_with('-'))
.and_then(|a| a.parse().ok())
.unwrap_or_else(|| std::thread::available_parallelism().map_or(4, std::num::NonZero::get));
let db = Arc::new(Database::open(Config::in_memory())?);
db.register::<Row>()?;
db.transaction(|tx| {
for id in 0..ROWS {
tx.insert(Row { id, value: 0 })?;
}
Ok(())
})?;
println!("{threads} threads, {ROWS} rows, {SAMPLES} samples of {RUN:?} per workload\n");
println!(" {:<28} {:>14} range", "workload", "median ops/s");
let reads = sample(threads, &db, |db, rng| {
let mut tx = db.begin_with::<Snapshot>();
for _ in 0..100 {
let _ = tx.get::<Row>(&(rng() % ROWS)).expect("get");
}
100
});
reads.report("point reads (100 per txn)");
let hot = sample(threads, &db, |db, rng| {
let mut tx = db.begin_with::<Snapshot>();
for _ in 0..100 {
let _ = tx.get::<Row>(&(rng() % HOT_ROWS)).expect("get");
}
100
});
hot.report("point reads (4 hot rows)");
let read_txns = sample(threads, &db, |db, rng| {
let mut tx = db.begin_with::<Snapshot>();
let _ = tx.get::<Row>(&(rng() % ROWS)).expect("get");
1
});
read_txns.report("read-only txns (1 read)");
let writes = sample(threads, &db, |db, rng| {
let key = rng() % ROWS;
db.transaction(|tx| tx.update::<Row>(&key, |r| r.value += 1).map(|_| ()))
.expect("update");
1
});
writes.report("write txns (snapshot)");
let ssi_writes = sample(threads, &db, |db, rng| {
let key = rng() % ROWS;
db.transaction_with::<Serializable, _, _>(|tx| {
let current = tx.get::<Row>(&key)?.map(|r| r.value).unwrap_or(0);
tx.update::<Row>(&key, |r| r.value = current + 1)
.map(|_| ())
})
.expect("update");
1
});
ssi_writes.report("write txns (serializable)");
let scan_selective = sample(threads, &db, |db, _| {
let mut tx = db.begin_with::<Snapshot>();
tx.scan_where::<Row, _>(|r| r.id.is_multiple_of(100))
.expect("scan");
1
});
scan_selective.report("scan_where 1% (snapshot)");
let scan_all = sample(threads, &db, |db, _| {
let mut tx = db.begin_with::<Snapshot>();
tx.scan_where::<Row, _>(|_| true).expect("scan");
1
});
scan_all.report("scan_where 100% (snapshot)");
let scan_ssi = sample(threads, &db, |db, rng| {
let key = rng() % ROWS;
db.transaction_with::<Serializable, _, _>(|tx| {
tx.update::<Row>(&key, |r| r.value += 1)?;
tx.scan_where::<Row, _>(|r| r.id.is_multiple_of(100))?;
Ok(())
})
.expect("scan+update");
1
});
scan_ssi.report("update+scan 1% (serializable)");
println!(
"\n oracle ≈ {:.0} ns/txn, commit path ≈ {:.0} ns/txn, SSI ≈ {:.0} ns/txn",
nanos_each(read_txns.median()) - nanos_each(reads.median()),
nanos_each(writes.median()) - nanos_each(read_txns.median()),
nanos_each(ssi_writes.median()) - nanos_each(writes.median()),
);
Ok(())
}
fn nanos_each(ops_per_sec: u64) -> f64 {
1e9 / ops_per_sec as f64
}
struct Samples(Vec<u64>);
impl Samples {
fn median(&self) -> u64 {
self.0[self.0.len() / 2]
}
fn report(&self, name: &str) {
let (lo, hi) = (self.0[0], self.0[self.0.len() - 1]);
let median = self.median();
let spread = (hi - lo) as f64 / median as f64 * 100.0;
println!(
" {name:<28} {:>14} {} … {} ±{spread:.0}%",
thousands(median),
thousands(lo),
thousands(hi),
);
}
}
fn sample(
threads: usize,
db: &Arc<Database>,
op: impl Fn(&Database, &mut dyn FnMut() -> u64) -> u64 + Send + Sync + Copy + 'static,
) -> Samples {
let mut runs: Vec<u64> = (0..SAMPLES).map(|_| run(threads, db, op)).collect();
runs.sort_unstable();
Samples(runs)
}
fn run(
threads: usize,
db: &Arc<Database>,
op: impl Fn(&Database, &mut dyn FnMut() -> u64) -> u64 + Send + Sync + Copy + 'static,
) -> u64 {
let total = Arc::new(AtomicU64::new(0));
let start = Instant::now();
let handles: Vec<_> = (0..threads)
.map(|t| {
let db = Arc::clone(db);
let total = Arc::clone(&total);
thread::spawn(move || {
let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
let mut rng = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
let mut done = 0u64;
while start.elapsed() < RUN {
for _ in 0..64 {
done += op(&db, &mut rng);
}
}
total.fetch_add(done, Ordering::Relaxed);
})
})
.collect();
for h in handles {
h.join().expect("worker panicked");
}
(total.load(Ordering::Relaxed) as f64 / start.elapsed().as_secs_f64()) as u64
}
fn thousands(n: u64) -> String {
let s = n.to_string();
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if i > 0 && (s.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(c);
}
out
}