use crate::backend::native::adjacency::{LinearDetector, SequentialReadBuffer};
use crate::backend::native::graph_ops::{TraversalCache, TraversalCacheStats};
use crate::backend::native::types::NativeNodeId;
use ahash::AHashMap;
use serde_json::json;
pub struct TraversalContext {
pub detector: LinearDetector,
pub buffer: SequentialReadBuffer,
pub cache: TraversalCache,
pub stats: TraversalCacheStats,
pub buffer_hits: u64,
pub buffer_misses: u64,
pub cluster_buffer: Option<Vec<u8>>,
pub cluster_buffer_offsets: Vec<(u64, u32)>,
pub node_cluster_index: AHashMap<NativeNodeId, usize>,
pub time_total_ms: f64,
pub nodes_visited: u64,
pub overshoot_count: u64,
pub undershoot_count: u64,
pub cluster_buffer_reallocs: u64,
pub dedupe_time_ms: f64,
pub sort_time_ms: f64,
}
impl TraversalContext {
pub fn new() -> Self {
Self {
detector: LinearDetector::new(),
buffer: SequentialReadBuffer::new(),
cache: TraversalCache::new(),
stats: TraversalCacheStats::new(),
buffer_hits: 0,
buffer_misses: 0,
cluster_buffer: None,
cluster_buffer_offsets: Vec::new(),
node_cluster_index: AHashMap::new(),
time_total_ms: 0.0,
nodes_visited: 0,
overshoot_count: 0,
undershoot_count: 0,
cluster_buffer_reallocs: 0,
dedupe_time_ms: 0.0,
sort_time_ms: 0.0,
}
}
pub fn record_buffer_hit(&mut self) {
self.buffer_hits += 1;
}
pub fn record_buffer_miss(&mut self) {
self.buffer_misses += 1;
}
pub fn combined_hit_rate(&self) -> f64 {
let total_hits = self.buffer_hits + self.stats.hits;
let total_lookups = total_hits + self.buffer_misses + self.stats.misses;
if total_lookups == 0 {
0.0
} else {
total_hits as f64 / total_lookups as f64
}
}
pub fn clear_cluster_buffer(&mut self) {
self.cluster_buffer = None;
self.cluster_buffer_offsets.clear();
self.node_cluster_index.clear();
}
pub fn record_node_visit(&mut self) {
self.nodes_visited += 1;
}
pub fn record_overshoot(&mut self) {
self.overshoot_count += 1;
}
pub fn record_undershoot(&mut self) {
self.undershoot_count += 1;
}
pub fn record_buffer_realloc(&mut self) {
self.cluster_buffer_reallocs += 1;
}
pub fn get_cluster_info(
&self,
graph_file: &mut crate::backend::native::graph_file::GraphFile,
node_id: NativeNodeId,
) -> Option<(u64, u32)> {
match graph_file.read_node_at(node_id) {
Ok(node_record) => Some((
node_record.outgoing_cluster_offset,
node_record.outgoing_cluster_size,
)),
Err(_) => None, }
}
fn calculate_fragmentation(&self) -> f64 {
let gap_bytes = self.calculate_gap_bytes();
if gap_bytes == 0 {
return 0.0;
}
let offsets = self.detector.cluster_offsets();
if offsets.is_empty() {
return 0.0;
}
let first_offset = offsets[0].0;
let last_offset = offsets.last().unwrap().0;
let last_size = offsets.last().unwrap().1;
let total_span = (last_offset + last_size as u64) - first_offset;
if total_span == 0 {
0.0
} else {
gap_bytes as f64 / total_span as f64
}
}
fn calculate_gap_bytes(&self) -> u64 {
let offsets = self.detector.cluster_offsets();
if offsets.len() < 2 {
return 0;
}
let mut gap_bytes = 0u64;
for i in 0..offsets.len() - 1 {
let (current_offset, current_size) = offsets[i];
let (next_offset, _) = offsets[i + 1];
let expected_next = current_offset.saturating_add(current_size as u64);
if next_offset > expected_next {
gap_bytes += next_offset - expected_next;
}
}
gap_bytes
}
pub fn export_telemetry(&self) -> String {
let telemetry = json!({
"time_total_ms": self.time_total_ms,
"nodes_visited": self.nodes_visited,
"cluster_hits": self.buffer_hits,
"cluster_misses": self.buffer_misses,
"overshoot_count": self.overshoot_count,
"undershoot_count": self.undershoot_count,
"cluster_buffer_reallocs": self.cluster_buffer_reallocs,
"l2_cache_hits": self.stats.hits,
"l2_cache_misses": self.stats.misses,
"dedupe_ms": self.dedupe_time_ms,
"sort_ms": self.sort_time_ms,
"linear_detection_ms": self.detector.time_linear_detection_ms(),
"contiguity_validation_ms": self.detector.time_contiguity_validation_ms(),
"chains_detected": self.detector.chain_count(),
"average_chain_length": self.detector.average_chain_length(),
"fragmentation_score": self.calculate_fragmentation(),
"gap_bytes": self.calculate_gap_bytes(),
"cluster_offsets_count": self.detector.cluster_offsets().len(),
"combined_hit_rate": self.combined_hit_rate(),
});
telemetry.to_string()
}
}
impl Default for TraversalContext {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traversal_context_new() {
let ctx = TraversalContext::new();
assert!(!ctx.detector.is_linear_confirmed());
assert_eq!(ctx.buffer.len(), 0);
assert!(ctx.cache.is_empty());
assert_eq!(ctx.buffer_hits, 0);
assert_eq!(ctx.buffer_misses, 0);
assert_eq!(ctx.stats.hits, 0);
assert_eq!(ctx.stats.misses, 0);
assert!(ctx.cluster_buffer.is_none());
assert!(ctx.cluster_buffer_offsets.is_empty());
assert!(ctx.node_cluster_index.is_empty());
assert_eq!(ctx.time_total_ms, 0.0);
assert_eq!(ctx.nodes_visited, 0);
assert_eq!(ctx.overshoot_count, 0);
assert_eq!(ctx.undershoot_count, 0);
assert_eq!(ctx.cluster_buffer_reallocs, 0);
assert_eq!(ctx.dedupe_time_ms, 0.0);
assert_eq!(ctx.sort_time_ms, 0.0);
}
#[test]
fn test_traversal_context_default() {
let ctx = TraversalContext::default();
assert!(!ctx.detector.is_linear_confirmed());
assert_eq!(ctx.buffer.len(), 0);
assert_eq!(ctx.buffer_hits, 0);
assert_eq!(ctx.buffer_misses, 0);
assert!(ctx.cluster_buffer.is_none());
assert!(ctx.cluster_buffer_offsets.is_empty());
assert!(ctx.node_cluster_index.is_empty());
}
#[test]
fn test_record_buffer_hit() {
let mut ctx = TraversalContext::new();
assert_eq!(ctx.buffer_hits, 0);
ctx.record_buffer_hit();
assert_eq!(ctx.buffer_hits, 1);
ctx.record_buffer_hit();
ctx.record_buffer_hit();
assert_eq!(ctx.buffer_hits, 3);
}
#[test]
fn test_record_buffer_miss() {
let mut ctx = TraversalContext::new();
assert_eq!(ctx.buffer_misses, 0);
ctx.record_buffer_miss();
assert_eq!(ctx.buffer_misses, 1);
ctx.record_buffer_miss();
ctx.record_buffer_miss();
assert_eq!(ctx.buffer_misses, 3);
}
#[test]
fn test_combined_hit_rate_empty() {
let ctx = TraversalContext::new();
assert_eq!(ctx.combined_hit_rate(), 0.0);
}
#[test]
fn test_combined_hit_rate_only_buffer() {
let mut ctx = TraversalContext::new();
ctx.buffer_hits = 10;
ctx.buffer_misses = 5;
let rate = ctx.combined_hit_rate();
assert!((rate - 2.0 / 3.0).abs() < f64::EPSILON);
}
#[test]
fn test_combined_hit_rate_only_cache() {
let mut ctx = TraversalContext::new();
ctx.stats.hits = 8;
ctx.stats.misses = 2;
assert_eq!(ctx.combined_hit_rate(), 0.8);
}
#[test]
fn test_combined_hit_rate_both() {
let mut ctx = TraversalContext::new();
ctx.buffer_hits = 5;
ctx.stats.hits = 3;
ctx.buffer_misses = 2;
ctx.stats.misses = 1;
let rate = ctx.combined_hit_rate();
assert!((rate - 8.0 / 11.0).abs() < f64::EPSILON);
}
#[test]
fn test_combined_hit_rate_perfect() {
let mut ctx = TraversalContext::new();
ctx.buffer_hits = 10;
ctx.stats.hits = 5;
ctx.buffer_misses = 0;
ctx.stats.misses = 0;
assert_eq!(ctx.combined_hit_rate(), 1.0);
}
#[test]
fn test_combined_hit_rate_zero() {
let mut ctx = TraversalContext::new();
ctx.buffer_hits = 0;
ctx.stats.hits = 0;
ctx.buffer_misses = 10;
ctx.stats.misses = 5;
assert_eq!(ctx.combined_hit_rate(), 0.0);
}
#[test]
fn test_traversal_context_components_accessible() {
let mut ctx = TraversalContext::new();
let _ = &ctx.detector;
let _ = &ctx.buffer;
let _ = &mut ctx.cache;
let _ = &ctx.stats;
let _ = &ctx.buffer_hits;
let _ = &ctx.buffer_misses;
ctx.buffer_hits = 100;
ctx.buffer_misses = 50;
assert_eq!(ctx.buffer_hits, 100);
assert_eq!(ctx.buffer_misses, 50);
}
#[test]
fn test_traversal_context_new_has_empty_cluster_buffer() {
let ctx = TraversalContext::new();
assert!(ctx.cluster_buffer.is_none());
assert!(ctx.cluster_buffer_offsets.is_empty());
}
#[test]
fn test_clear_cluster_buffer() {
let mut ctx = TraversalContext::new();
ctx.cluster_buffer = Some(vec![1, 2, 3, 4]);
ctx.cluster_buffer_offsets = vec![(100, 4), (104, 4)];
ctx.node_cluster_index.insert(1, 0);
ctx.node_cluster_index.insert(2, 1);
ctx.clear_cluster_buffer();
assert!(ctx.cluster_buffer.is_none());
assert!(ctx.cluster_buffer_offsets.is_empty());
assert!(ctx.node_cluster_index.is_empty());
}
#[test]
fn test_clear_cluster_buffer_idempotent() {
let mut ctx = TraversalContext::new();
ctx.clear_cluster_buffer();
assert!(ctx.cluster_buffer.is_none());
assert!(ctx.cluster_buffer_offsets.is_empty());
assert!(ctx.node_cluster_index.is_empty());
ctx.cluster_buffer = Some(vec![1, 2, 3]);
ctx.cluster_buffer_offsets = vec![(100, 3)];
ctx.node_cluster_index.insert(1, 0);
ctx.clear_cluster_buffer();
ctx.clear_cluster_buffer();
assert!(ctx.cluster_buffer.is_none());
assert!(ctx.cluster_buffer_offsets.is_empty());
assert!(ctx.node_cluster_index.is_empty());
}
#[test]
fn test_node_cluster_index_field() {
let mut ctx = TraversalContext::new();
assert!(ctx.node_cluster_index.is_empty());
ctx.node_cluster_index.insert(1, 0);
assert_eq!(ctx.node_cluster_index.get(&1), Some(&0));
assert_eq!(ctx.node_cluster_index.len(), 1);
ctx.node_cluster_index.insert(2, 1);
assert_eq!(ctx.node_cluster_index.get(&2), Some(&1));
assert_eq!(ctx.node_cluster_index.len(), 2);
ctx.clear_cluster_buffer();
assert!(ctx.node_cluster_index.is_empty());
}
#[test]
fn test_clear_cluster_buffer_clears_mapping() {
let mut ctx = TraversalContext::new();
ctx.node_cluster_index.insert(1, 0);
ctx.node_cluster_index.insert(2, 1);
ctx.node_cluster_index.insert(3, 2);
ctx.node_cluster_index.insert(4, 3);
assert_eq!(ctx.node_cluster_index.len(), 4);
ctx.clear_cluster_buffer();
assert!(ctx.node_cluster_index.is_empty());
assert_eq!(ctx.node_cluster_index.len(), 0);
}
#[test]
fn test_get_cluster_info_helper_exists() {
let ctx = TraversalContext::new();
let _method_exists = TraversalContext::get_cluster_info;
let _ = &ctx; let _ = _method_exists; }
}