graph_d 1.3.2

A native graph database implementation in Rust with built-in JSON support and SQLite-like simplicity
Documentation
//! Runtime memory profiling and monitoring
//!
//! Satisfies: RT-5 (Memory usage MUST stay within bounds)
//! Satisfies: B3 (Memory usage ≤1GB for 1M documents + 4M edges)
//! Satisfies: GAP-6 (Memory limit enforcement not in CI)
//!
//! This module provides runtime memory tracking, profiling, and alerting
//! to ensure the graph database stays within memory constraints.

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

/// Memory profiler for tracking runtime memory usage
/// Satisfies: RT-5 (Memory usage MUST stay within bounds)
#[derive(Debug)]
pub struct MemoryProfiler {
    /// Current tracked memory usage in bytes
    current_usage: AtomicUsize,
    /// Peak memory usage observed
    peak_usage: AtomicUsize,
    /// Configured memory limit in bytes
    limit_bytes: usize,
    /// Number of allocations tracked
    allocation_count: AtomicUsize,
    /// Number of deallocations tracked
    deallocation_count: AtomicUsize,
}

/// Memory profiling snapshot
#[derive(Debug, Clone)]
pub struct MemorySnapshot {
    /// Current memory usage in bytes
    pub current_bytes: usize,
    /// Peak memory usage in bytes
    pub peak_bytes: usize,
    /// Memory limit in bytes
    pub limit_bytes: usize,
    /// Usage as percentage of limit
    pub usage_percent: f64,
    /// Total allocations
    pub allocations: usize,
    /// Total deallocations
    pub deallocations: usize,
    /// Whether we're approaching the limit (>80%)
    pub warning: bool,
    /// Whether we've exceeded the limit
    pub exceeded: bool,
}

/// Memory allocation event for detailed tracking
#[derive(Debug, Clone)]
pub struct AllocationEvent {
    /// Size of allocation in bytes
    pub size: usize,
    /// Category of allocation
    pub category: AllocationCategory,
    /// Timestamp (monotonic counter)
    pub sequence: usize,
}

/// Categories of memory allocations for profiling
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocationCategory {
    /// Node data storage
    Node,
    /// Relationship data storage
    Relationship,
    /// Property/JSON data
    Property,
    /// Index structures
    Index,
    /// Cache storage
    Cache,
    /// Temporary/arena allocation
    Temporary,
    /// Other/uncategorized
    Other,
}

impl Default for MemoryProfiler {
    fn default() -> Self {
        Self::new(1024 * 1024 * 1024) // Default 1GB limit (B3 constraint)
    }
}

impl MemoryProfiler {
    /// Create a new memory profiler with the specified limit
    /// Satisfies: B3 (configurable memory limit)
    pub fn new(limit_bytes: usize) -> Self {
        Self {
            current_usage: AtomicUsize::new(0),
            peak_usage: AtomicUsize::new(0),
            limit_bytes,
            allocation_count: AtomicUsize::new(0),
            deallocation_count: AtomicUsize::new(0),
        }
    }

    /// Create a profiler with the default 1GB limit
    /// Satisfies: B3 (≤1GB for 1M documents)
    pub fn with_default_limit() -> Self {
        Self::default()
    }

    /// Record a memory allocation
    /// Satisfies: RT-5 (track memory usage)
    pub fn record_alloc(&self, size: usize) -> bool {
        let current = self.current_usage.fetch_add(size, Ordering::SeqCst) + size;
        self.allocation_count.fetch_add(1, Ordering::Relaxed);

        // Update peak if necessary
        let mut peak = self.peak_usage.load(Ordering::SeqCst);
        while current > peak {
            match self.peak_usage.compare_exchange_weak(
                peak,
                current,
                Ordering::SeqCst,
                Ordering::Relaxed,
            ) {
                Ok(_) => break,
                Err(p) => peak = p,
            }
        }

        // Return whether we're still within limits
        current <= self.limit_bytes
    }

    /// Record a memory deallocation
    pub fn record_dealloc(&self, size: usize) {
        self.current_usage.fetch_sub(size, Ordering::SeqCst);
        self.deallocation_count.fetch_add(1, Ordering::Relaxed);
    }

    /// Get current memory usage in bytes
    pub fn current_usage(&self) -> usize {
        self.current_usage.load(Ordering::SeqCst)
    }

    /// Get peak memory usage in bytes
    pub fn peak_usage(&self) -> usize {
        self.peak_usage.load(Ordering::SeqCst)
    }

    /// Get the configured memory limit
    pub fn limit(&self) -> usize {
        self.limit_bytes
    }

    /// Check if memory usage is within limits
    /// Satisfies: RT-5 (validate memory bounds)
    pub fn is_within_limits(&self) -> bool {
        self.current_usage() <= self.limit_bytes
    }

    /// Check if we're approaching limits (>80%)
    pub fn is_warning(&self) -> bool {
        let usage = self.current_usage();
        let threshold = (self.limit_bytes as f64 * 0.8) as usize;
        usage > threshold
    }

