use crate::capture::backends::{AsyncAllocation, TaskMemoryProfile, TaskType};
use crate::event_store::{MemoryEvent, MemoryEventType};
use crate::task_registry::{TaskGraph, TaskStatus};
use std::collections::HashMap;
use std::sync::Arc;
struct LiveBinding {
task_id: u64,
size: usize,
generation: u64,
timestamp: u64,
var_name: Option<String>,
type_name: Option<String>,
}
pub struct RebuiltProfiles {
pub profiles: Vec<TaskMemoryProfile>,
pub live_allocations: Vec<AsyncAllocation>,
pub global_peak_memory: u64,
pub total_allocations: u64,
pub total_memory: u64,
pub current_memory: u64,
}
#[derive(Clone)]
struct LiveMeta {
task_name: String,
created_at_ms: u64,
completed_at_ms: Option<u64>,
tokio_task_id: Option<u64>,
thread_id_u64: Option<u64>,
task_type: TaskType,
}
pub(crate) struct ProfileMetadata(HashMap<u64, LiveMeta>);
impl ProfileMetadata {
pub(crate) fn capture(
graph: &TaskGraph,
tracker: Option<&crate::capture::backends::async_tracker::AsyncTracker>,
) -> Self {
let _guard = crate::core::allocator::TrackingGuard::acquire();
let mut metadata = HashMap::new();
let ids: std::collections::HashSet<_> = graph.nodes.iter().map(|node| node.id).collect();
for meta in crate::task_registry::global_registry().get_all_tasks() {
if ids.contains(&meta.id) {
metadata.insert(
meta.id,
LiveMeta {
task_name: meta.name,
created_at_ms: meta.created_at / 1_000_000,
completed_at_ms: meta.completed_at.map(|ns| ns / 1_000_000),
tokio_task_id: meta.tokio_id,
thread_id_u64: None,
task_type: TaskType::default(),
},
);
}
}
if let Some(tracker) = tracker {
for profile in tracker.lifecycle_profiles() {
let completed_at_ms = profile.completed_at_ms.or_else(|| {
metadata
.get(&profile.task_id)
.and_then(|meta| meta.completed_at_ms)
});
metadata.insert(
profile.task_id,
LiveMeta {
task_name: profile.task_name,
created_at_ms: profile.created_at_ms,
completed_at_ms,
tokio_task_id: profile.tokio_task_id,
thread_id_u64: profile.thread_id_u64,
task_type: profile.task_type,
},
);
}
}
Self(metadata)
}
pub(crate) fn reconcile_graph(&self, graph: &mut TaskGraph) {
for node in &mut graph.nodes {
if self
.0
.get(&node.id)
.is_some_and(|meta| meta.completed_at_ms.is_some())
{
node.status = TaskStatus::Completed;
}
}
}
}
pub(crate) fn capture_cut_ns() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos().min(u128::from(u64::MAX)) as u64)
.unwrap_or(0)
}
pub fn rebuild_async_profiles_from_events(
events: &[MemoryEvent],
task_graph: &TaskGraph,
) -> RebuiltProfiles {
let _guard = crate::core::allocator::TrackingGuard::acquire();
let metadata = ProfileMetadata::capture(task_graph, None);
rebuild_at(events, task_graph, &metadata, capture_cut_ns())
}
pub fn rebuild_with_live_tracker(
events: &[MemoryEvent],
task_graph: &TaskGraph,
live_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
) -> RebuiltProfiles {
let _guard = crate::core::allocator::TrackingGuard::acquire();
let metadata = ProfileMetadata::capture(task_graph, Some(live_tracker));
rebuild_at(events, task_graph, &metadata, capture_cut_ns())
}
pub(crate) fn rebuild_at(
events: &[MemoryEvent],
task_graph: &TaskGraph,
metadata: &ProfileMetadata,
captured_at_ns: u64,
) -> RebuiltProfiles {
let _guard = crate::core::allocator::TrackingGuard::acquire();
let live_meta = Some(&metadata.0);
let mut profiles: HashMap<u64, TaskMemoryProfile> = HashMap::new();
let mut live: HashMap<usize, Vec<LiveBinding>> = HashMap::new();
for node in &task_graph.nodes {
let mut profile = TaskMemoryProfile::new(node.id, node.name.clone(), TaskType::default());
profile.created_at_ms = 0;
let mut node_completed = node.status == TaskStatus::Completed;
if let Some(meta) = live_meta.and_then(|m| m.get(&node.id)) {
if meta.created_at_ms > 0 {
profile.created_at_ms = meta.created_at_ms;
}
if !meta.task_name.is_empty() {
profile.task_name = meta.task_name.clone();
}
profile.completed_at_ms = meta.completed_at_ms;
profile.tokio_task_id = meta.tokio_task_id;
profile.thread_id_u64 = meta.thread_id_u64;
profile.task_type = meta.task_type;
if meta.completed_at_ms.is_some() {
node_completed = true;
}
}
if node_completed && profile.completed_at_ms.is_none() {
profile.completed_at_ms = Some(profile.created_at_ms);
}
profiles.insert(node.id, profile);
}
for (&id, meta) in &metadata.0 {
profiles.entry(id).or_insert_with(|| {
let mut profile = TaskMemoryProfile::new(id, meta.task_name.clone(), meta.task_type);
profile.created_at_ms = meta.created_at_ms;
profile.completed_at_ms = meta.completed_at_ms;
profile.tokio_task_id = meta.tokio_task_id;
profile.thread_id_u64 = meta.thread_id_u64;
profile
});
}
let mut global_current: u64 = 0;
let mut global_peak: u64 = 0;
for event in events {
match event.event_type {
MemoryEventType::Allocate => {
let task_id = match event.task_id {
Some(tid) if tid != 0 => tid,
_ => continue,
};
let profile = profiles.entry(task_id).or_insert_with(|| {
let mut p = TaskMemoryProfile::new(task_id, String::new(), TaskType::default());
if let Some(meta) = live_meta.and_then(|m| m.get(&task_id)) {
if meta.created_at_ms > 0 {
p.created_at_ms = meta.created_at_ms;
}
if !meta.task_name.is_empty() {
p.task_name = meta.task_name.clone();
}
p.completed_at_ms = meta.completed_at_ms;
}
p
});
if profile.created_at_ms == 0 || event.timestamp / 1_000_000 < profile.created_at_ms
{
profile.created_at_ms = event.timestamp / 1_000_000;
}
if profile.task_name.is_empty() {
if let Some(ref name) = event.var_name {
profile.task_name = name.clone();
}
}
profile.set_thread_id_u64(Some(event.thread_id));
profile.record_allocation(event.size as u64);
global_current = global_current.saturating_add(event.size as u64);
if global_current > global_peak {
global_peak = global_current;
}
let gen = event.binding_serial.unwrap_or(0);
live.entry(event.ptr).or_default().push(LiveBinding {
task_id,
size: event.size,
generation: gen,
timestamp: event.timestamp,
var_name: event.var_name.clone(),
type_name: event.type_name.clone(),
});
}
MemoryEventType::Deallocate => {
let closed =
close_live_binding(&mut live, event.ptr, event.size, event.binding_serial);
if let Some(binding) = closed {
if let Some(profile) = profiles.get_mut(&binding.task_id) {
profile.record_deallocation(binding.size as u64);
}
global_current = global_current.saturating_sub(binding.size as u64);
}
}
MemoryEventType::Reallocate => {
let old_size = event.old_size.unwrap_or(0);
let new_size = event.size;
let new_ptr = event.new_ptr.unwrap_or(event.ptr);
let closed =
close_live_binding(&mut live, event.ptr, old_size, event.binding_serial);
if let Some(binding) = closed {
if let Some(profile) = profiles.get_mut(&binding.task_id) {
profile.record_reallocation(binding.size as u64, new_size as u64);
}
global_current = global_current
.saturating_sub(binding.size as u64)
.saturating_add(new_size as u64);
if global_current > global_peak {
global_peak = global_current;
}
let serial = event.binding_serial.unwrap_or(binding.generation);
live.entry(new_ptr).or_default().push(LiveBinding {
task_id: binding.task_id,
size: new_size,
generation: serial,
timestamp: event.timestamp,
var_name: binding.var_name,
type_name: binding.type_name,
});
}
}
MemoryEventType::Metadata => {
if let Some(serial) = event.binding_serial {
if let Some(entries) = live.get_mut(&event.ptr) {
if let Some(binding) = entries.iter_mut().find(|b| b.generation == serial) {
if let Some(ref name) = event.var_name {
binding.var_name = Some(name.clone());
}
if let Some(ref tname) = event.type_name {
binding.type_name = Some(tname.clone());
}
}
}
}
if let Some(task_id) = event.task_id {
if task_id != 0 {
if let Some(profile) = profiles.get_mut(&task_id) {
if profile.task_name.is_empty() {
if let Some(ref name) = event.var_name {
profile.task_name = name.clone();
}
}
}
}
}
}
_ => {}
}
}
let mut live_allocations: Vec<AsyncAllocation> = Vec::new();
for (ptr, entries) in &live {
for binding in entries {
live_allocations.push(AsyncAllocation {
ptr: *ptr,
size: binding.size,
timestamp: binding.timestamp,
task_id: binding.task_id,
var_name: binding.var_name.clone(),
type_name: binding.type_name.clone(),
source_location: None,
generation: binding.generation,
});
}
}
let total_allocations: u64 = profiles.values().map(|p| p.total_allocations).sum();
let total_memory: u64 = profiles.values().map(|p| p.total_bytes).sum();
let current_memory: u64 = profiles.values().map(|p| p.current_memory).sum();
let mut profiles_vec: Vec<TaskMemoryProfile> = profiles.into_values().collect();
finalize_metrics_with_cutoff(&mut profiles_vec, captured_at_ns);
profiles_vec.sort_by_key(|p| p.task_id);
live_allocations.sort_by(|a, b| a.ptr.cmp(&b.ptr));
RebuiltProfiles {
profiles: profiles_vec,
live_allocations,
global_peak_memory: global_peak,
total_allocations,
total_memory,
current_memory,
}
}
pub fn finalize_metrics(profiles: &mut [TaskMemoryProfile]) {
for p in profiles {
if let Some(completed) = p.completed_at_ms {
let duration_ms = completed.saturating_sub(p.created_at_ms);
p.duration_ns = duration_ms * 1_000_000;
let duration_secs = duration_ms as f64 / 1000.0;
p.allocation_rate = if duration_secs > 0.0 {
p.total_bytes as f64 / duration_secs
} else {
0.0
};
} else {
p.duration_ns = 0;
p.allocation_rate = 0.0;
}
p.efficiency_score = if p.total_bytes == 0 {
1.0
} else {
let deallocated = p.total_bytes.saturating_sub(p.current_memory);
deallocated as f64 / p.total_bytes as f64
};
p.average_allocation_size = if p.total_allocations > 0 {
p.total_bytes as f64 / p.total_allocations as f64
} else {
0.0
};
}
}
fn finalize_metrics_with_cutoff(profiles: &mut [TaskMemoryProfile], captured_at_ns: u64) {
let now_ms = captured_at_ns / 1_000_000;
for p in profiles {
let end_ms = if let Some(completed) = p.completed_at_ms {
completed
} else {
now_ms
};
let duration_ms = if p.created_at_ms == 0 {
0
} else {
end_ms.saturating_sub(p.created_at_ms)
};
p.duration_ns = duration_ms * 1_000_000;
let duration_secs = duration_ms as f64 / 1000.0;
p.allocation_rate = if duration_secs > 0.0 {
p.total_bytes as f64 / duration_secs
} else {
0.0
};
p.efficiency_score = if p.total_bytes == 0 {
1.0
} else {
let deallocated = p.total_bytes.saturating_sub(p.current_memory);
deallocated as f64 / p.total_bytes as f64
};
p.average_allocation_size = if p.total_allocations > 0 {
p.total_bytes as f64 / p.total_allocations as f64
} else {
0.0
};
}
}
fn close_live_binding(
live: &mut HashMap<usize, Vec<LiveBinding>>,
ptr: usize,
size: usize,
binding_serial: Option<u64>,
) -> Option<LiveBinding> {
let entries = live.get_mut(&ptr)?;
let index = match binding_serial {
Some(serial) => entries.iter().position(|b| b.generation == serial),
None => entries.iter().position(|b| b.size == size),
};
let index = index?;
let closed = entries.remove(index);
if entries.is_empty() {
live.remove(&ptr);
}
Some(closed)
}
#[cfg(test)]
mod tests;