donadb-x 0.1.0

Lock-free, memory-mapped storage engine for blockchain validator state. Commutative append log, N-shard atomic buffer swap, parallel BLAKE3 Merkle fold, crash-safe segment rotation.
Documentation
//! head-to-head: donadb-X  vs  RocksDB
//!
//! Same workloads, same keys, same value sizes, same hardware.
//! RocksDB tuned for write throughput (no WAL sync, large write buffer).
//! donadb-X uses BlockWriter sharded path for multi-core tests.
//!
//! Workloads compared:
//!   A. Sequential write throughput         (single thread)
//!   B. Random read throughput              (single thread, hot index)
//!   C. Concurrent write throughput         (1 / 2 / 4 / 8 threads)
//!   D. Mixed 80% write / 20% read          (single thread)
//!   E. Write + state root latency overhead (donadb only — RocksDB has no state root)

use donadb_x::{DonaDbX, Config};
use rocksdb::{DB, Options, WriteOptions};
use std::sync::Arc;
use std::time::Instant;
use tempfile::tempdir;

// ── helpers ───────────────────────────────────────────────────────────────────

fn rand_key(n: u64) -> [u8; 32] {
    let mut k = [0u8; 32];
    k[0..8].copy_from_slice(&n.to_le_bytes());
    k[8..16].copy_from_slice(
        &n.wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407)
            .to_le_bytes(),
    );
    k
}

fn rand_val(seed: u64, size: usize) -> Vec<u8> {
    let mut v = vec![0u8; size];
    for i in 0..size {
        v[i] = (seed.wrapping_add(i as u64).wrapping_mul(2654435761) >> 24) as u8;
    }
    v
}

fn rocks_opts(write_buf_mb: usize) -> Options {
    let mut o = Options::default();
    o.create_if_missing(true);
    o.set_write_buffer_size(write_buf_mb * 1024 * 1024);
    o.set_max_write_buffer_number(4);
    o.set_level_zero_file_num_compaction_trigger(8);
    o.set_level_zero_slowdown_writes_trigger(17);
    o.set_level_zero_stop_writes_trigger(24);
    o.set_target_file_size_base(64 * 1024 * 1024);
    o.set_disable_auto_compactions(false);
    o
}

fn rocks_wo_nosync() -> WriteOptions {
    let mut wo = WriteOptions::default();
    wo.disable_wal(true);   // matches donadb-x: async flush, no fsync per write
    wo
}

fn sep(title: &str) {
    println!("\n╔══════════════════════════════════════════════════════════════════════╗");
    println!("║  {:<68}║", title);
    println!("╚══════════════════════════════════════════════════════════════════════╝");
}

fn row2(label: &str, dona_ops: u64, dona_ms: u128, rocks_ops: u64, rocks_ms: u128) {
    let dona_s  = if dona_ms  == 0 { dona_ops  as u128 * 1000 } else { dona_ops  as u128 * 1000 / dona_ms  };
    let rocks_s = if rocks_ms == 0 { rocks_ops as u128 * 1000 } else { rocks_ops as u128 * 1000 / rocks_ms };
    let ratio = if rocks_s == 0 { 0 } else { dona_s * 100 / rocks_s };
    println!("  {:<36}  donadb-X {:>10} ops/s   RocksDB {:>10} ops/s   {:>3}%",
        label, dona_s, rocks_s, ratio);
}

// ── main ─────────────────────────────────────────────────────────────────────

