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};
#[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();
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);
}
const CI_MEMORY_LIMIT_MB: usize = 100;
const BYTES_PER_NODE_BUDGET: usize = 1500;
const BYTES_PER_RELATIONSHIP_BUDGET: usize = 500;
#[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
);
assert!(
stats.total_allocated < 1024 * 1024, "MemoryManager initial allocation too high: {} bytes",
stats.total_allocated
);
}
#[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
);
}
#[test]
fn test_relationship_memory_efficiency() {
let mut graph = Graph::new().unwrap();
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
);
}
#[test]
fn test_memory_stays_within_ci_limits() {
let mut graph = Graph::new().unwrap();
let limit_bytes = CI_MEMORY_LIMIT_MB * 1024 * 1024;
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();
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
);
}
#[test]
fn test_batch_memory_efficiency() {
let manager = MemoryManager::with_defaults().unwrap();
let mut batch = manager.start_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);
batch.finish();
let final_stats = manager.batch_stats();
println!("After batch finish:");
println!(" Current usage: {} bytes", final_stats.current_usage);
assert!(
final_stats.batch_count >= 1,
"Batch count should be at least 1"
);
}
#[test]
fn test_memory_cleanup() {
let manager = MemoryManager::with_defaults().unwrap();
let _handle = manager.arena_alloc(vec![1u8; 1000]);
let before = manager.memory_stats();
println!("Before cleanup: arena {} bytes", before.arena_bytes);
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");
}
#[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;
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);
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
);
}