use crate::workload::schbench::plat::PlatStats;
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::collections::HashMap;
use std::collections::{BTreeMap, VecDeque};
use std::sync::{Condvar, Mutex};
const SHARDS: usize = 256;
const VALUE_SIZES: [usize; 8] = [64, 128, 256, 512, 1024, 4096, 16384, 65536];
const VALUE_WEIGHTS: [u32; 8] = [450, 300, 130, 70, 30, 12, 3, 1];
const STOP_POLL_QUANTUM_NS: u64 = 50_000_000;
fn monotonic_nanos() -> u64 {
let mut ts: libc::timespec = unsafe { core::mem::zeroed() };
let rc = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
assert_eq!(rc, 0, "clock_gettime(CLOCK_MONOTONIC) failed");
(ts.tv_sec as u64) * 1_000_000_000 + ts.tv_nsec as u64
}
fn backing_store_fetch(usec: u64) {
if usec > 0 {
std::thread::sleep(std::time::Duration::from_micros(usec));
}
}
const MAX_SERVICE_US: u64 = 2_000_000;
fn sample_service_us(rng: &mut Rng, scale: f64, inv_neg_alpha: f64) -> u64 {
let us = scale * rng.f64_open01().powf(inv_neg_alpha);
(us as u64).min(MAX_SERVICE_US)
}
fn touch(bytes: &[u8]) -> u64 {
let mut acc = 0u64;
for &b in bytes {
acc = acc.wrapping_add(b as u64);
}
std::hint::black_box(acc)
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn below(&mut self, n: u64) -> u64 {
self.next_u64() % n
}
fn f64_open01(&mut self) -> f64 {
let bits = (self.next_u64() >> 11) + 1;
bits as f64 * (1.0 / ((1u64 << 53) as f64))
}
}
fn sample_value_size(rng: &mut Rng) -> usize {
let total: u32 = VALUE_WEIGHTS.iter().sum();
let mut pick = (rng.below(total as u64)) as u32;
for (i, &w) in VALUE_WEIGHTS.iter().enumerate() {
if pick < w {
return VALUE_SIZES[i];
}
pick -= w;
}
VALUE_SIZES[VALUE_SIZES.len() - 1]
}
fn make_value(size: usize) -> Box<[u8]> {
vec![0xABu8; size].into_boxed_slice()
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct TaobenchConfig {
pub client_threads: usize,
pub slow_threads: usize,
pub cache_capacity_mib: usize,
pub target_hit_pct: usize,
pub slow_path_sleep_us: u64,
pub slow_path_p99_us: u64,
pub arrival_rate: usize,
}
impl Default for TaobenchConfig {
fn default() -> Self {
Self {
client_threads: 0,
slow_threads: 0,
cache_capacity_mib: 64,
target_hit_pct: 90,
slow_path_sleep_us: 100,
slow_path_p99_us: 0,
arrival_rate: 0,
}
}
}
impl TaobenchConfig {
#[must_use = "builder methods consume self; bind the result"]
pub fn client_threads(mut self, n: usize) -> Self {
self.client_threads = n;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn slow_threads(mut self, n: usize) -> Self {
self.slow_threads = n;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn cache_capacity_mib(mut self, mib: usize) -> Self {
self.cache_capacity_mib = mib;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn target_hit_pct(mut self, pct: usize) -> Self {
self.target_hit_pct = pct;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn slow_path_sleep_us(mut self, us: u64) -> Self {
self.slow_path_sleep_us = us;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn slow_path_p99_us(mut self, us: u64) -> Self {
self.slow_path_p99_us = us;
self
}
#[must_use = "builder methods consume self; bind the result"]
pub fn arrival_rate(mut self, ops_per_sec: usize) -> Self {
self.arrival_rate = ops_per_sec;
self
}
fn resolve_client_threads(&self, allowed_cpus: usize) -> usize {
if self.client_threads == 0 {
allowed_cpus.max(1)
} else {
self.client_threads
}
}
fn resolve_slow_threads(&self, clients: usize) -> usize {
if self.slow_threads == 0 {
(clients / 3).max(1)
} else {
self.slow_threads
}
}
fn target_hit_fraction(&self) -> f64 {
(self.target_hit_pct.clamp(1, 99) as f64) / 100.0
}
fn resolve_service_pareto(&self) -> Option<(f64, f64)> {
let p50 = self.slow_path_sleep_us;
let p99 = self.slow_path_p99_us;
if p50 == 0 || p99 <= p50 {
return None;
}
let (p50f, p99f) = (p50 as f64, p99 as f64);
let alpha = 50.0_f64.ln() / (p99f / p50f).ln();
let scale = p50f * 0.5_f64.powf(1.0 / alpha);
Some((scale, -1.0 / alpha))
}
}
struct Shard {
map: HashMap<u64, Box<[u8]>>,
fifo: VecDeque<u64>,
cap: usize,
}
impl Shard {
fn with_cap(cap: usize) -> Self {
Shard {
map: HashMap::new(),
fifo: VecDeque::new(),
cap,
}
}
fn get_touch(&self, k: u64) -> bool {
match self.map.get(&k) {
Some(v) => {
touch(v);
true
}
None => false,
}
}
fn insert(&mut self, k: u64, v: Box<[u8]>) {
if self.map.insert(k, v).is_none() {
self.fifo.push_back(k);
while self.map.len() > self.cap {
match self.fifo.pop_front() {
Some(old) => {
self.map.remove(&old);
}
None => break,
}
}
}
}
}
struct Cache {
shards: Vec<Mutex<Shard>>,
}
impl Cache {
fn new(total_objects: usize) -> Self {
let per_shard = (total_objects / SHARDS).max(1);
let shards = (0..SHARDS)
.map(|_| Mutex::new(Shard::with_cap(per_shard)))
.collect();
Cache { shards }
}
fn shard(&self, k: u64) -> &Mutex<Shard> {
&self.shards[(k as usize) & (SHARDS - 1)]
}
fn get_touch(&self, k: u64) -> bool {
self.shard(k)
.lock()
.expect("cache shard poisoned")
.get_touch(k)
}
fn fill(&self, k: u64, size: usize) {
let v = make_value(size);
self.shard(k)
.lock()
.expect("cache shard poisoned")
.insert(k, v);
}
}
struct SlowReq {
key: u64,
client: usize,
}
struct SlowQueue {
q: Mutex<VecDeque<SlowReq>>,
cv: Condvar,
}
impl SlowQueue {
fn new() -> Self {
SlowQueue {
q: Mutex::new(VecDeque::new()),
cv: Condvar::new(),
}
}
fn push(&self, req: SlowReq) {
self.q.lock().expect("slow queue poisoned").push_back(req);
self.cv.notify_one();
}
}
struct Slot {
done: Mutex<bool>,
cv: Condvar,
}
impl Slot {
fn new() -> Self {
Slot {
done: Mutex::new(false),
cv: Condvar::new(),
}
}
fn wait_filled(&self) {
let mut done = self.done.lock().expect("slot poisoned");
while !*done {
done = self.cv.wait(done).expect("slot poisoned");
}
*done = false;
}
fn signal(&self) {
*self.done.lock().expect("slot poisoned") = true;
self.cv.notify_one();
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TaobenchStats {
pub get_cmds: u64,
pub get_misses: u64,
pub fast_ops: u64,
pub slow_ops: u64,
pub elapsed_ns: u64,
}
impl TaobenchStats {
pub(crate) fn merge(&mut self, o: &TaobenchStats) {
self.get_cmds = self.get_cmds.saturating_add(o.get_cmds);
self.get_misses = self.get_misses.saturating_add(o.get_misses);
self.fast_ops = self.fast_ops.saturating_add(o.fast_ops);
self.slow_ops = self.slow_ops.saturating_add(o.slow_ops);
self.elapsed_ns = self.elapsed_ns.max(o.elapsed_ns);
}
pub fn total_ops(&self) -> u64 {
self.fast_ops.saturating_add(self.slow_ops)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct TaobenchPhaseStats {
pub(crate) counters: TaobenchStats,
pub(crate) serve_lat: PlatStats,
}
impl TaobenchPhaseStats {
pub(crate) fn merge(&mut self, other: &TaobenchPhaseStats) {
self.counters.merge(&other.counters);
self.serve_lat.combine(&other.serve_lat);
}
}
pub(crate) struct TaobenchOutcome {
pub whole_run: TaobenchStats,
pub phases: Vec<(u32, TaobenchPhaseStats)>,
}
struct ThreadAccum {
cur_epoch: u32,
cur: TaobenchPhaseStats,
phases: BTreeMap<u32, TaobenchPhaseStats>,
whole: TaobenchStats,
phase_start_ns: u64,
thread_start_ns: u64,
}
impl ThreadAccum {
fn new(epoch: u32) -> Self {
let now = monotonic_nanos();
ThreadAccum {
cur_epoch: epoch,
cur: TaobenchPhaseStats::default(),
phases: BTreeMap::new(),
whole: TaobenchStats::default(),
phase_start_ns: now,
thread_start_ns: now,
}
}
fn roll_to(&mut self, epoch: u32) {
if epoch != self.cur_epoch {
let now = monotonic_nanos();
let mut cur = std::mem::take(&mut self.cur);
cur.counters.elapsed_ns = now.saturating_sub(self.phase_start_ns);
self.phases.entry(self.cur_epoch).or_default().merge(&cur);
self.cur_epoch = epoch;
self.phase_start_ns = now;
}
}
fn record_cmd(&mut self, epoch: u32, hit: bool) {
self.roll_to(epoch);
self.cur.counters.get_cmds += 1;
self.whole.get_cmds += 1;
if !hit {
self.cur.counters.get_misses += 1;
self.whole.get_misses += 1;
}
}
fn record_complete(&mut self, epoch: u32, hit: bool) {
self.roll_to(epoch);
if hit {
self.cur.counters.fast_ops += 1;
self.whole.fast_ops += 1;
} else {
self.cur.counters.slow_ops += 1;
self.whole.slow_ops += 1;
}
}
fn record_serve_lat(&mut self, epoch: u32, ns: u64) {
self.roll_to(epoch);
let us = (ns / 1000).min(u32::MAX as u64) as u32;
self.cur.serve_lat.add_lat(us);
}
fn finalize(mut self) -> Self {
let now = monotonic_nanos();
let mut cur = std::mem::take(&mut self.cur);
cur.counters.elapsed_ns = now.saturating_sub(self.phase_start_ns);
self.phases.entry(self.cur_epoch).or_default().merge(&cur);
self.whole.elapsed_ns = now.saturating_sub(self.thread_start_ns);
self
}
}
fn read_epoch(phase_epoch: Option<&AtomicU32>) -> u32 {
phase_epoch.map(|e| e.load(Ordering::Relaxed)).unwrap_or(0)
}
pub(crate) fn run(
config: &TaobenchConfig,
stop: &AtomicBool,
progress: &AtomicU64,
phase_epoch: Option<&AtomicU32>,
) -> TaobenchOutcome {
let allowed_cpus = resolve_allowed_cpus();
let n_clients = config.resolve_client_threads(allowed_cpus);
let n_slow = config.resolve_slow_threads(n_clients);
let interval_ns: u64 = if config.arrival_rate == 0 {
0
} else {
(1_000_000_000u64.saturating_mul(n_clients as u64) / config.arrival_rate as u64).max(1)
};
let service_pareto = config.resolve_service_pareto();
let mean_value = mean_value_size();
let capacity_bytes = config.cache_capacity_mib.max(1) * 1024 * 1024;
let total_objects = (capacity_bytes / mean_value).max(SHARDS);
let key_range = ((total_objects as f64) / config.target_hit_fraction()).ceil() as u64;
let cache = Cache::new(total_objects);
{
let mut warm = Rng::new(0xC0FFEE);
for k in 0..total_objects as u64 {
cache.fill(k, sample_value_size(&mut warm));
}
}
let slow_q = SlowQueue::new();
let slots: Vec<Slot> = (0..n_clients).map(|_| Slot::new()).collect();
let disp_stop = AtomicBool::new(false);
let client_accums: Vec<ThreadAccum> = std::thread::scope(|s| {
let dispatchers: Vec<_> = (0..n_slow)
.map(|i| {
let cache = &cache;
let slow_q = &slow_q;
let slots = &slots;
let disp_stop = &disp_stop;
s.spawn(move || {
dispatcher_loop(i, config, service_pareto, cache, slow_q, slots, disp_stop)
})
})
.collect();
let clients: Vec<_> = (0..n_clients)
.map(|i| {
let cache = &cache;
let slow_q = &slow_q;
let slot = &slots[i];
s.spawn(move || {
client_loop(
i,
stop,
progress,
phase_epoch,
cache,
slow_q,
slot,
key_range,
interval_ns,
)
})
})
.collect();
let accums: Vec<ThreadAccum> = clients
.into_iter()
.map(|c| c.join().expect("taobench client panicked"))
.collect();
disp_stop.store(true, Ordering::Release);
slow_q.cv.notify_all();
for d in dispatchers {
d.join().expect("taobench dispatcher panicked");
}
accums
});
let mut all_phases: BTreeMap<u32, TaobenchPhaseStats> = BTreeMap::new();
let mut whole = TaobenchStats::default();
for accum in client_accums {
for (e, s) in accum.phases {
all_phases.entry(e).or_default().merge(&s);
}
whole.merge(&accum.whole);
}
TaobenchOutcome {
whole_run: whole,
phases: all_phases.into_iter().collect(),
}
}
#[allow(clippy::too_many_arguments)]
fn client_loop(
id: usize,
stop: &AtomicBool,
progress: &AtomicU64,
phase_epoch: Option<&AtomicU32>,
cache: &Cache,
slow_q: &SlowQueue,
slot: &Slot,
key_range: u64,
interval_ns: u64,
) -> ThreadAccum {
let mut rng = Rng::new(0x5EED_0000 ^ (id as u64).wrapping_mul(0x9E37_79B1));
let mut accum = ThreadAccum::new(read_epoch(phase_epoch));
let open_loop = interval_ns != 0;
let mut intended = monotonic_nanos();
'client: while !stop.load(Ordering::Acquire) {
if open_loop {
intended = intended.saturating_add(interval_ns);
loop {
if stop.load(Ordering::Acquire) {
break 'client;
}
let now = monotonic_nanos();
if now >= intended {
break;
}
let nap = (intended - now).min(STOP_POLL_QUANTUM_NS);
std::thread::sleep(std::time::Duration::from_nanos(nap));
}
}
let epoch_cmd = read_epoch(phase_epoch);
let key = rng.below(key_range);
let hit = cache.get_touch(key);
accum.record_cmd(epoch_cmd, hit);
if hit {
accum.record_complete(epoch_cmd, true);
if open_loop {
let lat = monotonic_nanos().saturating_sub(intended);
accum.record_serve_lat(epoch_cmd, lat);
}
} else {
slow_q.push(SlowReq { key, client: id });
slot.wait_filled();
let resp_epoch = read_epoch(phase_epoch);
accum.record_complete(resp_epoch, false);
if open_loop {
let lat = monotonic_nanos().saturating_sub(intended);
accum.record_serve_lat(resp_epoch, lat);
}
}
progress.fetch_add(1, Ordering::Relaxed);
}
accum.finalize()
}
fn dispatcher_loop(
id: usize,
config: &TaobenchConfig,
service_pareto: Option<(f64, f64)>,
cache: &Cache,
slow_q: &SlowQueue,
slots: &[Slot],
disp_stop: &AtomicBool,
) {
let mut rng = Rng::new(0xD15A_0000 ^ (id as u64).wrapping_mul(0x9E37_79B1));
loop {
let req = {
let mut q = slow_q.q.lock().expect("slow queue poisoned");
loop {
if let Some(r) = q.pop_front() {
break Some(r);
}
if disp_stop.load(Ordering::Acquire) {
break None;
}
q = slow_q.cv.wait(q).expect("slow queue poisoned");
}
};
let Some(req) = req else { break };
let usec = match service_pareto {
Some((scale, inv_neg_alpha)) => sample_service_us(&mut rng, scale, inv_neg_alpha),
None => config.slow_path_sleep_us,
};
backing_store_fetch(usec);
cache.fill(req.key, sample_value_size(&mut rng));
slots[req.client].signal();
}
}
fn mean_value_size() -> usize {
let total_w: u64 = VALUE_WEIGHTS.iter().map(|&w| w as u64).sum();
let weighted: u64 = VALUE_SIZES
.iter()
.zip(VALUE_WEIGHTS.iter())
.map(|(&s, &w)| s as u64 * w as u64)
.sum();
(weighted / total_w).max(1) as usize
}
fn resolve_allowed_cpus() -> usize {
unsafe {
let mut set: libc::cpu_set_t = core::mem::zeroed();
if libc::sched_getaffinity(0, core::mem::size_of::<libc::cpu_set_t>(), &mut set) == 0 {
let n = libc::CPU_COUNT(&set);
if n > 0 {
return n as usize;
}
}
}
let n = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) };
if n > 0 { n as usize } else { 1 }
}
pub fn run_standalone(config: &TaobenchConfig, run_secs: u64) -> TaobenchStandaloneReport {
let stop = AtomicBool::new(false);
let progress = AtomicU64::new(0);
let allowed_cpus = resolve_allowed_cpus();
let nr_client_threads = config.resolve_client_threads(allowed_cpus);
let nr_slow_threads = config.resolve_slow_threads(nr_client_threads);
let outcome = std::thread::scope(|s| {
let h = s.spawn(|| run(config, &stop, &progress, None));
std::thread::sleep(std::time::Duration::from_secs(run_secs));
stop.store(true, Ordering::Release);
h.join().expect("taobench standalone run panicked")
});
let mut serve = PlatStats::default();
for (_epoch, p) in &outcome.phases {
serve.combine(&p.serve_lat);
}
TaobenchStandaloneReport::from_run(
&outcome.whole_run,
&serve,
nr_client_threads,
nr_slow_threads,
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TaobenchStandaloneReport {
pub total_qps: f64,
pub fast_qps: f64,
pub slow_qps: f64,
pub hit_ratio: f64,
pub hit_rate: f64,
pub total_ops: u64,
pub fast_ops: u64,
pub slow_ops: u64,
pub elapsed_secs: f64,
pub nr_client_threads: usize,
pub nr_slow_threads: usize,
pub serve_p50_us: Option<u32>,
pub serve_p99_us: Option<u32>,
pub serve_p999_us: Option<u32>,
pub serve_min_us: Option<u32>,
pub serve_max_us: Option<u32>,
}
impl TaobenchStandaloneReport {
fn from_run(
w: &TaobenchStats,
serve: &PlatStats,
nr_client_threads: usize,
nr_slow_threads: usize,
) -> Self {
use crate::workload::schbench::plat::Pct;
let secs = (w.elapsed_ns as f64 / 1e9).max(f64::MIN_POSITIVE);
let total = w.total_ops();
let serve_q = (serve.sample_count() > 0).then(|| serve.percentiles());
TaobenchStandaloneReport {
total_qps: total as f64 / secs,
fast_qps: w.fast_ops as f64 / secs,
slow_qps: w.slow_ops as f64 / secs,
hit_ratio: if total > 0 {
w.fast_ops as f64 / total as f64
} else {
0.0
},
hit_rate: if w.get_cmds > 0 {
1.0 - (w.get_misses as f64 / w.get_cmds as f64)
} else {
0.0
},
total_ops: total,
fast_ops: w.fast_ops,
slow_ops: w.slow_ops,
elapsed_secs: secs,
nr_client_threads,
nr_slow_threads,
serve_p50_us: serve_q.as_ref().map(|q| q.value_at(Pct::P50)),
serve_p99_us: serve_q.as_ref().map(|q| q.value_at(Pct::P99)),
serve_p999_us: serve_q.as_ref().map(|q| q.value_at(Pct::P999)),
serve_min_us: serve_q.as_ref().map(|q| q.min),
serve_max_us: serve_q.as_ref().map(|q| q.max),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_size_distribution_mean_is_small_object_heavy() {
let m = mean_value_size();
assert!(
(200..=500).contains(&m),
"mean value size {m} B is small-object-heavy"
);
assert_eq!(*VALUE_SIZES.last().unwrap(), 65536, "tail reaches 64 KiB");
}
#[test]
fn taobench_stats_merge_and_total_ops_saturate_on_overflow() {
let mut a = TaobenchStats {
get_cmds: u64::MAX,
get_misses: u64::MAX,
fast_ops: u64::MAX,
slow_ops: 1,
elapsed_ns: 1000,
};
let b = TaobenchStats {
get_cmds: 5,
get_misses: 5,
fast_ops: 5,
slow_ops: 5,
elapsed_ns: 9000,
};
a.merge(&b);
assert_eq!(a.get_cmds, u64::MAX, "saturates, not wraps to 4");
assert_eq!(a.get_misses, u64::MAX);
assert_eq!(a.fast_ops, u64::MAX);
assert_eq!(a.slow_ops, 6);
assert_eq!(a.elapsed_ns, 9000, "wall window is MAX, not summed");
assert_eq!(a.total_ops(), u64::MAX, "total_ops saturates");
}
#[test]
fn engine_serves_ops_and_hit_ratio_settles_near_target_not_one() {
let cfg = TaobenchConfig::default()
.client_threads(4)
.slow_threads(2)
.cache_capacity_mib(8)
.target_hit_pct(90)
.slow_path_sleep_us(10);
let stop = AtomicBool::new(false);
let progress = AtomicU64::new(0);
std::thread::scope(|s| {
let h = s.spawn(|| run(&cfg, &stop, &progress, None));
while progress.load(Ordering::Relaxed) < 100_000 {
std::hint::spin_loop();
}
stop.store(true, Ordering::Release);
let out = h.join().expect("engine panicked");
let total = out.whole_run.total_ops();
assert!(total > 0, "engine served ops");
assert!(out.whole_run.elapsed_ns > 0, "the run window was measured");
let hit = out.whole_run.fast_ops as f64 / total as f64;
assert!(
(0.80..=0.97).contains(&hit),
"hit ratio {hit} settles near target 0.90 (eviction equilibrium), not 1.0"
);
assert!(
out.whole_run.slow_ops > 0,
"the slow/miss path is exercised"
);
});
}
#[test]
fn taobench_config_serde_roundtrips() {
let cfg = TaobenchConfig::default()
.client_threads(8)
.slow_threads(3)
.cache_capacity_mib(128)
.target_hit_pct(85)
.slow_path_sleep_us(250)
.slow_path_p99_us(2500)
.arrival_rate(50_000);
let json = serde_json::to_string(&cfg).expect("TaobenchConfig must serialize");
let back: TaobenchConfig =
serde_json::from_str(&json).expect("TaobenchConfig must deserialize");
assert_eq!(cfg, back, "config roundtrips unchanged");
}
#[test]
fn worktype_taobench_registration_and_serde() {
use crate::workload::WorkType;
let wt = WorkType::taobench(
TaobenchConfig::default()
.client_threads(4)
.target_hit_pct(95),
);
assert_eq!(wt.name(), "Taobench");
assert_eq!(
WorkType::from_name("Taobench"),
Some(WorkType::Taobench {
config: TaobenchConfig::default()
})
);
let json = serde_json::to_string(&wt).expect("WorkType::Taobench must serialize");
let back: WorkType =
serde_json::from_str(&json).expect("WorkType::Taobench must deserialize");
assert_eq!(wt, back);
}
#[test]
fn taobench_config_reachable_via_prelude() {
let cfg: crate::prelude::TaobenchConfig = crate::prelude::TaobenchConfig::default();
assert_eq!(cfg, TaobenchConfig::default());
}
#[test]
fn taobench_stats_serde_roundtrips() {
let s = TaobenchStats {
get_cmds: 1000,
get_misses: 150,
fast_ops: 850,
slow_ops: 150,
elapsed_ns: 9_000_000_000,
};
let json = serde_json::to_string(&s).expect("TaobenchStats must serialize");
let back: TaobenchStats =
serde_json::from_str(&json).expect("TaobenchStats must deserialize");
assert_eq!(s, back, "TaobenchStats roundtrips unchanged");
}
#[test]
fn taobench_stats_reachable_via_prelude() {
let s: crate::prelude::TaobenchStats = crate::prelude::TaobenchStats::default();
assert_eq!(s, TaobenchStats::default());
}
#[test]
fn taobench_config_arrival_rate_default_and_setter() {
assert_eq!(
TaobenchConfig::default().arrival_rate,
0,
"default arrival_rate is 0 (closed loop)"
);
let cfg = TaobenchConfig::default().arrival_rate(100_000);
assert_eq!(cfg.arrival_rate, 100_000, "setter sets arrival_rate");
}
#[test]
fn taobench_phase_stats_merge_sums_counters_and_unions_serve_lat() {
let mut a = TaobenchPhaseStats {
counters: TaobenchStats {
get_cmds: 10,
get_misses: 2,
fast_ops: 8,
slow_ops: 2,
elapsed_ns: 1000,
},
..Default::default()
};
a.serve_lat.add_lat(5);
a.serve_lat.add_lat(50);
let mut b = TaobenchPhaseStats {
counters: TaobenchStats {
get_cmds: 6,
get_misses: 1,
fast_ops: 5,
slow_ops: 1,
elapsed_ns: 4000,
},
..Default::default()
};
b.serve_lat.add_lat(500);
a.merge(&b);
assert_eq!(a.counters.get_cmds, 16, "Σ get_cmds");
assert_eq!(a.counters.fast_ops, 13, "Σ fast_ops");
assert_eq!(a.counters.slow_ops, 3, "Σ slow_ops");
assert_eq!(a.counters.elapsed_ns, 4000, "wall is MAX, not summed");
assert_eq!(
a.serve_lat.sample_count(),
3,
"serve histograms union (2 + 1 samples)"
);
}
#[test]
fn taobench_phase_stats_serde_roundtrips() {
let mut s = TaobenchPhaseStats {
counters: TaobenchStats {
get_cmds: 1000,
get_misses: 150,
fast_ops: 850,
slow_ops: 150,
elapsed_ns: 9_000_000_000,
},
..Default::default()
};
s.serve_lat.add_lat(12);
s.serve_lat.add_lat(340);
s.serve_lat.add_lat(99_999);
let json = serde_json::to_string(&s).expect("TaobenchPhaseStats must serialize");
let back: TaobenchPhaseStats =
serde_json::from_str(&json).expect("TaobenchPhaseStats must deserialize");
assert_eq!(
s, back,
"TaobenchPhaseStats roundtrips unchanged (counters + serve_lat)"
);
}
#[test]
fn open_loop_stamps_serve_latency_on_every_completion() {
let cfg = TaobenchConfig::default()
.client_threads(4)
.slow_threads(2)
.cache_capacity_mib(8)
.target_hit_pct(90)
.slow_path_sleep_us(10)
.arrival_rate(1_000_000_000);
let stop = AtomicBool::new(false);
let progress = AtomicU64::new(0);
let out = std::thread::scope(|s| {
let h = s.spawn(|| run(&cfg, &stop, &progress, None));
while progress.load(Ordering::Relaxed) < 50_000 {
std::hint::spin_loop();
}
stop.store(true, Ordering::Release);
h.join().expect("engine panicked")
});
let total = out.whole_run.total_ops();
assert!(total > 0, "engine served ops");
assert!(out.whole_run.slow_ops > 0, "the miss arm is exercised");
let mut serve = PlatStats::default();
for (_epoch, p) in &out.phases {
serve.combine(&p.serve_lat);
}
assert_eq!(
serve.sample_count(),
total,
"every completion (hit + miss) records one serve-latency sample"
);
}
#[test]
fn closed_loop_records_no_serve_latency() {
let cfg = TaobenchConfig::default()
.client_threads(4)
.slow_threads(2)
.cache_capacity_mib(8)
.target_hit_pct(90)
.slow_path_sleep_us(10)
.slow_path_p99_us(1000); let stop = AtomicBool::new(false);
let progress = AtomicU64::new(0);
let out = std::thread::scope(|s| {
let h = s.spawn(|| run(&cfg, &stop, &progress, None));
while progress.load(Ordering::Relaxed) < 20_000 {
std::hint::spin_loop();
}
stop.store(true, Ordering::Release);
h.join().expect("engine panicked")
});
assert!(out.whole_run.total_ops() > 0, "ops completed");
for (epoch, p) in &out.phases {
assert_eq!(
p.serve_lat.sample_count(),
0,
"closed loop records no per-phase serve latency (epoch {epoch})"
);
}
}
#[test]
fn open_loop_per_phase_carrier_holds_serve_latency() {
let cfg = TaobenchConfig::default()
.client_threads(4)
.slow_threads(2)
.cache_capacity_mib(8)
.target_hit_pct(90)
.slow_path_sleep_us(10)
.arrival_rate(1_000_000_000);
let stop = AtomicBool::new(false);
let progress = AtomicU64::new(0);
let epoch = AtomicU32::new(7);
let out = std::thread::scope(|s| {
let h = s.spawn(|| run(&cfg, &stop, &progress, Some(&epoch)));
while progress.load(Ordering::Relaxed) < 30_000 {
std::hint::spin_loop();
}
stop.store(true, Ordering::Release);
h.join().expect("engine panicked")
});
let phase7 = out
.phases
.iter()
.find(|(e, _)| *e == 7)
.map(|(_, p)| p)
.expect("epoch-7 phase carrier present");
assert!(
phase7.serve_lat.sample_count() > 0,
"per-phase serve latency recorded"
);
assert_eq!(
phase7.serve_lat.sample_count(),
out.whole_run.total_ops(),
"single-epoch run: every completion stamps one per-phase serve sample"
);
}
#[test]
fn open_loop_pacing_sleep_is_bounded_for_prompt_shutdown() {
let cfg = TaobenchConfig::default()
.client_threads(1)
.slow_threads(1)
.cache_capacity_mib(4)
.target_hit_pct(90)
.slow_path_sleep_us(0)
.arrival_rate(1); let stop = AtomicBool::new(false);
let progress = AtomicU64::new(0);
let start = std::time::Instant::now();
std::thread::scope(|s| {
let h = s.spawn(|| run(&cfg, &stop, &progress, None));
std::thread::sleep(std::time::Duration::from_millis(20));
stop.store(true, Ordering::Release);
let _ = h.join().expect("engine panicked");
});
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(500),
"open-loop client joined in {elapsed:?}; the pacing sleep must be \
bounded (interval is ~1s — an unbounded sleep would block the join)"
);
}
#[test]
fn taobench_config_slow_path_p99_default_and_setter() {
assert_eq!(
TaobenchConfig::default().slow_path_p99_us,
0,
"default slow_path_p99_us is 0 (fixed latency)"
);
let cfg = TaobenchConfig::default().slow_path_p99_us(5000);
assert_eq!(cfg.slow_path_p99_us, 5000, "setter sets slow_path_p99_us");
}
#[test]
fn taobench_service_pareto_mapping_and_legacy_gate() {
let cfg = TaobenchConfig::default()
.slow_path_sleep_us(100)
.slow_path_p99_us(1000);
let (scale, inv_neg_alpha) = cfg.resolve_service_pareto().expect("tail enabled");
let alpha = -1.0 / inv_neg_alpha;
assert!(
(alpha - 50.0_f64.ln() / 10.0_f64.ln()).abs() < 1e-9,
"alpha = ln(50)/ln(10), got {alpha}",
);
let median = scale * 0.5_f64.powf(-1.0 / alpha);
let p99 = scale * 0.01_f64.powf(-1.0 / alpha);
assert!(
(median - 100.0).abs() < 1e-6,
"median quantile = p50 (100), got {median}",
);
assert!(
(p99 - 1000.0).abs() < 1e-6,
"p99 quantile = configured (1000), got {p99}",
);
let base = TaobenchConfig::default().slow_path_sleep_us(100);
assert!(
base.clone()
.slow_path_p99_us(0)
.resolve_service_pareto()
.is_none(),
"p99 == 0 is fixed",
);
assert!(
base.clone()
.slow_path_p99_us(50)
.resolve_service_pareto()
.is_none(),
"p99 < p50 is fixed",
);
assert!(
base.clone()
.slow_path_p99_us(100)
.resolve_service_pareto()
.is_none(),
"p99 == p50 is fixed",
);
assert!(
TaobenchConfig::default()
.slow_path_sleep_us(0)
.slow_path_p99_us(1000)
.resolve_service_pareto()
.is_none(),
"p50 == 0 has no Pareto scale",
);
}
#[test]
fn taobench_service_sampler_matches_configured_quantiles() {
use crate::workload::schbench::plat::Pct;
let cfg = TaobenchConfig::default()
.slow_path_sleep_us(100)
.slow_path_p99_us(1000);
let (scale, inv_neg_alpha) = cfg.resolve_service_pareto().unwrap();
let mut rng = Rng::new(0x5A3C_1234);
let mut plat = PlatStats::default();
for _ in 0..200_000 {
let us = sample_service_us(&mut rng, scale, inv_neg_alpha);
plat.add_lat(us.min(u32::MAX as u64) as u32);
}
let q = plat.percentiles();
let p50 = q.value_at(Pct::P50);
let p99 = q.value_at(Pct::P99);
assert!(
(90..=115).contains(&p50),
"empirical p50 {p50} ≈ configured 100µs (tight band: rejects a sampler \
bit-shift / lost-scale bug, absorbs only log-bucketing + seed noise)",
);
assert!(
(850..=1200).contains(&p99),
"empirical p99 {p99} ≈ configured 1000µs (tight band: rejects a ~±50% \
sampler error, absorbs only log-bucketing + seed noise)",
);
}
#[test]
fn taobench_service_sampler_clamps_pathological_tail() {
let mut rng = Rng::new(7);
let v = sample_service_us(&mut rng, 1e12, -0.5);
assert_eq!(v, MAX_SERVICE_US, "huge draw clamps to MAX_SERVICE_US");
}
#[test]
fn taobench_service_sampler_is_deterministic_from_seed() {
let cfg = TaobenchConfig::default()
.slow_path_sleep_us(100)
.slow_path_p99_us(1000);
let (scale, inv_neg_alpha) = cfg.resolve_service_pareto().unwrap();
let mut a = Rng::new(42);
let mut b = Rng::new(42);
for _ in 0..1000 {
assert_eq!(
sample_service_us(&mut a, scale, inv_neg_alpha),
sample_service_us(&mut b, scale, inv_neg_alpha),
);
}
}
#[test]
fn rng_f64_open01_is_in_open_closed_unit_interval() {
let mut rng = Rng::new(0xF00D_BEEF);
for _ in 0..1_000_000 {
let u = rng.f64_open01();
assert!(
u > 0.0 && u <= 1.0,
"f64_open01 yielded {u}, outside (0, 1]"
);
}
}
#[test]
fn heavy_tail_service_model_reproduces_configured_quantiles() {
let heavy = TaobenchConfig::default()
.slow_path_sleep_us(100)
.slow_path_p99_us(2000);
let (scale, inv_neg_alpha) = heavy
.resolve_service_pareto()
.expect("p99 2000µs above median 100µs resolves to a Pareto tail");
for (p50, p99, why) in [
(100u64, 0u64, "p99 == 0 keeps the fixed path"),
(100, 100, "p99 == median is not a tail"),
(0, 2000, "zero median has no Pareto scale"),
] {
assert!(
TaobenchConfig::default()
.slow_path_sleep_us(p50)
.slow_path_p99_us(p99)
.resolve_service_pareto()
.is_none(),
"{why}",
);
}
let mut rng = Rng::new(0x00C0_FFEE);
let mut samples: Vec<u64> = (0..200_000)
.map(|_| sample_service_us(&mut rng, scale, inv_neg_alpha))
.collect();
samples.sort_unstable();
let q = |p: f64| samples[((samples.len() as f64 * p) as usize).min(samples.len() - 1)];
let (p50, p99, max) = (q(0.50), q(0.99), *samples.last().unwrap());
assert!(
(50..=200).contains(&p50),
"empirical median {p50}µs should track the configured 100µs",
);
assert!(
(1000..=4000).contains(&p99),
"empirical p99 {p99}µs should track the configured 2000µs",
);
assert!(
max > p99.saturating_mul(2),
"max {max}µs must dwarf p99 {p99}µs — the Pareto tail the fixed path lacks",
);
}
}