sqlitegraph 2.2.2

Embedded graph database with full ACID transactions, HNSW vector search, dual backend support, and comprehensive graph algorithms library
Documentation
//! Connection pool performance benchmarks.
//!
//! Measures connection pool performance benefits including:
//! - Checkout latency (first vs subsequent)
//! - Concurrent access patterns
//! - Query throughput comparison (pooled vs direct)
//! - Pool size impact on throughput
//!
//! # Benchmark Results (2026-01-20)
//!
//! ## Checkout Latency
//! - Direct open: ~17-21 µs per connection
//! - First checkout: ~26-32 µs (includes connection creation overhead)
//! - Warm checkout: ~1.7 µs (connection reuse - **12x faster** than direct open)
//!
//! ## Query Throughput (Pooled vs Direct)
//! - 100 queries: Pooled 942µs vs Direct 3940µs (**4.2x faster**)
//! - 500 queries: Pooled 4326µs vs Direct 19593µs (**4.5x faster**)
//! - 1000 queries: Pooled 8806µs vs Direct 40652µs (**4.6x faster**)
//!
//! Throughput: Pooled 106-115 Kelem/s vs Direct 24-25 Kelem/s (**4-5x benefit**)
//!
//! ## Pool Size Impact (8 concurrent threads, 400 queries total)
//! - Pool size 1: 4665µs (severe bottleneck)
//! - Pool size 2: 3286µs (significant improvement)
//! - Pool size 5: 1429µs (good performance)
//! - Pool size 10: 1418µs (optimal)
//! - Pool size 20: 1437µs (no additional benefit)
//!
//! **Optimal pool size: 5-10 connections for 8 concurrent threads**
//!
//! ## Concurrent Access Scaling
//! - 1 thread: 110µs (baseline)
//! - 2 threads: 218µs (near-linear)
//! - 4 threads: 428µs (near-linear)
//! - 8 threads: 823µs (near-linear scaling)
//!
//! The pool demonstrates excellent concurrent access with near-linear scaling
//! across threads and 4-5x throughput improvement over direct connections.

use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
use r2d2::Pool as R2d2Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::Connection;
use std::sync::Arc;
use std::thread;
use std::time::Duration;

mod bench_utils;
use bench_utils::create_benchmark_temp_dir;

const WARM_UP_TIME: Duration = Duration::from_secs(2);
const MEASURE_TIME: Duration = Duration::from_secs(5);

/// Type alias for the r2d2 pool (supports Arc cloning)
type SharedPool = Arc<R2d2Pool<SqliteConnectionManager>>;

/// Setup a test database with a simple schema.
fn setup_test_db(db_path: &std::path::Path) {
    let conn = Connection::open(db_path).unwrap();
    conn.execute(
        "CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, value TEXT)",
        [],
    )
    .unwrap();
    for i in 0..100 {
        conn.execute(
            "INSERT INTO test (value) VALUES (?)",
            [format!("value_{}", i)],
        )
        .unwrap();
    }
}

/// Create a shared r2d2 pool for benchmarking.
fn create_shared_pool(db_path: &std::path::Path, max_size: u32) -> SharedPool {
    let manager = SqliteConnectionManager::file(db_path);
    let pool = R2d2Pool::builder()
        .max_size(max_size)
        .build(manager)
        .unwrap();
    Arc::new(pool)
}

/// Benchmark connection checkout latency.
///
/// Compares:
/// - Direct connection open overhead
/// - First pool checkout (creates new connection)
/// - Subsequent pool checkouts (reuses connections)
fn bench_checkout_latency(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("pool_checkout_latency");
    group.warm_up_time(WARM_UP_TIME);
    group.measurement_time(MEASURE_TIME);

    let temp_dir = create_benchmark_temp_dir();
    let db_path = temp_dir.path().join("benchmark.db");
    setup_test_db(&db_path);

    // Baseline: Direct connection open
    group.bench_function("direct_open", |b| {
        b.iter(|| {
            let conn = Connection::open(&db_path).unwrap();
            black_box(&conn);
            drop(conn);
        })
    });

    // First pool checkout (creates connection)
    group.bench_function("pool_checkout_first", |b| {
        b.iter(|| {
            let temp_dir = create_benchmark_temp_dir();
            let db_path = temp_dir.path().join("first.db");
            setup_test_db(&db_path);
            let pool = create_shared_pool(&db_path, 5);
            let conn = pool.get().unwrap();
            black_box(&conn);
            drop(conn);
        })
    });

    // Subsequent pool checkouts (reuses connections)
    group.bench_function("pool_checkout_warm", |b| {
        let temp_dir = create_benchmark_temp_dir();
        let db_path = temp_dir.path().join("warm.db");
        setup_test_db(&db_path);
        let pool = create_shared_pool(&db_path, 5);
        // Warm up the pool
        {
            let _conn = pool.get().unwrap();
            let _conn2 = pool.get().unwrap();
        }
        b.iter(|| {
            let conn = pool.get().unwrap();
            black_box(&conn);
            drop(conn);
        })
    });

    group.finish();
}

