graph_d 1.3.2

A native graph database implementation in Rust with built-in JSON support and SQLite-like simplicity
Documentation
//! Custom allocators for optimized memory management.
//!
//! This module provides specialized allocators for different types of graph data,
//! optimized for the specific access patterns of graph operations.
//!
//! Note: This module uses unsafe code for performance-critical allocations.
//! For safe alternatives, see the allocator_safe module.

use crate::error::{GraphError, Result};
use bumpalo::Bump;
use parking_lot::RwLock;
use std::alloc::Layout;
use std::ptr::NonNull;
use std::sync::Arc;

/// A pool-based allocator for fixed-size allocations.
///
/// This allocator maintains pools of pre-allocated blocks for common sizes,
/// reducing allocation overhead and improving cache locality.
pub struct PoolAllocator {
    /// Pool for small allocations (≤ 64 bytes)
    small_pool: Arc<RwLock<Vec<NonNull<u8>>>>,
    /// Pool for medium allocations (65-512 bytes)
    medium_pool: Arc<RwLock<Vec<NonNull<u8>>>>,
    /// Pool for large allocations (513-4096 bytes)
    large_pool: Arc<RwLock<Vec<NonNull<u8>>>>,
    /// Layout information for pool blocks
    small_layout: Layout,
    medium_layout: Layout,
    large_layout: Layout,
}

// SAFETY: PoolAllocator uses Arc<RwLock<_>> for all mutable state,
// which provides thread-safe access. The NonNull<u8> pointers are only
// accessed through the RwLock guards.
#[allow(unsafe_code)]
unsafe impl Send for PoolAllocator {}
#[allow(unsafe_code)]
unsafe impl Sync for PoolAllocator {}

impl PoolAllocator {
    /// Create a new pool allocator with pre-allocated pools.
    #[allow(clippy::arc_with_non_send_sync)]
    pub fn new() -> Result<Self> {
        let small_layout = Layout::from_size_align(64, 8)
            .map_err(|e| GraphError::Memory(format!("Invalid small layout: {}", e)))?;
        let medium_layout = Layout::from_size_align(512, 8)
            .map_err(|e| GraphError::Memory(format!("Invalid medium layout: {}", e)))?;
        let large_layout = Layout::from_size_align(4096, 8)
            .map_err(|e| GraphError::Memory(format!("Invalid large layout: {}", e)))?;

        let allocator = Self {
            small_pool: Arc::new(RwLock::new(Vec::new())),
            medium_pool: Arc::new(RwLock::new(Vec::new())),
            large_pool: Arc::new(RwLock::new(Vec::new())),
            small_layout,
            medium_layout,
            large_layout,
        };

        // Pre-allocate pools
        allocator.fill_pools()?;

        Ok(allocator)
    }

    /// Fill the pools with pre-allocated blocks.
    #[allow(unsafe_code)]
    fn fill_pools(&self) -> Result<()> {
        // SAFETY: We use std::alloc::alloc with valid layouts and check for null pointers.
        unsafe {
            // Allocate small blocks
            let mut small_pool = self.small_pool.write();
            for _ in 0..1000 {
                let ptr = std::alloc::alloc(self.small_layout);
                if !ptr.is_null() {
                    small_pool.push(NonNull::new_unchecked(ptr));
                }
            }

            // Allocate medium blocks
            let mut medium_pool = self.medium_pool.write();
            for _ in 0..500 {
                let ptr = std::alloc::alloc(self.medium_layout);
                if !ptr.is_null() {
                    medium_pool.push(NonNull::new_unchecked(ptr));
                }
            }

            // Allocate large blocks
            let mut large_pool = self.large_pool.write();
            for _ in 0..100 {
                let ptr = std::alloc::alloc(self.large_layout);
                if !ptr.is_null() {
                    large_pool.push(NonNull::new_unchecked(ptr));
                }
            }
        }

        Ok(())
    }

    /// Allocate a block from the appropriate pool.
    pub fn allocate(&self, size: usize) -> Option<NonNull<u8>> {
        if size <= 64 {
            self.small_pool.write().pop()
        } else if size <= 512 {
            self.medium_pool.write().pop()
        } else if size <= 4096 {
            self.large_pool.write().pop()
        } else {
            // Fall back to system allocator for very large allocations
            None
        }
    }