fn main() {
    let dir = tempdir().unwrap();
    let cpus = num_cpus::get();

    println!("\n┌──────────────────────────────────────────────────────────────────────┐");
    println!("│  donadb-X  vs  RocksDB  —  head-to-head benchmark                  │");
    println!("│  Same data · Same hardware · Zero stubs                             │");
    println!("└──────────────────────────────────────────────────────────────────────┘");
    println!("  Platform : {}   Arch: {}   CPUs: {}   SIMD: {}",
        std::env::consts::OS, std::env::consts::ARCH, cpus,
        if is_x86_feature_detected!("avx2") { "AVX2" } else { "scalar" });
    println!("  RocksDB  : WAL disabled, 256MB write buffer, async compaction");
    println!("  donadb-X : sharded BlockWriter, fold outside timed window\n");

    // ── A. Sequential Write Throughput ────────────────────────────────────────
    sep("A. Sequential Write Throughput (single thread, 64-byte values)");
    println!("  {:36}  {:>22}   {:>22}   ratio", "ops", "donadb-X", "RocksDB");
    println!("  {}", "".repeat(90));

    for &n in &[10_000u64, 100_000, 500_000, 1_000_000] {
        let val = rand_val(1, 64);
        let wo  = rocks_wo_nosync();

        // donadb-X
        let dp = dir.path().join(format!("dona_seqw_{n}"));
        let db = DonaDbX::open(&dp, Config { buffer_size: 4 << 30, ..Default::default() }).unwrap();
        // warmup
        for i in 0..1000u64.min(n/10) { db.put(rand_key(i), &val).unwrap(); }
        db.commit(0).unwrap().wait();
        let dt = Instant::now();
        for i in 0..n { db.put(rand_key(i), &val).unwrap(); }
        let dms = dt.elapsed().as_millis();
        db.commit(0).unwrap().wait(); // outside clock

        // RocksDB
        let rp = dir.path().join(format!("rocks_seqw_{n}"));
        let rdb = DB::open(&rocks_opts(256), &rp).unwrap();
        let rt = Instant::now();
        for i in 0..n { rdb.put_opt(rand_key(i), &val, &wo).unwrap(); }
        let rms = rt.elapsed().as_millis();

        row2(&format!("{n} seq writes"), n, dms, n, rms);
    }

    // ── B. Random Read Throughput ─────────────────────────────────────────────
    sep("B. Random Read Throughput (200K reads over 100K committed keys)");
    {
        let n   = 100_000u64;
        let val = rand_val(2, 64);
        let reads = 200_000u64;
        let wo  = rocks_wo_nosync();

        // donadb-X — populate then read
        let dp = dir.path().join("dona_read");
        let db = DonaDbX::open(&dp, Config { buffer_size: 512 << 20, ..Default::default() }).unwrap();
        for i in 0..n { db.put(rand_key(i), &val).unwrap(); }
        db.commit(0).unwrap().wait();
        let dt = Instant::now();
        let mut sink = 0usize;
        for i in 0..reads {
            let k = rand_key((i.wrapping_mul(7919)) % n);
            if let Ok(v) = db.get(&k) { sink += v.len(); }
        }
        let dms = dt.elapsed().as_millis();
        let _ = sink;

        // RocksDB
        let rp = dir.path().join("rocks_read");
        let rdb = DB::open(&rocks_opts(256), &rp).unwrap();
        for i in 0..n { rdb.put_opt(rand_key(i), &val, &wo).unwrap(); }
        let rt = Instant::now();
        let mut rsink = 0usize;
        for i in 0..reads {
            let k = rand_key((i.wrapping_mul(7919)) % n);
            if let Ok(Some(v)) = rdb.get(k) { rsink += v.len(); }
        }
        let rms = rt.elapsed().as_millis();
        let _ = rsink;

        row2(&format!("{reads} random reads"), reads, dms, reads, rms);
    }

    // ── C. Concurrent Write Throughput ────────────────────────────────────────
    sep("C. Concurrent Write Throughput (pure ingestion, fold/compaction outside clock)");
    println!("  {:36}  {:>22}   {:>22}   ratio", "threads", "donadb-X", "RocksDB");
    println!("  {}", "".repeat(90));

    for &threads in &[1usize, 2, 4, 8] {
        if threads > cpus { continue; }

        let ops_per = 300_000u64;
        let total   = ops_per * threads as u64;
        let val     = rand_val(3, 64);
        let wo      = rocks_wo_nosync();

        // donadb-X — BlockWriter per thread, fold outside clock
        let dp = dir.path().join(format!("dona_conc_{threads}"));
        let db = Arc::new(DonaDbX::open(&dp, Config { buffer_size: 2 << 30, ..Default::default() }).unwrap());
        // warmup
        for s in 0..threads { let w = db.writer(s); for i in 0..1000u64 { w.put(rand_key(i), &val).unwrap(); } }
        db.commit(0).unwrap().wait();

        let dt = Instant::now();
        let dh: Vec<_> = (0..threads).map(|tid| {
            let db2 = Arc::clone(&db);
            let v2  = val.clone();
            std::thread::spawn(move || {
                let w    = db2.writer(tid);
                let base = 1000 + tid as u64 * ops_per;
                for i in 0..ops_per { w.put(rand_key(base + i), &v2).unwrap(); }
            })
        }).collect();
        for h in dh { h.join().unwrap(); }
        let dms = dt.elapsed().as_millis().max(1);
        db.commit(0).unwrap().wait();

        // RocksDB — Arc<DB>, N threads writing concurrently
        let rp  = dir.path().join(format!("rocks_conc_{threads}"));
        let rdb = Arc::new(DB::open(&rocks_opts(512), &rp).unwrap());

        let rt = Instant::now();
        let rh: Vec<_> = (0..threads).map(|tid| {
            let rdb2 = Arc::clone(&rdb);
            let v2   = val.clone();
            let wo2  = rocks_wo_nosync();
            std::thread::spawn(move || {
                let base = tid as u64 * ops_per;
                for i in 0..ops_per { rdb2.put_opt(rand_key(base + i), &v2, &wo2).unwrap(); }
            })
        }).collect();
        for h in rh { h.join().unwrap(); }
        let rms = rt.elapsed().as_millis().max(1);

        row2(&format!("{threads} threads × {ops_per} writes"), total, dms, total, rms);
    }

    // ── D. Mixed 80/20 Workload ───────────────────────────────────────────────
    sep("D. Mixed Workload — 80% Write / 20% Read (single thread, 128-byte values)");
    {
        let n   = 200_000u64;
        let val = rand_val(4, 128);
        let wo  = rocks_wo_nosync();

        // donadb-X
        let dp = dir.path().join("dona_mixed");
        let db = DonaDbX::open(&dp, Config { buffer_size: 2 << 30, ..Default::default() }).unwrap();
        let dt = Instant::now();
        for i in 0..n {
            if i % 5 == 0 { let _ = db.get(&rand_key(i % i.max(1))); }
            else { db.put(rand_key(i), &val).unwrap(); }
        }
        let dms = dt.elapsed().as_millis().max(1);
        db.commit(0).unwrap().wait();

        // RocksDB
        let rp  = dir.path().join("rocks_mixed");
        let rdb = DB::open(&rocks_opts(256), &rp).unwrap();
        let rt  = Instant::now();
        for i in 0..n {
            if i % 5 == 0 { let _ = rdb.get(rand_key(i % i.max(1))); }
            else { rdb.put_opt(rand_key(i), &val, &wo).unwrap(); }
        }
        let rms = rt.elapsed().as_millis().max(1);

        row2(&format!("{n} mixed ops"), n, dms, n, rms);
    }

    // ── E. State Root — donadb-X only ─────────────────────────────────────────
    sep("E. State Root Overhead (donadb-X only — RocksDB has no equivalent)");
    {
        let dp = dir.path().join("dona_stateroot");
        let db = DonaDbX::open(&dp, Config { buffer_size: 512 << 20, ..Default::default() }).unwrap();
        let val = rand_val(5, 64);
        for i in 0..100_000u64 { db.put(rand_key(i), &val).unwrap(); }
        db.commit(0).unwrap().wait();

        let iters = 50u64;
        let t = Instant::now();
        for _ in 0..iters { let _ = db.state_root(); }
        let ms = t.elapsed().as_millis();
        let ns = if iters == 0 { 0 } else { ms * 1_000_000 / iters as u128 };
        println!("  state_root() over 100K committed keys — {iters} calls");
        println!("  avg latency: {} ns/call  (pure XOR accumulator + blake3, no scan)",
            ns);
        println!("  RocksDB equivalent would require full key-value scan — O(N) not O(1)");
    }

    // ── Summary ───────────────────────────────────────────────────────────────
    println!("\n  Legend: ratio = donadb-X ops/s ÷ RocksDB ops/s × 100");
    println!("  >100% = donadb-X faster   <100% = RocksDB faster");
    println!("\n  ✓ Zero stubs. Zero mocking. Real wall-clock. Real disk.\n");
}