use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::cell::RefCell;
use crate::diagnostics::{DiagnosticCode, DiagnosticLevel};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AllocKind {
Frame,
Pool,
Heap,
Scratch,
}
impl std::fmt::Display for AllocKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Frame => write!(f, "frame"),
Self::Pool => write!(f, "pool"),
Self::Heap => write!(f, "heap"),
Self::Scratch => write!(f, "scratch"),
}
}
}
#[derive(Debug, Clone)]
pub struct TagBehaviorStats {
pub tag: &'static str,
pub kind: AllocKind,
pub total_allocs: u64,
pub survived_frame_count: u64,
pub total_lifetime_frames: u64,
pub promotion_count: u64,
pub same_frame_frees: u64,
pub peak_bytes: usize,
pub current_bytes: usize,
pub last_alloc_frame: u64,
pub first_seen_frame: u64,
}
impl TagBehaviorStats {
pub fn new(tag: &'static str, kind: AllocKind, frame: u64) -> Self {
Self {
tag,
kind,
total_allocs: 0,
survived_frame_count: 0,
total_lifetime_frames: 0,
promotion_count: 0,
same_frame_frees: 0,
peak_bytes: 0,
current_bytes: 0,
last_alloc_frame: frame,
first_seen_frame: frame,
}
}
pub fn avg_lifetime_frames(&self) -> f32 {
if self.survived_frame_count == 0 {
0.0
} else {
self.total_lifetime_frames as f32 / self.survived_frame_count as f32
}
}
pub fn promotion_rate(&self) -> f32 {
let frames_active = self.last_alloc_frame.saturating_sub(self.first_seen_frame) + 1;
if frames_active == 0 {
0.0
} else {
self.promotion_count as f32 / frames_active as f32
}
}
pub fn same_frame_free_rate(&self) -> f32 {
if self.total_allocs == 0 {
0.0
} else {
self.same_frame_frees as f32 / self.total_allocs as f32
}
}
pub fn survival_rate(&self) -> f32 {
if self.total_allocs == 0 {
0.0
} else {
self.survived_frame_count as f32 / self.total_allocs as f32
}
}
}
#[derive(Debug, Clone)]
pub struct BehaviorIssue {
pub code: DiagnosticCode,
pub level: DiagnosticLevel,
pub tag: &'static str,
pub kind: AllocKind,
pub message: String,
pub suggestion: String,
pub observed_value: String,
pub threshold: String,
}
impl std::fmt::Display for BehaviorIssue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{}] {}: {} allocation behaves unexpectedly\n \
tag: {}\n \
observed: {}\n \
threshold: {}\n \
suggestion: {}",
self.code,
self.level,
self.kind,
self.tag,
self.observed_value,
self.threshold,
self.suggestion
)
}
}
#[derive(Debug, Clone)]
pub struct BehaviorThresholds {
pub frame_survival_frames: u64,
pub frame_survival_rate: f32,
pub pool_same_frame_free_rate: f32,
pub promotion_churn_rate: f32,
pub heap_in_hot_path_count: u64,
pub min_samples: u64,
}
impl Default for BehaviorThresholds {
fn default() -> Self {
Self {
frame_survival_frames: 60, frame_survival_rate: 0.5, pool_same_frame_free_rate: 0.8, promotion_churn_rate: 0.5, heap_in_hot_path_count: 100, min_samples: 10, }
}
}
impl BehaviorThresholds {
pub fn strict() -> Self {
Self {
frame_survival_frames: 10,
frame_survival_rate: 0.2,
pool_same_frame_free_rate: 0.5,
promotion_churn_rate: 0.2,
heap_in_hot_path_count: 10,
min_samples: 5,
}
}
pub fn relaxed() -> Self {
Self {
frame_survival_frames: 300, frame_survival_rate: 0.8,
pool_same_frame_free_rate: 0.95,
promotion_churn_rate: 0.8,
heap_in_hot_path_count: 1000,
min_samples: 50,
}
}
}
#[derive(Debug, Clone)]
pub struct BehaviorReport {
pub issues: Vec<BehaviorIssue>,
pub stats: Vec<TagBehaviorStats>,
pub frames_analyzed: u64,
pub filter_enabled: bool,
}
impl BehaviorReport {
pub fn issues_at_level(&self, min_level: DiagnosticLevel) -> impl Iterator<Item = &BehaviorIssue> {
self.issues.iter().filter(move |i| i.level >= min_level)
}
pub fn has_errors(&self) -> bool {
self.issues.iter().any(|i| i.level == DiagnosticLevel::Error)
}
pub fn has_warnings(&self) -> bool {
self.issues.iter().any(|i| i.level >= DiagnosticLevel::Warning)
}
pub fn summary(&self) -> String {
let errors = self.issues.iter().filter(|i| i.level == DiagnosticLevel::Error).count();
let warnings = self.issues.iter().filter(|i| i.level == DiagnosticLevel::Warning).count();
let hints = self.issues.iter().filter(|i| i.level == DiagnosticLevel::Hint).count();
format!(
"Behavior analysis: {} errors, {} warnings, {} hints ({} frames, {} tags)",
errors, warnings, hints, self.frames_analyzed, self.stats.len()
)
}
}
pub struct BehaviorFilter {
enabled: AtomicBool,
current_frame: AtomicU64,
thresholds: BehaviorThresholds,
stats: RefCell<HashMap<(&'static str, AllocKind), TagBehaviorStats>>,
pending_this_frame: RefCell<HashMap<*const u8, (&'static str, AllocKind, usize)>>,
}
impl BehaviorFilter {
pub fn new() -> Self {
Self {
enabled: AtomicBool::new(false),
current_frame: AtomicU64::new(0),
thresholds: BehaviorThresholds::default(),
stats: RefCell::new(HashMap::new()),
pending_this_frame: RefCell::new(HashMap::new()),
}
}
pub fn with_thresholds(thresholds: BehaviorThresholds) -> Self {
Self {
enabled: AtomicBool::new(false),
current_frame: AtomicU64::new(0),
thresholds,
stats: RefCell::new(HashMap::new()),
pending_this_frame: RefCell::new(HashMap::new()),
}
}
pub fn enable(&self) {
self.enabled.store(true, Ordering::SeqCst);
}
pub fn disable(&self) {
self.enabled.store(false, Ordering::SeqCst);
}
pub fn is_enabled(&self) -> bool {
self.enabled.load(Ordering::SeqCst)
}
pub fn set_thresholds(&mut self, thresholds: BehaviorThresholds) {
self.thresholds = thresholds;
}
pub fn thresholds(&self) -> &BehaviorThresholds {
&self.thresholds
}
pub fn record_alloc(&self, ptr: *const u8, tag: &'static str, kind: AllocKind, size: usize) {
if !self.is_enabled() {
return;
}
let frame = self.current_frame.load(Ordering::SeqCst);
let key = (tag, kind);
let mut stats = self.stats.borrow_mut();
let entry = stats.entry(key).or_insert_with(|| TagBehaviorStats::new(tag, kind, frame));
entry.total_allocs += 1;
entry.current_bytes += size;
entry.peak_bytes = entry.peak_bytes.max(entry.current_bytes);
entry.last_alloc_frame = frame;
self.pending_this_frame.borrow_mut().insert(ptr, (tag, kind, size));
}
pub fn record_free(&self, ptr: *const u8, tag: &'static str, kind: AllocKind, size: usize) {
if !self.is_enabled() {
return;
}
let key = (tag, kind);
let same_frame = self.pending_this_frame.borrow_mut().remove(&ptr).is_some();
let mut stats = self.stats.borrow_mut();
if let Some(entry) = stats.get_mut(&key) {
entry.current_bytes = entry.current_bytes.saturating_sub(size);
if same_frame {
entry.same_frame_frees += 1;
}
}
}
pub fn record_promotion(&self, tag: &'static str, from_kind: AllocKind) {
if !self.is_enabled() {
return;
}
let key = (tag, from_kind);
let mut stats = self.stats.borrow_mut();
if let Some(entry) = stats.get_mut(&key) {
entry.promotion_count += 1;
}
}
pub fn record_survival(&self, tag: &'static str, kind: AllocKind, frames_alive: u64) {
if !self.is_enabled() {
return;
}
let key = (tag, kind);
let mut stats = self.stats.borrow_mut();
if let Some(entry) = stats.get_mut(&key) {
entry.survived_frame_count += 1;
entry.total_lifetime_frames += frames_alive;
}
}
pub fn end_frame(&self) {
if !self.is_enabled() {
return;
}
self.current_frame.fetch_add(1, Ordering::SeqCst);
self.pending_this_frame.borrow_mut().clear();
}
pub fn current_frame(&self) -> u64 {
self.current_frame.load(Ordering::SeqCst)
}
pub fn analyze(&self) -> BehaviorReport {
let stats_map = self.stats.borrow();
let stats: Vec<_> = stats_map.values().cloned().collect();
let frames = self.current_frame.load(Ordering::SeqCst);
let mut issues = Vec::new();
for stat in &stats {
if stat.total_allocs < self.thresholds.min_samples {
continue;
}
if stat.kind == AllocKind::Frame {
let avg_lifetime = stat.avg_lifetime_frames();
if avg_lifetime > self.thresholds.frame_survival_frames as f32 {
issues.push(BehaviorIssue {
code: FA501,
level: DiagnosticLevel::Warning,
tag: stat.tag,
kind: stat.kind,
message: "Frame allocation behaves like long-lived data".into(),
suggestion: "Consider using pool_alloc() or scratch_pool()".into(),
observed_value: format!("avg lifetime: {:.1} frames", avg_lifetime),
threshold: format!("expected < {} frames", self.thresholds.frame_survival_frames),
});
}
}
if stat.kind == AllocKind::Frame {
let survival_rate = stat.survival_rate();
if survival_rate > self.thresholds.frame_survival_rate {
issues.push(BehaviorIssue {
code: FA502,
level: DiagnosticLevel::Warning,
tag: stat.tag,
kind: stat.kind,
message: "High frame allocation survival rate".into(),
suggestion: "Frame allocations should be ephemeral; use pool or heap".into(),
observed_value: format!("{:.0}% survive beyond frame", survival_rate * 100.0),
threshold: format!("expected < {:.0}%", self.thresholds.frame_survival_rate * 100.0),
});
}
}
if stat.kind == AllocKind::Pool {
let same_frame_rate = stat.same_frame_free_rate();
if same_frame_rate > self.thresholds.pool_same_frame_free_rate {
issues.push(BehaviorIssue {
code: FA510,
level: DiagnosticLevel::Hint,
tag: stat.tag,
kind: stat.kind,
message: "Pool allocation used as scratch memory".into(),
suggestion: "Consider using frame_alloc() for ephemeral data".into(),
observed_value: format!("{:.0}% freed same frame", same_frame_rate * 100.0),
threshold: format!("expected < {:.0}%", self.thresholds.pool_same_frame_free_rate * 100.0),
});
}
}
let promotion_rate = stat.promotion_rate();
if promotion_rate > self.thresholds.promotion_churn_rate {
issues.push(BehaviorIssue {
code: FA520,
level: DiagnosticLevel::Warning,
tag: stat.tag,
kind: stat.kind,
message: "Excessive promotion churn detected".into(),
suggestion: "Consider allocating directly in the target allocator".into(),
observed_value: format!("{:.2} promotions/frame", promotion_rate),
threshold: format!("expected < {:.2}/frame", self.thresholds.promotion_churn_rate),
});
}
if stat.kind == AllocKind::Heap {
let allocs_per_frame = stat.total_allocs as f32 / frames.max(1) as f32;
if allocs_per_frame > self.thresholds.heap_in_hot_path_count as f32 / 60.0 {
issues.push(BehaviorIssue {
code: FA530,
level: DiagnosticLevel::Warning,
tag: stat.tag,
kind: stat.kind,
message: "Frequent heap allocations detected".into(),
suggestion: "Consider using pool_alloc() or frame_alloc() for hot paths".into(),
observed_value: format!("{:.1} heap allocs/frame", allocs_per_frame),
threshold: format!("expected < {:.1}/frame", self.thresholds.heap_in_hot_path_count as f32 / 60.0),
});
}
}
}
issues.sort_by(|a, b| b.level.cmp(&a.level));
BehaviorReport {
issues,
stats,
frames_analyzed: frames,
filter_enabled: self.is_enabled(),
}
}
pub fn reset(&self) {
self.stats.borrow_mut().clear();
self.pending_this_frame.borrow_mut().clear();
self.current_frame.store(0, Ordering::SeqCst);
}
}
impl Default for BehaviorFilter {
fn default() -> Self {
Self::new()
}
}
pub const FA501: DiagnosticCode = DiagnosticCode::new("FA501");
pub const FA502: DiagnosticCode = DiagnosticCode::new("FA502");
pub const FA510: DiagnosticCode = DiagnosticCode::new("FA510");
pub const FA520: DiagnosticCode = DiagnosticCode::new("FA520");
pub const FA530: DiagnosticCode = DiagnosticCode::new("FA530");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_behavior_filter_disabled_by_default() {
let filter = BehaviorFilter::new();
assert!(!filter.is_enabled());
}
#[test]
fn test_behavior_filter_enable_disable() {
let filter = BehaviorFilter::new();
filter.enable();
assert!(filter.is_enabled());
filter.disable();
assert!(!filter.is_enabled());
}
#[test]
fn test_tag_behavior_stats() {
let stats = TagBehaviorStats::new("test", AllocKind::Frame, 0);
assert_eq!(stats.tag, "test");
assert_eq!(stats.kind, AllocKind::Frame);
assert_eq!(stats.total_allocs, 0);
}
#[test]
fn test_behavior_thresholds() {
let default = BehaviorThresholds::default();
let strict = BehaviorThresholds::strict();
let relaxed = BehaviorThresholds::relaxed();
assert!(strict.frame_survival_frames < default.frame_survival_frames);
assert!(relaxed.frame_survival_frames > default.frame_survival_frames);
}
#[test]
fn test_behavior_report_summary() {
let report = BehaviorReport {
issues: vec![],
stats: vec![],
frames_analyzed: 100,
filter_enabled: true,
};
let summary = report.summary();
assert!(summary.contains("100 frames"));
}
#[test]
fn test_record_alloc_when_disabled() {
let filter = BehaviorFilter::new();
filter.record_alloc(std::ptr::null(), "test", AllocKind::Frame, 64);
let report = filter.analyze();
assert!(report.stats.is_empty());
}
#[test]
fn test_record_alloc_when_enabled() {
let filter = BehaviorFilter::new();
filter.enable();
filter.record_alloc(0x1000 as *const u8, "physics", AllocKind::Frame, 64);
filter.record_alloc(0x2000 as *const u8, "physics", AllocKind::Frame, 128);
let report = filter.analyze();
assert_eq!(report.stats.len(), 1);
assert_eq!(report.stats[0].total_allocs, 2);
assert_eq!(report.stats[0].current_bytes, 192);
}
#[test]
fn test_same_frame_free_detection() {
let filter = BehaviorFilter::new();
filter.enable();
let ptr = 0x1000 as *const u8;
filter.record_alloc(ptr, "test", AllocKind::Pool, 64);
filter.record_free(ptr, "test", AllocKind::Pool, 64);
let report = filter.analyze();
assert_eq!(report.stats[0].same_frame_frees, 1);
}
#[test]
fn test_frame_advancement() {
let filter = BehaviorFilter::new();
filter.enable();
assert_eq!(filter.current_frame(), 0);
filter.end_frame();
assert_eq!(filter.current_frame(), 1);
filter.end_frame();
assert_eq!(filter.current_frame(), 2);
}
}