sqlitegraph 2.2.2

Embedded graph database with full ACID transactions, HNSW vector search, dual backend support, and comprehensive graph algorithms library
Documentation
//! Non-chain pattern regression benchmark for pub/sub.
//!
//! Validates that Star, Random, and Tree graph traversals stay within 10% of baseline.
//! This ensures pub/sub emission doesn't degrade non-chain traversal patterns.

use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use rand::Rng;
use rand::SeedableRng;
use sqlitegraph::backend::SubscriptionFilter;
use sqlitegraph::{EdgeSpec, GraphConfig, NodeSpec, open_graph, snapshot::SnapshotId};

mod bench_utils;
use bench_utils::{MEASURE, WARM_UP, create_benchmark_temp_dir};

/// Create a star graph (1 center, N neighbors)
fn create_star_graph(size: usize) -> (tempfile::TempDir, std::path::PathBuf, i64) {
    let temp_dir = create_benchmark_temp_dir();
    let db_path = temp_dir.path().join("benchmark.db");

    let graph = open_graph(&db_path, &GraphConfig::native()).expect("Failed to create graph");

    let mut node_ids = Vec::with_capacity(size + 1);

    // Create nodes
    for i in 0..=size {
        let node_id = graph
            .insert_node(NodeSpec {
                kind: "Node".to_string(),
                name: format!("node_{}", i),
                file_path: None,
                data: serde_json::json!({"id": i}),
            })
            .expect("Failed to insert node");
        node_ids.push(node_id);
    }

    // Create star edges (center to all others)
    let center = node_ids[0];
    for i in 1..=size {
        graph
            .insert_edge(EdgeSpec {
                from: center,
                to: node_ids[i],
                edge_type: "star".to_string(),
                data: serde_json::json!({"spoke": i}),
            })
            .expect("Failed to insert edge");
    }

    (temp_dir, db_path, center)
}