    /// Return a block to the appropriate pool.
    pub fn deallocate(&self, ptr: NonNull<u8>, size: usize) {
        if size <= 64 {
            self.small_pool.write().push(ptr);
        } else if size <= 512 {
            self.medium_pool.write().push(ptr);
        } else if size <= 4096 {
            self.large_pool.write().push(ptr);
        }
        // Large allocations are handled by system allocator
    }

    /// Get pool statistics for monitoring.
    pub fn pool_stats(&self) -> PoolStats {
        PoolStats {
            small_available: self.small_pool.read().len(),
            medium_available: self.medium_pool.read().len(),
            large_available: self.large_pool.read().len(),
        }
    }
}

impl Drop for PoolAllocator {
    #[allow(unsafe_code)]
    fn drop(&mut self) {
        // SAFETY: We deallocate pointers that were allocated with the same layouts.
        unsafe {
            // Deallocate all pooled blocks
            for ptr in self.small_pool.write().drain(..) {
                std::alloc::dealloc(ptr.as_ptr(), self.small_layout);
            }
            for ptr in self.medium_pool.write().drain(..) {
                std::alloc::dealloc(ptr.as_ptr(), self.medium_layout);
            }
            for ptr in self.large_pool.write().drain(..) {
                std::alloc::dealloc(ptr.as_ptr(), self.large_layout);
            }
        }
    }
}

/// Statistics for pool allocator monitoring.
#[derive(Debug, Clone)]
pub struct PoolStats {
    /// Number of available blocks in the small pool (≤64 bytes)
    pub small_available: usize,
    /// Number of available blocks in the medium pool (65-512 bytes)
    pub medium_available: usize,
    /// Number of available blocks in the large pool (513-4096 bytes)
    pub large_available: usize,
}

/// An arena allocator optimized for graph traversal operations.
///
/// This allocator uses bump allocation for temporary objects created during
/// graph traversals, providing O(1) allocation performance.
pub struct GraphArenaAllocator {
    /// Current arena for allocations
    current_arena: Arc<RwLock<Bump>>,
    /// Reserved arena for when current arena fills up
    backup_arena: Arc<RwLock<Bump>>,
    /// Size of each arena in bytes
    arena_size: usize,
}

impl GraphArenaAllocator {
    /// Create a new graph arena allocator.
    #[allow(clippy::arc_with_non_send_sync)]
    pub fn new(arena_size: usize) -> Self {
        Self {
            current_arena: Arc::new(RwLock::new(Bump::with_capacity(arena_size))),
            backup_arena: Arc::new(RwLock::new(Bump::with_capacity(arena_size))),
            arena_size,
        }
    }

    /// Allocate an object in the current arena.
    ///
    /// # Safety
    /// The returned reference is valid until `reset_and_swap` is called.
    /// Callers must ensure they do not hold references across reset calls.
    #[allow(unsafe_code, clippy::mut_from_ref)]
    pub fn alloc<T>(&self, value: T) -> &mut T {
        let arena = self.current_arena.read();
        let ptr = arena.alloc(value) as *mut T;
        // SAFETY: The arena keeps memory valid until reset. The raw pointer
        // conversion extends the lifetime beyond the guard, which is safe
        // because the underlying memory in Bump remains valid.
        unsafe { &mut *ptr }
    }

    /// Allocate a slice in the current arena.
    ///
    /// # Safety
    /// The returned reference is valid until `reset_and_swap` is called.
    /// Callers must ensure they do not hold references across reset calls.
    #[allow(unsafe_code, clippy::mut_from_ref)]
    pub fn alloc_slice<T: Clone>(&self, slice: &[T]) -> &mut [T] {
        let arena = self.current_arena.read();
        let allocated = arena.alloc_slice_clone(slice);
        let ptr = allocated.as_mut_ptr();
        let len = allocated.len();
        // SAFETY: The arena keeps memory valid until reset. The raw pointer
        // conversion extends the lifetime beyond the guard, which is safe
        // because the underlying memory in Bump remains valid.
        unsafe { std::slice::from_raw_parts_mut(ptr, len) }
    }

    /// Reset the current arena and swap it with the backup.
    /// This allows continuing allocations while the old arena is being reset.
    pub fn reset_and_swap(&self) {
        let mut current = self.current_arena.write();
        let mut backup = self.backup_arena.write();

        // Reset the current arena
        current.reset();

        // Swap arenas
        std::mem::swap(&mut *current, &mut *backup);
    }

    /// Get the current memory usage of the arena.
    pub fn memory_usage(&self) -> usize {
        let current = self.current_arena.read();
        current.allocated_bytes()
    }

