heroforge-core 0.2.2

Pure Rust core library for reading and writing Fossil SCM repositories
Documentation
//! QUIC Sync Benchmark
//!
//! Syncs a real Heroforge repository using QUIC protocol.
//!
//! Usage:
//!   cargo run --example quic_sync_benchmark --features sync-quic --release -- /tmp/heroforge-scm.forge

use heroforge_core::sync::quic::{QuicClient, QuicServer};
use heroforge_core::Repository;
use std::env;
use std::path::{Path, PathBuf};
use std::time::Instant;
use tokio::runtime::Runtime;

fn format_bytes(bytes: usize) -> String {
    if bytes >= 1_000_000_000 {
        format!("{:.2} GB", bytes as f64 / 1_000_000_000.0)
    } else if bytes >= 1_000_000 {
        format!("{:.2} MB", bytes as f64 / 1_000_000.0)
    } else if bytes >= 1_000 {
        format!("{:.2} KB", bytes as f64 / 1_000.0)
    } else {
        format!("{} bytes", bytes)
    }
}

fn format_duration(secs: f64) -> String {
    if secs >= 60.0 {
        format!("{:.1} min", secs / 60.0)
    } else if secs >= 1.0 {
        format!("{:.2} sec", secs)
    } else {
        format!("{:.1} ms", secs * 1000.0)
    }
}

fn count_artifacts(repo: &Repository) -> usize {
    let conn = repo.database().connection();
    let count: i64 = conn
        .query_row("SELECT COUNT(*) FROM blob", [], |row| row.get(0))
        .unwrap_or(0);
    count as usize
}

fn get_repo_size(path: &Path) -> u64 {
    std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage: {} <source.forge>", args[0]);
        eprintln!("\nExample:");
        eprintln!("  cargo run --example quic_sync_benchmark --features sync-quic --release -- /tmp/heroforge-scm.forge");
        std::process::exit(1);
    }

    let source_path = PathBuf::from(&args[1]);
    if !source_path.exists() {
        eprintln!("Error: Source file not found: {}", source_path.display());
        std::process::exit(1);
    }

    println!("╔════════════════════════════════════════════════════════════╗");
    println!("║          QUIC Sync Benchmark - heroforge                   ║");
    println!("╚════════════════════════════════════════════════════════════╝");

    // Create temp directory for destination
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let dest_path = temp_dir.path().join("dest.forge");

    // Open source repo
    let source_repo = Repository::open(&source_path).expect("Failed to open source repo");
    let source_artifacts = count_artifacts(&source_repo);
    let source_size = get_repo_size(&source_path);

    println!("\nSource repository:");
    println!("  Path: {}", source_path.display());
    println!("  Size: {}", format_bytes(source_size as usize));
    println!("  Artifacts: {}", source_artifacts);

    // Create empty destination repo with same project code
    let dest_repo = Repository::init(&dest_path).expect("Failed to create dest repo");
    let project_code = source_repo
        .project_code()
        .expect("Failed to get project code");
    dest_repo
        .database()
        .connection()
        .execute(
            "UPDATE config SET value = ?1 WHERE name = 'project-code'",
            [&project_code],
        )
        .expect("Failed to set project code");

    let dest_artifacts_before = count_artifacts(&dest_repo);
    println!("\nDestination repository:");
    println!("  Path: {}", dest_path.display());
    println!("  Artifacts before: {}", dest_artifacts_before);

    println!("\nSync options:");
    println!("  Protocol: QUIC (with built-in TLS 1.3)");
    println!("  Compression: LZ4");

    // Create runtime
    let rt = Runtime::new().expect("Failed to create runtime");

    // Use a channel to communicate the server address
    let (addr_tx, addr_rx) = std::sync::mpsc::channel();

    // Clone paths for threads
    let source_path_clone = source_path.clone();
    let dest_path_clone = dest_path.clone();

    // Start server
    println!("\nStarting server...");

    // Run server in background thread with its own runtime
    let server_handle = std::thread::spawn(move || {
        let rt = Runtime::new().expect("runtime");
        rt.block_on(async {
            let server = QuicServer::bind("127.0.0.1:0").expect("Failed to bind server");
            let addr = server.local_addr().expect("Failed to get addr").to_string();
            println!("  Server listening on {}", addr);
            addr_tx.send(addr).expect("Failed to send addr");

            let repo = Repository::open(&source_path_clone).expect("open source");
            println!("  Server ready, handling sync...");
            let stats = server
                .handle_sync(&repo, &source_path_clone)
                .await
                .expect("sync failed");
            println!("  Server: sync complete!");
            stats
        })
    });

    // Get the server address
    let addr = addr_rx.recv().expect("Failed to receive server address");

    // Run client
    println!("\nSyncing {} artifacts...", source_artifacts);
    let start = Instant::now();

    let client_stats = rt.block_on(async {
        // Small delay to let server start accepting
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;

        let repo = Repository::open_rw(&dest_path_clone).expect("open dest");
        QuicClient::sync(&repo, &dest_path_clone, &addr)
            .await
            .expect("sync failed")
    });

    let elapsed = start.elapsed();
    let elapsed_secs = elapsed.as_secs_f64();

    // Wait for server
    let server_stats = server_handle.join().expect("Server thread panicked");

    // Get final stats
    let dest_repo = Repository::open(&dest_path).expect("Failed to reopen dest");
    let dest_artifacts_after = count_artifacts(&dest_repo);
    let dest_size = get_repo_size(&dest_path);

    // Calculate metrics
    let artifacts_synced = dest_artifacts_after - dest_artifacts_before;
    let bytes_transferred = client_stats.bytes_received + client_stats.bytes_sent;
    let throughput = if elapsed_secs > 0.0 {
        bytes_transferred as f64 / elapsed_secs
    } else {
        0.0
    };

    println!("\n{}", "=".repeat(60));
    println!("                         RESULTS");
    println!("{}", "=".repeat(60));
    println!("Time: {}", format_duration(elapsed_secs));
    println!("Artifacts synced: {}", artifacts_synced);
    println!("Bytes transferred: {}", format_bytes(bytes_transferred));
    println!("Throughput: {}/sec", format_bytes(throughput as usize));

    if source_size > 0 {
        let ratio = source_size as f64 / bytes_transferred as f64;
        println!("Compression ratio: {:.1}x", ratio);
    }

    println!("\nClient stats:");
    println!("  Sent: {}", format_bytes(client_stats.bytes_sent));
    println!("  Received: {}", format_bytes(client_stats.bytes_received));
    println!("  Artifacts sent: {}", client_stats.artifacts_sent);
    println!("  Artifacts received: {}", client_stats.artifacts_received);

    println!("\nServer stats:");
    println!("  Sent: {}", format_bytes(server_stats.bytes_sent));
    println!("  Received: {}", format_bytes(server_stats.bytes_received));

    println!("\nDestination repository after sync:");
    println!("  Artifacts: {}", dest_artifacts_after);
    println!("  Size: {}", format_bytes(dest_size as usize));

    // Verify
    if dest_artifacts_after == source_artifacts {
        println!(
            "\n✓ Verification passed: all {} artifacts synced correctly",
            source_artifacts
        );
    } else {
        println!(
            "\n✗ Verification failed: expected {} artifacts, got {}",
            source_artifacts, dest_artifacts_after
        );
    }
}