use crate::analysis::unsafe_ffi_tracker::StackFrame;
use crate::core::types::{TrackingError, TrackingResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
pub type CallStackId = u32;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NormalizedCallStack {
pub id: CallStackId,
pub frames: Vec<StackFrame>,
pub hash: u64,
pub ref_count: u32,
pub created_at: u64,
}
pub struct CallStackNormalizer {
stack_registry: Arc<Mutex<HashMap<u64, NormalizedCallStack>>>,
id_to_hash: Arc<Mutex<HashMap<CallStackId, u64>>>,
next_id: Arc<Mutex<CallStackId>>,
config: NormalizerConfig,
stats: Arc<Mutex<NormalizerStats>>,
}
#[derive(Debug, Clone)]
pub struct NormalizerConfig {
pub max_registry_size: usize,
pub enable_auto_cleanup: bool,
pub min_ref_count_for_cleanup: u32,
pub enable_statistics: bool,
}
impl Default for NormalizerConfig {
fn default() -> Self {
Self {
max_registry_size: 10000,
enable_auto_cleanup: true,
min_ref_count_for_cleanup: 1,
enable_statistics: true,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NormalizerStats {
pub total_processed: usize,
pub unique_stacks: usize,
pub duplicates_avoided: usize,
pub memory_saved_bytes: usize,
pub cleanup_operations: usize,
pub average_stack_depth: f64,
pub stats_start_time: u64,
}
impl CallStackNormalizer {
pub fn new(config: NormalizerConfig) -> Self {
tracing::info!("📚 Initializing Call Stack Normalizer");
tracing::info!(" • Max registry size: {}", config.max_registry_size);
tracing::info!(" • Auto cleanup: {}", config.enable_auto_cleanup);
tracing::info!(" • Statistics: {}", config.enable_statistics);
Self {
stack_registry: Arc::new(Mutex::new(HashMap::new())),
id_to_hash: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(Mutex::new(1)),
config,
stats: Arc::new(Mutex::new(NormalizerStats {
stats_start_time: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
..Default::default()
})),
}
}
pub fn normalize_call_stack(&self, frames: &[StackFrame]) -> TrackingResult<CallStackId> {
if frames.is_empty() {
return Err(TrackingError::InvalidPointer(
"Empty call stack".to_string(),
));
}
let hash = self.calculate_call_stack_hash(frames);
if let Ok(mut registry) = self.stack_registry.lock() {
if let Some(existing) = registry.get_mut(&hash) {
existing.ref_count += 1;
self.update_stats_duplicate_avoided(frames.len());
tracing::debug!("📚 Reused existing call stack ID: {}", existing.id);
return Ok(existing.id);
}
let id = self.get_next_id()?;
let normalized = NormalizedCallStack {
id,
frames: frames.to_vec(),
hash,
ref_count: 1,
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
};
if registry.len() >= self.config.max_registry_size {
if self.config.enable_auto_cleanup {
self.cleanup_registry_internal(&mut registry)?;
} else {
return Err(TrackingError::ResourceExhausted(
"Call stack registry full".to_string(),
));
}
}
registry.insert(hash, normalized);
if let Ok(mut id_map) = self.id_to_hash.lock() {
id_map.insert(id, hash);
}
self.update_stats_new_stack(frames.len());
tracing::debug!("📚 Created new call stack ID: {} (hash: {:x})", id, hash);
Ok(id)
} else {
Err(TrackingError::LockContention(
"Failed to lock registry".to_string(),
))
}
}
pub fn get_call_stack(&self, id: CallStackId) -> TrackingResult<Vec<StackFrame>> {
let hash = if let Ok(id_map) = self.id_to_hash.lock() {
id_map.get(&id).copied().ok_or_else(|| {
TrackingError::InvalidPointer(format!("Invalid call stack ID: {id}"))
})?
} else {
return Err(TrackingError::LockContention(
"Failed to lock ID mapping".to_string(),
));
};
if let Ok(registry) = self.stack_registry.lock() {
registry
.get(&hash)
.map(|normalized| normalized.frames.clone())
.ok_or_else(|| {
TrackingError::InvalidPointer(format!("Call stack not found for ID: {id}"))
})
} else {
Err(TrackingError::LockContention(
"Failed to lock registry".to_string(),
))
}
}
pub fn increment_ref_count(&self, id: CallStackId) -> TrackingResult<()> {
let hash = if let Ok(id_map) = self.id_to_hash.lock() {
id_map.get(&id).copied().ok_or_else(|| {
TrackingError::InvalidPointer(format!("Invalid call stack ID: {id}"))
})?
} else {
return Err(TrackingError::LockContention(
"Failed to lock ID mapping".to_string(),
));
};
if let Ok(mut registry) = self.stack_registry.lock() {
if let Some(normalized) = registry.get_mut(&hash) {
normalized.ref_count += 1;
tracing::debug!(
"📚 Incremented ref count for ID: {} to {}",
id,
normalized.ref_count
);
Ok(())
} else {
Err(TrackingError::InvalidPointer(format!(
"Call stack not found for ID: {id}",
)))
}
} else {
Err(TrackingError::LockContention(
"Failed to lock registry".to_string(),
))
}
}
pub fn decrement_ref_count(&self, id: CallStackId) -> TrackingResult<()> {
let hash = if let Ok(id_map) = self.id_to_hash.lock() {
id_map.get(&id).copied().ok_or_else(|| {
TrackingError::InvalidPointer(format!("Invalid call stack ID: {id}"))
})?
} else {
return Err(TrackingError::LockContention(
"Failed to lock ID mapping".to_string(),
));
};
if let Ok(mut registry) = self.stack_registry.lock() {
if let Some(normalized) = registry.get_mut(&hash) {
if normalized.ref_count > 0 {
normalized.ref_count -= 1;
tracing::debug!(
"📚 Decremented ref count for ID: {} to {}",
id,
normalized.ref_count
);
}
Ok(())
} else {
Err(TrackingError::InvalidPointer(format!(
"Call stack not found for ID: {id}"
)))
}
} else {
Err(TrackingError::LockContention(
"Failed to lock registry".to_string(),
))
}
}
pub fn cleanup_registry(&self) -> TrackingResult<usize> {
if let Ok(mut registry) = self.stack_registry.lock() {
self.cleanup_registry_internal(&mut registry)
} else {
Err(TrackingError::LockContention(
"Failed to lock registry".to_string(),
))
}
}
pub fn get_stats(&self) -> NormalizerStats {
if let Ok(stats) = self.stats.lock() {
stats.clone()
} else {
tracing::error!("Failed to lock stats");
NormalizerStats::default()
}
}
pub fn get_registry_size(&self) -> usize {
self.stack_registry.lock().map(|r| r.len()).unwrap_or(0)
}
pub fn clear_registry(&self) {
if let Ok(mut registry) = self.stack_registry.lock() {
registry.clear();
}
if let Ok(mut id_map) = self.id_to_hash.lock() {
id_map.clear();
}
if let Ok(mut next_id) = self.next_id.lock() {
*next_id = 1;
}
if let Ok(mut stats) = self.stats.lock() {
*stats = NormalizerStats {
stats_start_time: stats.stats_start_time,
..Default::default()
};
}
tracing::info!("🧹 Cleared call stack registry");
}
fn calculate_call_stack_hash(&self, frames: &[StackFrame]) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
for frame in frames {
frame.function_name.hash(&mut hasher);
frame.file_name.hash(&mut hasher);
frame.line_number.hash(&mut hasher);
frame.is_unsafe.hash(&mut hasher);
}
hasher.finish()
}
fn get_next_id(&self) -> TrackingResult<CallStackId> {
if let Ok(mut next_id) = self.next_id.lock() {
let id = *next_id;
*next_id = next_id.wrapping_add(1);
Ok(id)
} else {
Err(TrackingError::LockContention(
"Failed to lock next ID counter".to_string(),
))
}
}
fn cleanup_registry_internal(
&self,
registry: &mut HashMap<u64, NormalizedCallStack>,
) -> TrackingResult<usize> {
let initial_size = registry.len();
let min_ref_count = self.config.min_ref_count_for_cleanup;
registry.retain(|_, normalized| normalized.ref_count >= min_ref_count);
if let Ok(mut id_map) = self.id_to_hash.lock() {
id_map.retain(|_, hash| registry.contains_key(hash));
}
let removed_count = initial_size - registry.len();
if let Ok(mut stats) = self.stats.lock() {
stats.cleanup_operations += 1;
stats.unique_stacks = registry.len();
}
tracing::info!(
"🧹 Cleaned up {} unused call stacks from registry",
removed_count
);
Ok(removed_count)
}
fn update_stats_new_stack(&self, stack_depth: usize) {
if !self.config.enable_statistics {
return;
}
if let Ok(mut stats) = self.stats.lock() {
stats.total_processed += 1;
stats.unique_stacks += 1;
let total_depth =
stats.average_stack_depth * (stats.unique_stacks - 1) as f64 + stack_depth as f64;
stats.average_stack_depth = total_depth / stats.unique_stacks as f64;
stats.memory_saved_bytes += stack_depth * std::mem::size_of::<StackFrame>();
}
}
fn update_stats_duplicate_avoided(&self, stack_depth: usize) {
if !self.config.enable_statistics {
return;
}
if let Ok(mut stats) = self.stats.lock() {
stats.total_processed += 1;
stats.duplicates_avoided += 1;
stats.memory_saved_bytes += stack_depth * std::mem::size_of::<StackFrame>();
}
}
}
impl Default for CallStackNormalizer {
fn default() -> Self {
Self::new(NormalizerConfig::default())
}
}
static GLOBAL_NORMALIZER: std::sync::OnceLock<Arc<CallStackNormalizer>> =
std::sync::OnceLock::new();
pub fn get_global_call_stack_normalizer() -> Arc<CallStackNormalizer> {
GLOBAL_NORMALIZER
.get_or_init(|| Arc::new(CallStackNormalizer::new(NormalizerConfig::default())))
.clone()
}
pub fn initialize_global_call_stack_normalizer(
config: NormalizerConfig,
) -> Arc<CallStackNormalizer> {
let normalizer = Arc::new(CallStackNormalizer::new(config));
if GLOBAL_NORMALIZER.set(normalizer.clone()).is_err() {
tracing::warn!("Global call stack normalizer already initialized");
}
normalizer
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallStackRef {
pub id: CallStackId,
pub depth: Option<usize>,
pub created_at: u64,
}
impl CallStackRef {
pub fn new(id: CallStackId, depth: Option<usize>) -> Self {
Self {
id,
depth,
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
}
}
pub fn get_frames(&self) -> TrackingResult<Vec<StackFrame>> {
let normalizer = get_global_call_stack_normalizer();
normalizer.get_call_stack(self.id)
}
pub fn get_depth(&self) -> TrackingResult<usize> {
match self.depth {
Some(depth) => Ok(depth),
None => {
let frames = self.get_frames()?;
Ok(frames.len())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_stack_frame(function_name: &str, line: u32) -> StackFrame {
StackFrame {
function_name: function_name.to_string(),
file_name: Some("test.rs".to_string()),
line_number: Some(line),
is_unsafe: false,
}
}
#[test]
fn test_call_stack_normalization() {
let normalizer = CallStackNormalizer::new(NormalizerConfig::default());
let frames1 = vec![
create_test_stack_frame("main", 10),
create_test_stack_frame("foo", 20),
];
let frames2 = vec![
create_test_stack_frame("main", 10),
create_test_stack_frame("foo", 20),
];
let id1 = normalizer
.normalize_call_stack(&frames1)
.expect("Failed to normalize call stack");
assert_eq!(id1, 1);
let _id2 = normalizer
.normalize_call_stack(&frames2)
.expect("Failed to normalize call stack");
assert_eq!(id1, _id2);
let retrieved_frames = normalizer
.get_call_stack(id1)
.expect("Failed to get call stack");
assert_eq!(retrieved_frames.len(), 2);
assert_eq!(retrieved_frames[0].function_name, "main");
assert_eq!(retrieved_frames[1].function_name, "foo");
}
#[test]
fn test_reference_counting() {
let normalizer = CallStackNormalizer::new(NormalizerConfig::default());
let frames = vec![create_test_stack_frame("test", 1)];
let id = normalizer
.normalize_call_stack(&frames)
.expect("Test operation failed");
normalizer
.increment_ref_count(id)
.expect("Failed to increment ref count");
normalizer
.decrement_ref_count(id)
.expect("Failed to decrement ref count");
let retrieved = normalizer
.get_call_stack(id)
.expect("Failed to get call stack");
assert_eq!(retrieved.len(), 1);
}
#[test]
fn test_call_stack_ref() {
let config = NormalizerConfig::default();
let _global_normalizer = initialize_global_call_stack_normalizer(config);
let frames = vec![
create_test_stack_frame("main", 10),
create_test_stack_frame("test", 20),
];
let global_normalizer = get_global_call_stack_normalizer();
let id = global_normalizer
.normalize_call_stack(&frames)
.expect("Failed to normalize call stack");
let stack_ref = CallStackRef::new(id, Some(2));
assert_eq!(stack_ref.get_depth().expect("Failed to get depth"), 2);
let retrieved_frames = stack_ref.get_frames().expect("Failed to get frames");
assert_eq!(retrieved_frames.len(), 2);
assert_eq!(retrieved_frames[0].function_name, "main");
}
#[test]
fn test_registry_cleanup() {
let config = NormalizerConfig {
max_registry_size: 2,
min_ref_count_for_cleanup: 2,
..Default::default()
};
let normalizer = CallStackNormalizer::new(config);
let frames1 = vec![create_test_stack_frame("func1", 1)];
let frames2 = vec![create_test_stack_frame("func2", 2)];
let frames3 = vec![create_test_stack_frame("func3", 3)];
let id1 = normalizer
.normalize_call_stack(&frames1)
.expect("Failed to normalize call stack");
let _id2 = normalizer
.normalize_call_stack(&frames2)
.expect("Failed to normalize call stack");
normalizer
.increment_ref_count(id1)
.expect("Failed to increment ref count");
let _id3 = normalizer
.normalize_call_stack(&frames3)
.expect("Failed to normalize call stack");
assert!(normalizer.get_call_stack(id1).is_ok());
let stats = normalizer.get_stats();
assert!(stats.cleanup_operations > 0);
}
#[test]
fn test_statistics_tracking() {
let normalizer = CallStackNormalizer::new(NormalizerConfig::default());
let frames1 = vec![create_test_stack_frame("func1", 1)];
let frames2 = vec![create_test_stack_frame("func1", 1)]; let frames3 = vec![create_test_stack_frame("func2", 2)];
normalizer
.normalize_call_stack(&frames1)
.expect("Failed to normalize call stack");
normalizer
.normalize_call_stack(&frames2)
.expect("Failed to normalize call stack"); normalizer
.normalize_call_stack(&frames3)
.expect("Failed to normalize call stack");
let stats = normalizer.get_stats();
assert_eq!(stats.total_processed, 3);
assert_eq!(stats.unique_stacks, 2);
assert_eq!(stats.duplicates_avoided, 1);
assert!(stats.memory_saved_bytes > 0);
}
}