    /// Check if the current arena is nearly full and needs swapping.
    pub fn needs_reset(&self) -> bool {
        let current = self.current_arena.read();
        current.allocated_bytes() > (self.arena_size * 3) / 4 // 75% full
    }

    /// Create a scoped allocation context that automatically resets when dropped.
    pub fn scoped<F, R>(&self, f: F) -> R
    where
        F: FnOnce() -> R,
    {
        let result = f();
        self.reset_and_swap();
        result
    }
}

/// A specialized allocator for graph topology data.
///
/// This allocator optimizes for the specific access patterns of graph databases:
/// - Frequent traversals (read-heavy)
/// - Batch insertions (write-heavy during imports)
/// - Long-lived data (nodes and relationships persist)
pub struct TopologyAllocator {
    /// Pool allocator for fixed-size topology elements
    pool: PoolAllocator,
    /// Arena for temporary topology calculations
    temp_arena: GraphArenaAllocator,
}

impl TopologyAllocator {
    /// Create a new topology allocator.
    pub fn new() -> Result<Self> {
        Ok(Self {
            pool: PoolAllocator::new()?,
            temp_arena: GraphArenaAllocator::new(4 * 1024 * 1024), // 4MB arena
        })
    }

    /// Allocate space for a node or relationship.
    pub fn alloc_topology_element(&self, size: usize) -> Option<NonNull<u8>> {
        self.pool.allocate(size)
    }

    /// Deallocate a topology element.
    pub fn dealloc_topology_element(&self, ptr: NonNull<u8>, size: usize) {
        self.pool.deallocate(ptr, size);
    }

    /// Allocate temporary space for traversal calculations.
    pub fn alloc_temp<T>(&self, value: T) -> &mut T {
        self.temp_arena.alloc(value)
    }

    /// Reset temporary allocations after traversal operations.
    pub fn reset_temp(&self) {
        self.temp_arena.reset_and_swap();
    }

    /// Get allocator statistics.
    pub fn stats(&self) -> (PoolStats, usize) {
        (self.pool.pool_stats(), self.temp_arena.memory_usage())
    }
}

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

    #[test]
    fn test_pool_allocator() {
        let allocator = PoolAllocator::new().unwrap();

        // Test small allocation
        let ptr = allocator.allocate(32);
        assert!(ptr.is_some());

        if let Some(ptr) = ptr {
            allocator.deallocate(ptr, 32);
        }

        let stats = allocator.pool_stats();
        assert!(stats.small_available > 0);
    }

    #[test]
    fn test_arena_allocator() {
        let allocator = GraphArenaAllocator::new(1024);

        let value = allocator.alloc(42u64);
        assert_eq!(*value, 42);

        let initial_usage = allocator.memory_usage();
        assert!(initial_usage > 0);

        // After reset_and_swap, we swap to the backup arena which should have 0 allocations
        allocator.reset_and_swap();
        let after_reset = allocator.memory_usage();
        // Note: Bumpalo's reset() doesn't reduce allocated_bytes() for the swapped-out arena,
        // but we swapped to a fresh backup arena, so usage should be 0
        assert!(
            after_reset == 0 || after_reset <= initial_usage,
            "Expected after_reset ({}) to be 0 or <= initial_usage ({})",
            after_reset,
            initial_usage
        );
    }

    #[test]
    fn test_scoped_allocation() {
        let allocator = GraphArenaAllocator::new(1024);

        let result = allocator.scoped(|| {
            let _val1 = allocator.alloc(1u32);
            let _val2 = allocator.alloc(2u32);
            42
        });

        assert_eq!(result, 42);
        // Arena should be reset after scope
    }

    #[test]
    fn test_topology_allocator() {
        let allocator = TopologyAllocator::new().unwrap();

        // Test topology element allocation
        let ptr = allocator.alloc_topology_element(64);
        assert!(ptr.is_some());

        // Test temporary allocation
        let temp = allocator.alloc_temp(123u32);
        assert_eq!(*temp, 123);

        let (_, before_reset) = allocator.stats();

        allocator.reset_temp();

        let (pool_stats, temp_usage) = allocator.stats();
        assert!(pool_stats.small_available > 0);
        // After reset, temp_usage should be less than or equal to before reset
        // Note: Bumpalo's behavior with pre-allocated capacity may vary
        assert!(
            temp_usage <= before_reset,
            "Expected temp_usage ({}) <= before_reset ({})",
            temp_usage,
            before_reset
        );
    }
}