use crate::core::types::TrackKind;
use crate::event_store::MemoryEvent;
use crate::snapshot::bindings::BindingLedger;
use crate::snapshot::types::{ActiveAllocation, MemorySnapshot, SnapshotBinding};
pub fn build_snapshot_from_events(events: &[MemoryEvent]) -> MemorySnapshot {
let mut ledger = BindingLedger::new();
for event in events {
ledger.apply(event);
}
project_snapshot(&ledger, MemorySnapshot::new().timestamp)
}
pub(crate) fn project_snapshot(ledger: &BindingLedger, timestamp: u64) -> MemorySnapshot {
let mut snapshot = MemorySnapshot::new();
snapshot.timestamp = timestamp;
let mut bindings = Vec::with_capacity(ledger.live().len());
for binding in ledger.live() {
let active = ActiveAllocation {
ptr: Some(binding.ptr),
size: binding.size,
kind: TrackKind::HeapOwner {
ptr: binding.ptr,
size: binding.size,
},
allocated_at: binding.allocated_at,
var_name: binding.var_name.clone(),
type_name: binding.type_name.clone(),
thread_id: binding.thread_id,
call_stack_hash: binding.call_stack_hash,
module_path: binding.module_path.clone(),
stack_ptr: binding.stack_ptr,
};
snapshot
.active_allocations
.insert(binding.ptr, active.clone());
bindings.push(SnapshotBinding {
generation_id: binding.generation,
binding_serial: binding.binding_serial,
allocation: active,
});
}
snapshot.active_bindings = Some(bindings);
let stats = ledger.stats();
snapshot.stats.total_allocations =
usize::try_from(stats.allocation_count).unwrap_or(usize::MAX);
snapshot.stats.total_deallocations =
usize::try_from(stats.deallocation_count).unwrap_or(usize::MAX);
snapshot.stats.total_reallocations =
usize::try_from(stats.reallocation_count).unwrap_or(usize::MAX);
snapshot.stats.unmatched_deallocations =
usize::try_from(stats.unmatched_deallocation_count).unwrap_or(usize::MAX);
snapshot.stats.degraded_releases =
usize::try_from(stats.degraded_releases).unwrap_or(usize::MAX);
snapshot.stats.total_allocated = usize::try_from(stats.allocated_bytes).unwrap_or(usize::MAX);
snapshot.stats.total_deallocated =
usize::try_from(stats.deallocated_bytes).unwrap_or(usize::MAX);
snapshot.stats.current_memory = usize::try_from(stats.current_bytes).unwrap_or(usize::MAX);
snapshot.stats.peak_memory = usize::try_from(stats.peak_bytes).unwrap_or(usize::MAX);
snapshot.stats.active_allocations = snapshot.active_count();
snapshot.thread_stats = ledger.thread_stats().clone();
snapshot
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_snapshot_empty() {
let events: Vec<MemoryEvent> = vec![];
let snapshot = build_snapshot_from_events(&events);
assert_eq!(snapshot.active_count(), 0, "no binding may be active");
assert_eq!(snapshot.current_memory(), 0, "no bytes may be live");
assert_eq!(snapshot.peak_memory(), 0, "no peak may be recorded");
}
#[test]
fn test_build_snapshot_with_allocations() {
let events = vec![
MemoryEvent::allocate(0x1000, 1024, 1),
MemoryEvent::allocate(0x2000, 2048, 1),
];
let snapshot = build_snapshot_from_events(&events);
assert_eq!(snapshot.active_count(), 2, "both bindings must be active");
assert_eq!(snapshot.current_memory(), 3072, "current bytes must sum up");
assert_eq!(
snapshot.stats.total_allocated, 3072,
"total allocated bytes must be recorded"
);
assert_eq!(
snapshot.stats.peak_memory, 3072,
"peak must be the high-water mark of the replay"
);
}
#[test]
fn test_build_snapshot_with_deallocations() {
let events = vec![
MemoryEvent::allocate(0x1000, 1024, 1),
MemoryEvent::deallocate(0x1000, 1024, 1),
];
let snapshot = build_snapshot_from_events(&events);
assert_eq!(snapshot.active_count(), 0, "the binding must be released");
assert_eq!(snapshot.current_memory(), 0, "current bytes must be zero");
assert_eq!(
snapshot.stats.total_deallocated, 1024,
"released bytes must still be reported"
);
assert_eq!(
snapshot.stats.total_deallocations, 1,
"exactly one release was matched"
);
}
#[test]
fn test_build_snapshot_reuse_keeps_newer_binding() {
let events = vec![
MemoryEvent::allocate(0x3000, 64, 1),
MemoryEvent::allocate(0x3000, 256, 1),
MemoryEvent::deallocate(0x3000, 64, 1),
];
let snapshot = build_snapshot_from_events(&events);
assert_eq!(
snapshot.active_count(),
1,
"one release must close exactly one binding"
);
let active = snapshot
.active_allocations
.get(&0x3000)
.expect("the reused address must stay active");
assert_eq!(active.size, 256, "the surviving binding keeps its size");
assert_eq!(
snapshot.current_memory(),
256,
"current bytes must follow the surviving binding"
);
assert_eq!(
snapshot.stats.peak_memory, 320,
"peak must include the overlapping window"
);
}
#[test]
fn test_build_snapshot_reallocation_updates_size() {
let events = vec![
MemoryEvent::allocate(0x4000, 100, 1),
MemoryEvent::reallocate(0x4000, 100, 300, 1),
];
let snapshot = build_snapshot_from_events(&events);
assert_eq!(snapshot.active_count(), 1, "reallocation keeps one binding");
let active = snapshot
.active_allocations
.get(&0x4000)
.expect("the reallocated binding must stay active");
assert_eq!(active.size, 300, "the size must be refreshed");
assert_eq!(
snapshot.stats.total_reallocations, 1,
"the reallocation must be counted separately"
);
assert_eq!(
snapshot.stats.current_memory, 300,
"current bytes must follow the new size"
);
assert_eq!(
snapshot.stats.peak_memory, 300,
"the peak must not contain an intermediate add-then-subtract value"
);
}
}