/// Benchmark concurrent connection access.
///
/// Measures checkout performance with multiple threads accessing the pool.
fn bench_concurrent_access(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("pool_concurrent_access");
    group.warm_up_time(WARM_UP_TIME);
    group.measurement_time(MEASURE_TIME);

    for thread_count in [1, 2, 4, 8].iter() {
        group.bench_with_input(
            BenchmarkId::from_parameter(thread_count),
            thread_count,
            |b, &thread_count| {
                b.iter_batched(
                    || {
                        let temp_dir = create_benchmark_temp_dir();
                        let db_path = temp_dir.path().join("concurrent.db");
                        setup_test_db(&db_path);
                        let pool = create_shared_pool(&db_path, 10);
                        (pool, temp_dir)
                    },
                    |(pool, _temp_dir)| {
                        let mut handles = Vec::new();

                        for _ in 0..thread_count {
                            let pool_clone = Arc::clone(&pool);
                            handles.push(thread::spawn(move || {
                                // Each thread performs multiple checkouts
                                for _ in 0..10 {
                                    let conn = pool_clone.get().unwrap();
                                    // Simulate some work
                                    let _: Vec<i64> = conn
                                        .prepare("SELECT id FROM test LIMIT 1")
                                        .unwrap()
                                        .query_map([], |row| row.get(0))
                                        .unwrap()
                                        .collect::<Result<_, _>>()
                                        .unwrap();
                                    drop(conn);
                                }
                            }));
                        }

                        for handle in handles {
                            handle.join().unwrap();
                        }
                    },
                    criterion::BatchSize::LargeInput,
                );
            },
        );
    }

    group.finish();
}

/// Compare query throughput: pooled vs direct connection.
///
/// Runs many simple queries to measure the benefit of connection reuse.
fn bench_query_throughput(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("pool_query_throughput");
    group.warm_up_time(WARM_UP_TIME);
    group.measurement_time(MEASURE_TIME);

    for query_count in [100, 500, 1000].iter() {
        // Pooled connection throughput
        group.throughput(Throughput::Elements(*query_count as u64));
        group.bench_with_input(
            BenchmarkId::new("pooled", query_count),
            query_count,
            |b, &query_count| {
                b.iter_batched(
                    || {
                        let temp_dir = create_benchmark_temp_dir();
                        let db_path = temp_dir.path().join("throughput.db");
                        setup_test_db(&db_path);
                        let pool = create_shared_pool(&db_path, 5);
                        (pool, temp_dir)
                    },
                    |(pool, _temp_dir)| {
                        for _ in 0..query_count {
                            let conn = pool.get().unwrap();
                            let _: Vec<i64> = conn
                                .prepare("SELECT id FROM test LIMIT 1")
                                .unwrap()
                                .query_map([], |row| row.get(0))
                                .unwrap()
                                .collect::<Result<_, _>>()
                                .unwrap();
                            drop(conn);
                        }
                    },
                    criterion::BatchSize::LargeInput,
                );
            },
        );

        // Direct connection throughput (re-opening each time)
        group.throughput(Throughput::Elements(*query_count as u64));
        group.bench_with_input(
            BenchmarkId::new("direct", query_count),
            query_count,
            |b, &query_count| {
                b.iter_batched(
                    || {
                        let temp_dir = create_benchmark_temp_dir();
                        let db_path = temp_dir.path().join("throughput.db");
                        setup_test_db(&db_path);
                        (db_path, temp_dir)
                    },
                    |(db_path, _temp_dir)| {
                        for _ in 0..query_count {
                            let conn = Connection::open(&db_path).unwrap();
                            let _: Vec<i64> = conn
                                .prepare("SELECT id FROM test LIMIT 1")
                                .unwrap()
                                .query_map([], |row| row.get(0))
                                .unwrap()
                                .collect::<Result<_, _>>()
                                .unwrap();
                            drop(conn);
                        }
                    },
                    criterion::BatchSize::LargeInput,
                );
            },
        );
    }

    group.finish();
}

/// Compare different pool sizes for concurrent workload.
///
/// Finds optimal pool size by measuring throughput with varying pool sizes.
fn bench_pool_sizes(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("pool_size_comparison");
    group.warm_up_time(WARM_UP_TIME);
    group.measurement_time(MEASURE_TIME);

    // Concurrent threads fixed at 8, varying pool size
    let concurrent_threads = 8;

    for pool_size in [1, 2, 5, 10, 20].iter() {
        group.bench_with_input(
            BenchmarkId::from_parameter(pool_size),
            pool_size,
            |b, &pool_size| {
                b.iter_batched(
                    || {
                        let temp_dir = create_benchmark_temp_dir();
                        let db_path = temp_dir.path().join("size_test.db");
                        setup_test_db(&db_path);
                        let pool = create_shared_pool(&db_path, pool_size);
                        (pool, temp_dir)
                    },
                    |(pool, _temp_dir)| {
                        let mut handles = Vec::new();

                        for _ in 0..concurrent_threads {
                            let pool_clone = Arc::clone(&pool);
                            handles.push(thread::spawn(move || {
                                // Each thread performs 50 queries
                                for _ in 0..50 {
                                    let conn = pool_clone.get().unwrap();
                                    let _: Vec<String> = conn
                                        .prepare("SELECT value FROM test WHERE id = ?")
                                        .unwrap()
                                        .query_map([1], |row| row.get(0))
                                        .unwrap()
                                        .collect::<Result<_, _>>()
                                        .unwrap();
                                    drop(conn);
                                }
                            }));
                        }

                        for handle in handles {
                            handle.join().unwrap();
                        }
                    },
                    criterion::BatchSize::LargeInput,
                );
            },
        );
    }

    group.finish();
}

criterion_group!(
    benches,
    bench_checkout_latency,
    bench_concurrent_access,
    bench_query_throughput,
    bench_pool_sizes
);
criterion_main!(benches);