/// Create a random graph
fn create_random_graph(
    size: usize,
    edge_count: usize,
) -> (tempfile::TempDir, std::path::PathBuf, i64) {
    let temp_dir = create_benchmark_temp_dir();
    let db_path = temp_dir.path().join("benchmark.db");

    let graph = open_graph(&db_path, &GraphConfig::native()).expect("Failed to create graph");

    let mut node_ids = Vec::with_capacity(size);

    // Create nodes
    for i in 0..size {
        let node_id = graph
            .insert_node(NodeSpec {
                kind: "Node".to_string(),
                name: format!("node_{}", i),
                file_path: None,
                data: serde_json::json!({"id": i}),
            })
            .expect("Failed to insert node");
        node_ids.push(node_id);
    }

    // Create random edges
    let mut rng = rand::rngs::StdRng::seed_from_u64(0xA17C);
    for _ in 0..edge_count {
        let from_idx = rng.gen_range(0..size);
        let mut to_idx = rng.gen_range(0..size);
        while to_idx == from_idx {
            to_idx = rng.gen_range(0..size);
        }

        let _ = graph.insert_edge(EdgeSpec {
            from: node_ids[from_idx],
            to: node_ids[to_idx],
            edge_type: "random".to_string(),
            data: serde_json::json!({"random_id": rng.r#gen::<u64>()}),
        });
    }

    (temp_dir, db_path, node_ids[0])
}

/// Create a tree graph (branching factor 3)
fn create_tree_graph(size: usize) -> (tempfile::TempDir, std::path::PathBuf, i64) {
    let temp_dir = create_benchmark_temp_dir();
    let db_path = temp_dir.path().join("benchmark.db");

    let graph = open_graph(&db_path, &GraphConfig::native()).expect("Failed to create graph");

    let mut node_ids = Vec::with_capacity(size);

    // Create nodes
    for i in 0..size {
        let node_id = graph
            .insert_node(NodeSpec {
                kind: "Node".to_string(),
                name: format!("node_{}", i),
                file_path: None,
                data: serde_json::json!({"id": i}),
            })
            .expect("Failed to insert node");
        node_ids.push(node_id);
    }

    // Create tree edges (branching factor 3)
    let mut parent_idx = 0;
    let mut child_idx = 1;
    while child_idx < size && parent_idx < size {
        // Add up to 3 children per parent
        for _ in 0..3 {
            if child_idx >= size {
                break;
            }
            let _ = graph.insert_edge(EdgeSpec {
                from: node_ids[parent_idx],
                to: node_ids[child_idx],
                edge_type: "tree".to_string(),
                data: serde_json::json!({"parent": parent_idx, "child": child_idx}),
            });
            child_idx += 1;
        }
        parent_idx += 1;
    }

    (temp_dir, db_path, node_ids[0])
}

/// Benchmark BFS on star graphs without pub/sub
///
/// Star graphs have high degree at center, testing degree handling.
/// This establishes a baseline without pub/sub subscribers.
fn bench_star_baseline(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("non_chain_star_baseline");
    group.measurement_time(MEASURE);
    group.warm_up_time(WARM_UP);

    for &size in &[100, 500, 1000] {
        group.throughput(Throughput::Elements(size as u64));

        group.bench_with_input(BenchmarkId::new("star_bfs", size), &size, |b, &_size| {
            b.iter(|| {
                let (_temp_dir, db_path, start_node) = create_star_graph(size);

                let graph =
                    open_graph(&db_path, &GraphConfig::native()).expect("Failed to open graph");

                let _result = graph.bfs(SnapshotId::current(), start_node, size as u32);

                std::mem::forget(_temp_dir);
            });
        });
    }

    group.finish();
}

/// Benchmark BFS on star graphs with pub/sub subscribers
///
/// Measures traversal time with pub/sub enabled but subscribers dropped.
/// Tests emit() overhead on non-chain patterns.
fn bench_star_with_pubsub(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("non_chain_star_pubsub");
    group.measurement_time(MEASURE);
    group.warm_up_time(WARM_UP);

    for &size in &[100, 500, 1000] {
        group.throughput(Throughput::Elements(size as u64));

        group.bench_with_input(BenchmarkId::new("star_bfs", size), &size, |b, &_size| {
            b.iter(|| {
                let (_temp_dir, db_path, start_node) = create_star_graph(size);

                let graph =
                    open_graph(&db_path, &GraphConfig::native()).expect("Failed to open graph");

                // Subscribe 5 receivers and drop them (test emit overhead)
                for _ in 0..5 {
                    let (_id, _rx) = graph
                        .subscribe(SubscriptionFilter::all())
                        .expect("Failed to subscribe");
                }

                let _result = graph.bfs(SnapshotId::current(), start_node, size as u32);

                std::mem::forget(_temp_dir);
            });
        });
    }

    group.finish();
}

/// Benchmark BFS on random graphs without pub/sub
///
/// Random graphs have mixed degrees, testing general traversal.
/// This establishes a baseline without pub/sub subscribers.
fn bench_random_baseline(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("non_chain_random_baseline");
    group.measurement_time(MEASURE);
    group.warm_up_time(WARM_UP);

    for &size in &[100, 500, 1000] {
        let edge_count = size * 2; // 2x edges for connectivity

        group.throughput(Throughput::Elements(size as u64));

        group.bench_with_input(BenchmarkId::new("random_bfs", size), &size, |b, &_size| {
            b.iter(|| {
                let (_temp_dir, db_path, start_node) = create_random_graph(size, edge_count);

                let graph =
                    open_graph(&db_path, &GraphConfig::native()).expect("Failed to open graph");

                let _result = graph.bfs(SnapshotId::current(), start_node, size as u32);

                std::mem::forget(_temp_dir);
            });
        });
    }

    group.finish();
}

/// Benchmark BFS on random graphs with pub/sub subscribers
///
/// Measures traversal time with pub/sub enabled.
/// Tests emit() overhead on random patterns.
fn bench_random_with_pubsub(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("non_chain_random_pubsub");
    group.measurement_time(MEASURE);
    group.warm_up_time(WARM_UP);

    for &size in &[100, 500, 1000] {
        let edge_count = size * 2;

        group.throughput(Throughput::Elements(size as u64));

        group.bench_with_input(BenchmarkId::new("random_bfs", size), &size, |b, &_size| {
            b.iter(|| {
                let (_temp_dir, db_path, start_node) = create_random_graph(size, edge_count);

                let graph =
                    open_graph(&db_path, &GraphConfig::native()).expect("Failed to open graph");

                // Subscribe 5 receivers and drop them
                for _ in 0..5 {
                    let (_id, _rx) = graph
                        .subscribe(SubscriptionFilter::all())
                        .expect("Failed to subscribe");
                }

                let _result = graph.bfs(SnapshotId::current(), start_node, size as u32);

                std::mem::forget(_temp_dir);
            });
        });
    }

    group.finish();
}

/// Benchmark BFS on tree graphs without pub/sub
///
/// Tree graphs have branching but no cycles, testing fallback behavior.
/// This establishes a baseline without pub/sub subscribers.
fn bench_tree_baseline(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("non_chain_tree_baseline");
    group.measurement_time(MEASURE);
    group.warm_up_time(WARM_UP);

    for &size in &[100, 500, 1000] {
        group.throughput(Throughput::Elements(size as u64));

        group.bench_with_input(BenchmarkId::new("tree_bfs", size), &size, |b, &_size| {
            b.iter(|| {
                let (_temp_dir, db_path, start_node) = create_tree_graph(size);

                let graph =
                    open_graph(&db_path, &GraphConfig::native()).expect("Failed to open graph");

                let _result = graph.bfs(SnapshotId::current(), start_node, size as u32);

                std::mem::forget(_temp_dir);
            });
        });
    }

    group.finish();
}

/// Benchmark BFS on tree graphs with pub/sub subscribers
///
/// Measures traversal time with pub/sub enabled.
/// Tests emit() overhead on tree patterns.
fn bench_tree_with_pubsub(criterion: &mut Criterion) {
    let mut group = criterion.benchmark_group("non_chain_tree_pubsub");
    group.measurement_time(MEASURE);
    group.warm_up_time(WARM_UP);

    for &size in &[100, 500, 1000] {
        group.throughput(Throughput::Elements(size as u64));

        group.bench_with_input(BenchmarkId::new("tree_bfs", size), &size, |b, &_size| {
            b.iter(|| {
                let (_temp_dir, db_path, start_node) = create_tree_graph(size);

                let graph =
                    open_graph(&db_path, &GraphConfig::native()).expect("Failed to open graph");

                // Subscribe 5 receivers and drop them
                for _ in 0..5 {
                    let (_id, _rx) = graph
                        .subscribe(SubscriptionFilter::all())
                        .expect("Failed to subscribe");
                }

                let _result = graph.bfs(SnapshotId::current(), start_node, size as u32);

                std::mem::forget(_temp_dir);
            });
        });
    }

    group.finish();
}

criterion_group!(
    benches,
    bench_star_baseline,
    bench_star_with_pubsub,
    bench_random_baseline,
    bench_random_with_pubsub,
    bench_tree_baseline,
    bench_tree_with_pubsub
);
criterion_main!(benches);