use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Mutex;
use std::thread::ThreadId;
use std::time::Instant;
#[derive(Debug, Clone)]
pub enum FrameEvent {
FrameBegin {
thread_id: ThreadId,
frame_number: u64,
timestamp: Instant,
},
Alloc {
thread_id: ThreadId,
size: usize,
tag: Option<&'static str>,
frame_number: u64,
},
Free {
thread_id: ThreadId,
size: usize,
was_cross_thread: bool,
},
FrameEnd {
thread_id: ThreadId,
frame_number: u64,
duration_us: u64,
total_allocated: usize,
peak_memory: usize,
},
CrossThreadFreeQueued {
from_thread: ThreadId,
to_thread: ThreadId,
size: usize,
},
DeferredProcessed {
thread_id: ThreadId,
count: usize,
total_bytes: usize,
},
TransferInitiated {
from_thread: ThreadId,
size: usize,
},
TransferCompleted {
to_thread: ThreadId,
size: usize,
},
MemoryPressure {
thread_id: ThreadId,
used: usize,
budget: usize,
},
BudgetExceeded {
thread_id: ThreadId,
requested: usize,
available: usize,
budget: usize,
},
}
pub type FrameEventCallback = Box<dyn Fn(&FrameEvent) + Send + Sync>;
pub struct LifecycleManager {
enabled: AtomicBool,
frame_number: AtomicU64,
callbacks: Mutex<Vec<FrameEventCallback>>,
thread_stats: Mutex<std::collections::HashMap<ThreadId, ThreadFrameStats>>,
}
impl LifecycleManager {
pub fn new() -> Self {
Self {
enabled: AtomicBool::new(false),
frame_number: AtomicU64::new(0),
callbacks: Mutex::new(Vec::new()),
thread_stats: Mutex::new(std::collections::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::Relaxed)
}
pub fn on_event<F>(&self, callback: F)
where
F: Fn(&FrameEvent) + Send + Sync + 'static,
{
let mut callbacks = self.callbacks.lock().unwrap();
callbacks.push(Box::new(callback));
}
pub fn clear_callbacks(&self) {
let mut callbacks = self.callbacks.lock().unwrap();
callbacks.clear();
}
pub fn emit(&self, event: FrameEvent) {
if !self.is_enabled() {
return;
}
let callbacks = self.callbacks.lock().unwrap();
for callback in callbacks.iter() {
callback(&event);
}
self.update_stats(&event);
}
fn update_stats(&self, event: &FrameEvent) {
let mut stats = self.thread_stats.lock().unwrap();
match event {
FrameEvent::FrameBegin { thread_id, frame_number, .. } => {
let entry = stats.entry(*thread_id).or_insert_with(ThreadFrameStats::new);
entry.frames_started += 1;
entry.current_frame = *frame_number;
}
FrameEvent::FrameEnd { thread_id, total_allocated, peak_memory, .. } => {
let entry = stats.entry(*thread_id).or_insert_with(ThreadFrameStats::new);
entry.frames_completed += 1;
entry.total_allocated += *total_allocated as u64;
if *peak_memory > entry.peak_memory as usize {
entry.peak_memory = *peak_memory as u64;
}
}
FrameEvent::CrossThreadFreeQueued { from_thread, .. } => {
let entry = stats.entry(*from_thread).or_insert_with(ThreadFrameStats::new);
entry.cross_thread_frees += 1;
}
_ => {}
}
}
pub fn frame_number(&self) -> u64 {
self.frame_number.load(Ordering::SeqCst)
}
pub fn increment_frame(&self) -> u64 {
self.frame_number.fetch_add(1, Ordering::SeqCst)
}
pub fn thread_stats(&self, thread_id: ThreadId) -> Option<ThreadFrameStats> {
let stats = self.thread_stats.lock().unwrap();
stats.get(&thread_id).cloned()
}
pub fn all_thread_stats(&self) -> std::collections::HashMap<ThreadId, ThreadFrameStats> {
self.thread_stats.lock().unwrap().clone()
}
pub fn reset_stats(&self) {
let mut stats = self.thread_stats.lock().unwrap();
stats.clear();
}
pub fn summary(&self) -> LifecycleSummary {
let stats = self.thread_stats.lock().unwrap();
let mut total_frames = 0u64;
let mut total_allocated = 0u64;
let mut peak_memory = 0u64;
let mut cross_thread_frees = 0u64;
for thread_stats in stats.values() {
total_frames += thread_stats.frames_completed;
total_allocated += thread_stats.total_allocated;
if thread_stats.peak_memory > peak_memory {
peak_memory = thread_stats.peak_memory;
}
cross_thread_frees += thread_stats.cross_thread_frees;
}
LifecycleSummary {
thread_count: stats.len(),
total_frames,
total_allocated,
peak_memory,
cross_thread_frees,
}
}
}
impl Default for LifecycleManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct ThreadFrameStats {
pub frames_started: u64,
pub frames_completed: u64,
pub current_frame: u64,
pub total_allocated: u64,
pub peak_memory: u64,
pub cross_thread_frees: u64,
}
impl ThreadFrameStats {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone)]
pub struct LifecycleSummary {
pub thread_count: usize,
pub total_frames: u64,
pub total_allocated: u64,
pub peak_memory: u64,
pub cross_thread_frees: u64,
}
impl std::fmt::Display for LifecycleSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Lifecycle Summary:")?;
writeln!(f, " Threads: {}", self.thread_count)?;
writeln!(f, " Total frames: {}", self.total_frames)?;
writeln!(f, " Total allocated: {} bytes", self.total_allocated)?;
writeln!(f, " Peak memory: {} bytes", self.peak_memory)?;
writeln!(f, " Cross-thread frees: {}", self.cross_thread_frees)?;
Ok(())
}
}
pub struct FrameLifecycleGuard<'a> {
manager: &'a LifecycleManager,
thread_id: ThreadId,
frame_number: u64,
start_time: Instant,
allocated: usize,
peak: usize,
}
impl<'a> FrameLifecycleGuard<'a> {
pub fn new(manager: &'a LifecycleManager) -> Self {
let thread_id = std::thread::current().id();
let frame_number = manager.frame_number();
manager.emit(FrameEvent::FrameBegin {
thread_id,
frame_number,
timestamp: Instant::now(),
});
Self {
manager,
thread_id,
frame_number,
start_time: Instant::now(),
allocated: 0,
peak: 0,
}
}
pub fn record_alloc(&mut self, size: usize) {
self.allocated += size;
if self.allocated > self.peak {
self.peak = self.allocated;
}
}
pub fn record_free(&mut self, size: usize) {
self.allocated = self.allocated.saturating_sub(size);
}
}
impl<'a> Drop for FrameLifecycleGuard<'a> {
fn drop(&mut self) {
let duration = self.start_time.elapsed();
self.manager.emit(FrameEvent::FrameEnd {
thread_id: self.thread_id,
frame_number: self.frame_number,
duration_us: duration.as_micros() as u64,
total_allocated: self.allocated,
peak_memory: self.peak,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
#[test]
fn test_lifecycle_disabled_by_default() {
let manager = LifecycleManager::new();
assert!(!manager.is_enabled());
}
#[test]
fn test_lifecycle_enable_disable() {
let manager = LifecycleManager::new();
manager.enable();
assert!(manager.is_enabled());
manager.disable();
assert!(!manager.is_enabled());
}
#[test]
fn test_lifecycle_callback() {
let manager = LifecycleManager::new();
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
manager.enable();
manager.on_event(move |_| {
counter_clone.fetch_add(1, Ordering::SeqCst);
});
manager.emit(FrameEvent::FrameBegin {
thread_id: std::thread::current().id(),
frame_number: 0,
timestamp: Instant::now(),
});
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
}