    /// Get usage as a percentage of the limit
    pub fn usage_percent(&self) -> f64 {
        let usage = self.current_usage() as f64;
        let limit = self.limit_bytes as f64;
        (usage / limit) * 100.0
    }

    /// Get a complete memory snapshot
    /// Satisfies: RT-5 (observable memory state)
    pub fn snapshot(&self) -> MemorySnapshot {
        let current = self.current_usage();
        let peak = self.peak_usage();
        let usage_percent = (current as f64 / self.limit_bytes as f64) * 100.0;

        MemorySnapshot {
            current_bytes: current,
            peak_bytes: peak,
            limit_bytes: self.limit_bytes,
            usage_percent,
            allocations: self.allocation_count.load(Ordering::Relaxed),
            deallocations: self.deallocation_count.load(Ordering::Relaxed),
            warning: usage_percent > 80.0,
            exceeded: current > self.limit_bytes,
        }
    }

    /// Reset the profiler statistics
    pub fn reset(&self) {
        self.current_usage.store(0, Ordering::SeqCst);
        self.peak_usage.store(0, Ordering::SeqCst);
        self.allocation_count.store(0, Ordering::Relaxed);
        self.deallocation_count.store(0, Ordering::Relaxed);
    }

    /// Format a human-readable memory report
    pub fn format_report(&self) -> String {
        let snapshot = self.snapshot();
        format!(
            "Memory Report:\n\
             ├── Current: {:.2} MB ({:.1}%)\n\
             ├── Peak: {:.2} MB\n\
             ├── Limit: {:.2} MB\n\
             ├── Allocations: {}\n\
             ├── Deallocations: {}\n\
             └── Status: {}",
            snapshot.current_bytes as f64 / 1024.0 / 1024.0,
            snapshot.usage_percent,
            snapshot.peak_bytes as f64 / 1024.0 / 1024.0,
            snapshot.limit_bytes as f64 / 1024.0 / 1024.0,
            snapshot.allocations,
            snapshot.deallocations,
            if snapshot.exceeded {
                "⚠️  EXCEEDED"
            } else if snapshot.warning {
                "⚠️  WARNING (>80%)"
            } else {
                "✅ OK"
            }
        )
    }
}

/// Shared memory profiler for global tracking
pub type SharedMemoryProfiler = Arc<MemoryProfiler>;

/// Create a new shared memory profiler
pub fn create_shared_profiler(limit_bytes: usize) -> SharedMemoryProfiler {
    Arc::new(MemoryProfiler::new(limit_bytes))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_profiler_creation() {
        let profiler = MemoryProfiler::default();
        assert_eq!(profiler.limit(), 1024 * 1024 * 1024); // 1GB
        assert_eq!(profiler.current_usage(), 0);
        assert!(profiler.is_within_limits());
    }

    #[test]
    fn test_allocation_tracking() {
        let profiler = MemoryProfiler::new(1024 * 1024); // 1MB limit

        // Allocate 500KB
        assert!(profiler.record_alloc(500 * 1024));
        assert_eq!(profiler.current_usage(), 500 * 1024);
        assert!(profiler.is_within_limits());

        // Allocate another 400KB (total 900KB)
        assert!(profiler.record_alloc(400 * 1024));
        assert_eq!(profiler.current_usage(), 900 * 1024);
        assert!(profiler.is_warning()); // >80%

        // Allocate 200KB more (exceeds limit)
        assert!(!profiler.record_alloc(200 * 1024)); // Returns false
        assert!(!profiler.is_within_limits());
    }

    #[test]
    fn test_deallocation_tracking() {
        let profiler = MemoryProfiler::new(1024 * 1024);

        profiler.record_alloc(500 * 1024);
        assert_eq!(profiler.current_usage(), 500 * 1024);

        profiler.record_dealloc(200 * 1024);
        assert_eq!(profiler.current_usage(), 300 * 1024);
    }

    #[test]
    fn test_peak_tracking() {
        let profiler = MemoryProfiler::new(1024 * 1024);

        profiler.record_alloc(500 * 1024);
        profiler.record_alloc(300 * 1024);
        assert_eq!(profiler.peak_usage(), 800 * 1024);

        profiler.record_dealloc(500 * 1024);
        assert_eq!(profiler.current_usage(), 300 * 1024);
        assert_eq!(profiler.peak_usage(), 800 * 1024); // Peak unchanged
    }

    #[test]
    fn test_snapshot() {
        let profiler = MemoryProfiler::new(1024 * 1024);

        profiler.record_alloc(512 * 1024);

        let snapshot = profiler.snapshot();
        assert_eq!(snapshot.current_bytes, 512 * 1024);
        assert_eq!(snapshot.limit_bytes, 1024 * 1024);
        assert!((snapshot.usage_percent - 50.0).abs() < 0.1);
        assert!(!snapshot.warning);
        assert!(!snapshot.exceeded);
    }

    #[test]
    fn test_format_report() {
        let profiler = MemoryProfiler::new(1024 * 1024);
        profiler.record_alloc(100 * 1024);

        let report = profiler.format_report();
        assert!(report.contains("Memory Report"));
        assert!(report.contains("OK"));
    }
}