graph_d 1.3.2

A native graph database implementation in Rust with built-in JSON support and SQLite-like simplicity
Documentation
//! Memory limit validation tests
//!
//! Validates: B3 (Memory usage ≤1GB for 1M documents + 4M edges)
//! Satisfies: RT-5 (Memory usage MUST stay within bounds)
//! Satisfies: GAP-6 (Memory limit enforcement not in CI)
//!
//! These tests verify that memory usage stays within acceptable bounds.
//! They use conservative limits for CI and can be run with larger datasets
//! for full validation.

use graph_d::memory::{MemoryConfig, MemoryManager};
use graph_d::Graph;
use serde_json::json;
use std::alloc::{GlobalAlloc, Layout, System};
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Custom allocator that tracks memory usage
/// Note: Not set as global_allocator to avoid interference with test harness
#[allow(dead_code)]
struct TrackingAllocator;

static ALLOCATED: AtomicUsize = AtomicUsize::new(0);
static PEAK_ALLOCATED: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for TrackingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let ptr = System.alloc(layout);
        if !ptr.is_null() {
            let current = ALLOCATED.fetch_add(layout.size(), Ordering::SeqCst) + layout.size();
            // Update peak if current is higher
            let mut peak = PEAK_ALLOCATED.load(Ordering::SeqCst);
            while current > peak {
                match PEAK_ALLOCATED.compare_exchange_weak(
                    peak,
                    current,
                    Ordering::SeqCst,
                    Ordering::Relaxed,
                ) {
                    Ok(_) => break,
                    Err(p) => peak = p,
                }
            }
        }
        ptr
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        System.dealloc(ptr, layout);
        ALLOCATED.fetch_sub(layout.size(), Ordering::SeqCst);
    }
}

fn get_current_memory() -> usize {
    ALLOCATED.load(Ordering::SeqCst)
}

#[allow(dead_code)]
fn get_peak_memory() -> usize {
    PEAK_ALLOCATED.load(Ordering::SeqCst)
}

fn reset_peak_memory() {
    PEAK_ALLOCATED.store(ALLOCATED.load(Ordering::SeqCst), Ordering::SeqCst);
}

/// Memory limit for CI tests (conservative)
/// Full 1GB test should be run in benchmarks
const CI_MEMORY_LIMIT_MB: usize = 100;

/// Per-node memory budget (bytes)
/// Target: ≤1GB for 1M nodes = ~1KB per node
const BYTES_PER_NODE_BUDGET: usize = 1500; // Allow some overhead

/// Per-relationship memory budget (bytes)
const BYTES_PER_RELATIONSHIP_BUDGET: usize = 500;

/// Validates: RT-5 - MemoryManager initialization stays within bounds
#[test]
fn test_memory_manager_initialization() {
    let config = MemoryConfig::default();
    let manager = MemoryManager::new(config).unwrap();

    let stats = manager.memory_stats();
    println!("Initial memory stats:");
    println!("  Total allocated: {} bytes", stats.total_allocated);
    println!("  Node cache: {} bytes", stats.node_cache_bytes);
    println!(
        "  Relationship cache: {} bytes",
        stats.relationship_cache_bytes
    );

    // Memory manager should start with minimal allocation
    assert!(
        stats.total_allocated < 1024 * 1024, // Less than 1MB initial
        "MemoryManager initial allocation too high: {} bytes",
        stats.total_allocated
    );
}

/// Validates: B3 - Node creation memory efficiency
#[test]
fn test_node_memory_efficiency() {
    let mut graph = Graph::new().unwrap();
    let node_count = 1000;

    let before = get_current_memory();
    reset_peak_memory();

    for i in 0..node_count {
        let mut props = HashMap::new();
        props.insert("id".to_string(), json!(i));
        props.insert("name".to_string(), json!(format!("test_node_{}", i)));
        props.insert("value".to_string(), json!(i * 100));
        graph.create_node(props).unwrap();
    }

    let after = get_current_memory();
    let memory_used = after.saturating_sub(before);
    let bytes_per_node = memory_used / node_count;

    println!("Memory for {} nodes:", node_count);
    println!(
        "  Total: {} bytes ({:.2} MB)",
        memory_used,
        memory_used as f64 / 1024.0 / 1024.0
    );
    println!(
        "  Per node: {} bytes (budget: {})",
        bytes_per_node, BYTES_PER_NODE_BUDGET
    );

    assert!(
        bytes_per_node <= BYTES_PER_NODE_BUDGET,
        "Memory per node ({} bytes) exceeds budget ({} bytes). \
         Constraint B3 requires ≤1GB for 1M documents.",
        bytes_per_node,
        BYTES_PER_NODE_BUDGET
    );
}

/// Validates: B3 - Relationship memory efficiency
#[test]
fn test_relationship_memory_efficiency() {
    let mut graph = Graph::new().unwrap();

    // Create nodes first
    let mut node_ids = Vec::new();
    for i in 0..100 {
        let mut props = HashMap::new();
        props.insert("id".to_string(), json!(i));
        let id = graph.create_node(props).unwrap();
        node_ids.push(id);
    }

    let rel_count = 500;
    let before = get_current_memory();
    reset_peak_memory();

    for i in 0..rel_count {
        let from = node_ids[i % node_ids.len()];
        let to = node_ids[(i + 1) % node_ids.len()];
        let mut props = HashMap::new();
        props.insert("weight".to_string(), json!(i));
        graph
            .create_relationship(from, to, "CONNECTS".to_string(), props)
            .unwrap();
    }

    let after = get_current_memory();
    let memory_used = after.saturating_sub(before);
    let bytes_per_rel = memory_used / rel_count;

    println!("Memory for {} relationships:", rel_count);
    println!(
        "  Total: {} bytes ({:.2} MB)",
        memory_used,
        memory_used as f64 / 1024.0 / 1024.0
    );
    println!(
        "  Per relationship: {} bytes (budget: {})",
        bytes_per_rel, BYTES_PER_RELATIONSHIP_BUDGET
    );

    assert!(
        bytes_per_rel <= BYTES_PER_RELATIONSHIP_BUDGET,
        "Memory per relationship ({} bytes) exceeds budget ({} bytes)",
        bytes_per_rel,
        BYTES_PER_RELATIONSHIP_BUDGET
    );
}

