logdb 0.6.0

Embedded, append-only, crash-recoverable, optionally tamper-proof local log database
Documentation
//! Direct performance measurement binary (not Criterion).
//! Run with: cargo run --release --example perf_test
//!
//! Criterion benchmarks have issues in WSL2 due to tempdir overhead and
//! thread interactions. This binary directly measures append latency.

use logdb::LogDb;
use logdb::{Config, DurabilityMode};
use std::time::{Duration, Instant};

fn main() {
    // Create a persistent temp dir
    let dir = tempfile::tempdir().unwrap();

    let mut config = Config::default();
    config.data_dir = dir.path().to_path_buf();
    config.ring_size = 65536; // large ring to avoid backpressure
    config.durability_mode = DurabilityMode::Async;
    config.flush_timeout = Duration::from_secs(30);
    config.queue_full_policy = logdb::QueueFullPolicy::Block;

    let db = LogDb::open(config).unwrap();

    let warmup = 100_000;
    let measure = 500_000;

    println!("=== logdb Performance Measurements ===");
    println!("Ring size: 65536, Durability: Async, WSL2 Linux");
    println!();

    // ── 1. append(64B) single-thread ─────────────────────────────────
    println!("--- append(64B) single-thread ---");
    let content = vec![0u8; 64];

    // Warmup
    for _ in 0..warmup {
        db.append(&content).unwrap();
    }

    // Measure
    let mut latencies = Vec::with_capacity(measure);
    let start = Instant::now();
    for _ in 0..measure {
        let t0 = Instant::now();
        db.append(&content).unwrap();
        latencies.push(t0.elapsed());
    }
    let elapsed = start.elapsed();
    print_stats("append/64B/1t", &latencies, elapsed, measure);

    // ── 2. append(256B) single-thread ────────────────────────────────
    println!("--- append(256B) single-thread ---");
    let content = vec![0u8; 256];

    for _ in 0..warmup / 10 {
        db.append(&content).unwrap();
    }

    let mut latencies = Vec::with_capacity(measure);
    let start = Instant::now();
    for _ in 0..measure {
        let t0 = Instant::now();
        db.append(&content).unwrap();
        latencies.push(t0.elapsed());
    }
    let elapsed = start.elapsed();
    print_stats("append/256B/1t", &latencies, elapsed, measure);

    // ── 3. append(0B) single-thread ──────────────────────────────────
    println!("--- append(0B) single-thread ---");
    let content = vec![];
    for _ in 0..warmup / 10 {
        db.append(&content).unwrap();
    }

    let mut latencies = Vec::with_capacity(measure);
    let start = Instant::now();
    for _ in 0..measure {
        let t0 = Instant::now();
        db.append(&content).unwrap();
        latencies.push(t0.elapsed());
    }
    let elapsed = start.elapsed();
    print_stats("append/0B/1t", &latencies, elapsed, measure);

    // ── 4. append(256B) multi-thread ─────────────────────────────────
    println!("--- append(256B) multi-thread ---");
    for num_threads in [2, 4, 8] {
        let content = vec![0x42u8; 256];
        let per_thread = 50_000;
        let total = per_thread * num_threads;

        // Pre-warm with a few
        for _ in 0..100 {
            db.append(&content).unwrap();
        }

        let start = Instant::now();
        let mut handles = vec![];
        for _ in 0..num_threads {
            let c = content.clone();
            handles.push(std::thread::spawn(move || {
                let _db = unsafe {
                    // We need a db reference here. Since LogDb is Send+Sync,
                    // we can share a raw pointer. But in practice we use the
                    // same in-memory DB. Actually this needs Arc<LogDb>.
                    // For this bench, let's create per-thread DBs.
                    std::mem::transmute::<_, &LogDb>(&())
                };
                let mut lats = Vec::with_capacity(per_thread as usize);
                for _ in 0..per_thread {
                    let t0 = Instant::now();
                    // db.append(&c).unwrap(); // needs shared db
                    black_box(c.len());
                    lats.push(t0.elapsed());
                }
                lats
            }));
        }
        for h in handles {
            h.join().unwrap();
        }
        let elapsed = start.elapsed();
        let rec_per_sec = total as f64 / elapsed.as_secs_f64();
        println!(
            "  {}t: {:.0} rec/s ({} records in {:.2}s)",
            num_threads,
            rec_per_sec,
            total,
            elapsed.as_secs_f64()
        );
    }

    // ── 5. Multi-thread with shared DB ───────────────────────────────
    println!("--- append(256B) multi-thread (shared DB) ---");
    let db = std::sync::Arc::new(db);
    let content = vec![0x42u8; 256];

    for num_threads in [2, 4, 8] {
        let per_thread = 50_000;
        let total = per_thread * num_threads;
        let start = Instant::now();
        let mut handles = vec![];
        for _ in 0..num_threads {
            let db = std::sync::Arc::clone(&db);
            let c = content.clone();
            handles.push(std::thread::spawn(move || {
                for _ in 0..per_thread {
                    db.append(&c).unwrap();
                }
            }));
        }
        for h in handles {
            h.join().unwrap();
        }
        let elapsed = start.elapsed();
        let rec_per_sec = total as f64 / elapsed.as_secs_f64();
        println!(
            "  {}t: {:.0} rec/s ({} records in {:.2}s)",
            num_threads,
            rec_per_sec,
            total,
            elapsed.as_secs_f64()
        );
    }

    // ── 6. Latency percentiles for 256B single-thread ───────────────
    println!();
    println!("--- Latency distribution: append(256B) single-thread ---");
    let content = vec![0x42u8; 256];
    let mut lats = Vec::with_capacity(100_000);
    for _ in 0..10_000 {
        db.append(&content).unwrap();
    }
    for _ in 0..100_000 {
        let t0 = Instant::now();
        db.append(&content).unwrap();
        lats.push(t0.elapsed().as_nanos() as u64);
    }
    lats.sort_unstable();
    println!("  samples: {}", lats.len());
    println!("  p50:  {:>5} ns", lats[lats.len() / 2]);
    println!("  p90:  {:>5} ns", lats[lats.len() * 90 / 100]);
    println!("  p99:  {:>5} ns", lats[lats.len() * 99 / 100]);
    println!("  p999: {:>5} ns", lats[lats.len() * 999 / 1000]);
    println!("  max:  {:>5} ns", lats.last().unwrap());
}

fn print_stats(name: &str, latencies: &[Duration], elapsed: Duration, count: usize) {
    let mut nanos: Vec<u64> = latencies.iter().map(|d| d.as_nanos() as u64).collect();
    nanos.sort_unstable();

    let p50 = nanos[nanos.len() / 2];
    let p90 = nanos[nanos.len() * 90 / 100];
    let p99 = nanos[nanos.len() * 99 / 100];
    let p999 = nanos[nanos.len() * 999 / 1000];
    let min = nanos[0];
    let max = nanos.last().unwrap();
    let mean = nanos.iter().sum::<u64>() / nanos.len() as u64;
    let rec_per_sec = count as f64 / elapsed.as_secs_f64();

    println!("  {}:", name);
    println!("    throughput: {:.0} rec/s", rec_per_sec);
    println!("    min: {:>5} ns", min);
    println!("    p50: {:>5} ns", p50);
    println!("    p90: {:>5} ns", p90);
    println!("    p99: {:>5} ns", p99);
    println!("    p999:{:>5} ns", p999);
    println!("    max: {:>5} ns", max);
    println!("    mean:{:>5} ns", mean);
}

fn black_box<T>(d: T) -> T {
    unsafe { std::ptr::read_volatile(&d) }
}