Skip to main content

bench_embed/
bench_embed.rs

1//! In-process throughput micro-bench for `kevy_embedded::Store` — the path an
2//! embed consumer (e.g. mailrs) actually pays per op: the embedded mutex + the
3//! keyspace op + (optionally) an AOF append. No socket, no reactor, no network
4//! round-trip — so absolute numbers are much higher than the TCP server bench;
5//! they measure the in-process data path, not the wire.
6//!
7//! Run: `cargo run -p kevy-embedded --example bench_embed --release`
8//! Override op count with `KEVY_BENCH_N` (default 2_000_000).
9
10use kevy_embedded::{AppendFsync, Config, Store};
11use std::time::Instant;
12
13const KEYS: usize = 256;
14const VAL: &[u8] = b"value-payload-16";
15
16fn bench(label: &str, store: &Store, n: usize, keys: &[Vec<u8>]) {
17    // Warm the keyspace so GET is all hits and allocations are amortized.
18    for k in keys {
19        store.set(k, VAL).unwrap();
20    }
21
22    let t = Instant::now();
23    for i in 0..n {
24        store.set(&keys[i % KEYS], VAL).unwrap();
25    }
26    let set_s = t.elapsed().as_secs_f64();
27
28    let t = Instant::now();
29    let mut hits = 0usize;
30    for i in 0..n {
31        if store.get(&keys[i % KEYS]).unwrap().is_some() {
32            hits += 1;
33        }
34    }
35    let get_s = t.elapsed().as_secs_f64();
36    std::hint::black_box(hits);
37
38    println!(
39        "[{label:<13}] SET {:>10.0} ops/s   GET {:>10.0} ops/s",
40        n as f64 / set_s,
41        n as f64 / get_s
42    );
43}
44
45fn main() {
46    let n: usize = std::env::var("KEVY_BENCH_N")
47        .ok()
48        .and_then(|s| s.parse().ok())
49        .unwrap_or(2_000_000);
50    // Keys precomputed outside the timed loop so `format!`/alloc cost isn't
51    // attributed to kevy.
52    let keys: Vec<Vec<u8>> = (0..KEYS).map(|i| format!("k{i}").into_bytes()).collect();
53
54    println!("kevy-embedded in-process throughput — single thread, n={n}, {KEYS} keys, {}B val", VAL.len());
55
56    let s1 = Store::open(Config::default().with_ttl_reaper_manual()).unwrap();
57    bench("in-memory", &s1, n, &keys);
58
59    let dir2 = std::env::temp_dir().join("kevy_embed_bench_everysec");
60    let _ = std::fs::remove_dir_all(&dir2);
61    let s2 = Store::open(
62        Config::default()
63            .with_persist(&dir2)
64            .with_ttl_reaper_manual()
65            .with_appendfsync(AppendFsync::EverySec),
66    )
67    .unwrap();
68    bench("aof-everysec", &s2, n, &keys);
69
70    let dir3 = std::env::temp_dir().join("kevy_embed_bench_always");
71    let _ = std::fs::remove_dir_all(&dir3);
72    let s3 = Store::open(
73        Config::default()
74            .with_persist(&dir3)
75            .with_ttl_reaper_manual()
76            .with_appendfsync(AppendFsync::Always),
77    )
78    .unwrap();
79    // Always-fsync is one fdatasync per write (no group commit on the embedded
80    // single-op path) — fsync-rate-bound, so run far fewer ops to stay bounded.
81    bench("aof-always", &s3, (n / 20).max(50_000), &keys);
82
83    // TTL'd-key GET: the mailrs path (every cache key has a TTL). With the
84    // background reaper the cached clock is trusted (no per-get Instant::now);
85    // manual mode reads a fresh clock per get — the gap is the cached-clock win.
86    bench_ttl_get("ttl GET (cached clk)", false, n, &keys); // background reaper
87    bench_ttl_get("ttl GET (fresh clk) ", true, n, &keys); // manual reaper
88
89    drop((s1, s2, s3));
90    let _ = std::fs::remove_dir_all(&dir2);
91    let _ = std::fs::remove_dir_all(&dir3);
92}
93
94/// GET throughput over keys that all carry a (long) TTL — the mailrs cache
95/// shape. `manual_reaper` toggles whether the store trusts the cached clock
96/// (background) or reads a fresh clock per get (manual).
97fn bench_ttl_get(label: &str, manual_reaper: bool, n: usize, keys: &[Vec<u8>]) {
98    let cfg = if manual_reaper {
99        Config::default().with_ttl_reaper_manual()
100    } else {
101        Config::default() // background reaper (default) → cached clock trusted
102    };
103    let store = Store::open(cfg).unwrap();
104    let ttl = std::time::Duration::from_secs(3600); // never expires during the run
105    for k in keys {
106        store.set_with_ttl(k, VAL, ttl).unwrap();
107    }
108    let t = Instant::now();
109    let mut hits = 0usize;
110    for i in 0..n {
111        if store.get(&keys[i % KEYS]).unwrap().is_some() {
112            hits += 1;
113        }
114    }
115    std::hint::black_box(hits);
116    println!("[{label}] GET {:>10.0} ops/s", n as f64 / t.elapsed().as_secs_f64());
117}