/// Validates: RT-5 - Memory stays within CI limits during operations
#[test]
fn test_memory_stays_within_ci_limits() {
    let mut graph = Graph::new().unwrap();
    let limit_bytes = CI_MEMORY_LIMIT_MB * 1024 * 1024;

    // Create a moderate dataset
    for i in 0..5000 {
        let mut props = HashMap::new();
        props.insert("id".to_string(), json!(i));
        props.insert("data".to_string(), json!(format!("node_data_{}", i)));
        graph.create_node(props).unwrap();

        // Check memory periodically
        if i % 1000 == 0 {
            let current = get_current_memory();
            println!(
                "After {} nodes: {:.2} MB",
                i,
                current as f64 / 1024.0 / 1024.0
            );

            if current > limit_bytes {
                println!("MEMORY_LIMIT_EXCEEDED");
                panic!(
                    "Memory usage ({:.2} MB) exceeded CI limit ({} MB)",
                    current as f64 / 1024.0 / 1024.0,
                    CI_MEMORY_LIMIT_MB
                );
            }
        }
    }

    let final_memory = get_current_memory();
    println!(
        "Final memory usage: {:.2} MB",
        final_memory as f64 / 1024.0 / 1024.0
    );

    assert!(
        final_memory <= limit_bytes,
        "Final memory usage ({:.2} MB) exceeded limit ({} MB)",
        final_memory as f64 / 1024.0 / 1024.0,
        CI_MEMORY_LIMIT_MB
    );
}

/// Validates: B3 - Batch operations are memory efficient
#[test]
fn test_batch_memory_efficiency() {
    let manager = MemoryManager::with_defaults().unwrap();

    // Start a batch operation
    let mut batch = manager.start_batch();

    // Allocate some data in the batch
    for i in 0..100 {
        let data = format!("batch_data_{}", i);
        let _ = batch.alloc(data);
    }

    let stats = manager.batch_stats();
    println!("Batch stats:");
    println!("  Current usage: {} bytes", stats.current_usage);
    println!("  Peak usage: {} bytes", stats.peak_usage);
    println!("  Batch count: {}", stats.batch_count);

    // Finish the batch
    batch.finish();

    // After finishing, memory should be reclaimed
    let final_stats = manager.batch_stats();
    println!("After batch finish:");
    println!("  Current usage: {} bytes", final_stats.current_usage);

    // Verify batch system works correctly
    assert!(
        final_stats.batch_count >= 1,
        "Batch count should be at least 1"
    );
}

/// Validates: RT-5 - Memory cleanup works correctly
#[test]
fn test_memory_cleanup() {
    let manager = MemoryManager::with_defaults().unwrap();

    // Use the arena for temporary allocations
    let _handle = manager.arena_alloc(vec![1u8; 1000]);

    let before = manager.memory_stats();
    println!("Before cleanup: arena {} bytes", before.arena_bytes);

    // Reset arena
    manager.reset_arena();

    let after = manager.memory_stats();
    println!("After cleanup: arena {} bytes", after.arena_bytes);

    assert_eq!(after.arena_bytes, 0, "Arena should be empty after reset");
}

/// Validates: B3 - Extrapolate 1M document memory usage
#[test]
fn test_extrapolate_1m_memory() {
    let mut graph = Graph::new().unwrap();
    let sample_size = 1000;

    let before = get_current_memory();

    for i in 0..sample_size {
        let mut props = HashMap::new();
        props.insert("id".to_string(), json!(i));
        props.insert("name".to_string(), json!(format!("node_{}", i)));
        props.insert("email".to_string(), json!(format!("user{}@example.com", i)));
        props.insert("age".to_string(), json!(i % 100));
        props.insert("active".to_string(), json!(i % 2 == 0));
        graph.create_node(props).unwrap();
    }

    let after = get_current_memory();
    let sample_memory = after.saturating_sub(before);
    let bytes_per_node = sample_memory / sample_size;

    // Extrapolate to 1M nodes
    let extrapolated_1m = bytes_per_node * 1_000_000;
    let extrapolated_mb = extrapolated_1m as f64 / 1024.0 / 1024.0;

    println!("Memory extrapolation:");
    println!("  Sample: {} nodes = {} bytes", sample_size, sample_memory);
    println!("  Per node: {} bytes", bytes_per_node);
    println!("  Extrapolated 1M nodes: {:.2} MB", extrapolated_mb);

    // B3 constraint: ≤1GB for 1M documents
    assert!(
        extrapolated_mb <= 1024.0,
        "Extrapolated memory for 1M nodes ({:.2} MB) exceeds 1GB limit. \
         Constraint B3 requires ≤1GB for 1M documents + 4M edges.",
        extrapolated_mb
    );

    println!(
        "Extrapolated 1M nodes would use {:.2} MB (within 1GB limit)",
        extrapolated_mb
    );
}