use crate::analysis::relation_inference::{Relation, RelationEdge};
use crate::core::types::TrackKind;
use crate::snapshot::types::ActiveAllocation;
#[derive(Debug, Clone, Copy)]
pub struct ContainerConfig {
pub time_window_ns: u64,
pub size_ratio: usize,
pub lookahead: usize,
}
impl Default for ContainerConfig {
fn default() -> Self {
Self {
time_window_ns: 1_000_000,
size_ratio: 10,
lookahead: 5,
}
}
}
pub fn detect_containers(
allocations: &[ActiveAllocation],
config: Option<ContainerConfig>,
) -> Vec<RelationEdge> {
let config = config.unwrap_or_default();
if allocations.is_empty() {
return Vec::new();
}
let mut sorted_allocs: Vec<(usize, &ActiveAllocation)> =
allocations.iter().enumerate().collect();
sorted_allocs.sort_by_key(|(_, alloc)| alloc.allocated_at);
let mut edges = Vec::new();
for (sorted_idx, (container_orig_idx, container)) in sorted_allocs.iter().enumerate() {
if !matches!(container.kind, TrackKind::Container) {
continue;
}
if container.size == 0 {
continue;
}
let start_idx = sorted_idx + 1;
let end_idx = (start_idx + config.lookahead).min(sorted_allocs.len());
for (candidate_orig_idx, candidate) in sorted_allocs[start_idx..end_idx].iter() {
if !matches!(candidate.kind, TrackKind::HeapOwner { .. }) {
continue;
}
if container.thread_id != candidate.thread_id {
continue;
}
let time_diff = candidate
.allocated_at
.saturating_sub(container.allocated_at);
if time_diff == 0 {
continue;
}
if time_diff > config.time_window_ns {
break;
}
if container.size == 0 {
} else {
let max_size = container.size * config.size_ratio;
if candidate.size > max_size {
continue;
}
}
edges.push(RelationEdge {
from: *container_orig_idx,
to: *candidate_orig_idx,
relation: Relation::Contains,
});
}
}
edges
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::relation_inference::Relation;
fn make_container(_id: usize, size: usize, time: u64, thread: u64) -> ActiveAllocation {
ActiveAllocation {
ptr: None,
size,
kind: TrackKind::Container,
allocated_at: time,
var_name: None,
type_name: None,
thread_id: thread,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
}
}
fn make_heap_owner(ptr: usize, size: usize, time: u64, thread: u64) -> ActiveAllocation {
ActiveAllocation {
ptr: Some(ptr),
size,
kind: TrackKind::HeapOwner { ptr, size },
allocated_at: time,
var_name: None,
type_name: None,
thread_id: thread,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
}
}
#[test]
fn test_detect_containers_empty() {
let allocs = vec![];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_detect_containers_single_container() {
let allocs = vec![make_container(0, 64, 1000, 1)];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_detect_containers_single_heap_owner() {
let allocs = vec![make_heap_owner(0x1000, 64, 1000, 1)];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_detect_containers_basic() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 128, 100500, 1),
];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].from, 0);
assert_eq!(edges[0].to, 1);
assert_eq!(edges[0].relation, Relation::Contains);
}
#[test]
fn test_detect_containers_time_window() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 128, 2_000_000, 1), ];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_detect_containers_custom_time_window() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 128, 2_000_000, 1), ];
let config = ContainerConfig {
time_window_ns: 3_000_000, ..Default::default()
};
let edges = detect_containers(&allocs, Some(config));
assert_eq!(edges.len(), 1);
}
#[test]
fn test_detect_containers_different_threads() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 128, 100500, 2), ];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_detect_containers_size_ratio_filter() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 1280, 100500, 1), ];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_detect_containers_custom_size_ratio() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 1280, 100500, 1), ];
let config = ContainerConfig {
size_ratio: 30, ..Default::default()
};
let edges = detect_containers(&allocs, Some(config));
assert_eq!(edges.len(), 1);
}
#[test]
fn test_detect_containers_multiple_candidates() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 64, 100500, 1),
make_heap_owner(0x3000, 64, 100600, 1),
make_heap_owner(0x4000, 64, 100700, 1),
];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 3);
assert!(edges.iter().all(|e| e.from == 0));
}
#[test]
fn test_detect_containers_lookahead_limit() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_heap_owner(0x2000, 64, 100500, 1),
make_heap_owner(0x3000, 64, 100600, 1),
make_heap_owner(0x4000, 64, 100700, 1),
make_heap_owner(0x5000, 64, 100800, 1),
make_heap_owner(0x6000, 64, 100900, 1), ];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 5);
}
#[test]
fn test_detect_containers_multiple_containers() {
let allocs = vec![
make_container(0, 64, 1000, 1),
make_container(1, 64, 2_000_000, 1), make_heap_owner(0x2000, 64, 100500, 1), make_heap_owner(0x3000, 64, 2_100_000, 1), ];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 2);
let contains_0_2 = edges.iter().any(|e| e.from == 0 && e.to == 2);
let contains_1_3 = edges.iter().any(|e| e.from == 1 && e.to == 3);
assert!(contains_0_2);
assert!(contains_1_3);
}
#[test]
fn test_detect_containers_ignore_value_types() {
let allocs = vec![
make_container(0, 64, 1000, 1),
ActiveAllocation {
ptr: None,
size: 32,
kind: TrackKind::Value,
allocated_at: 100500,
var_name: None,
type_name: None,
thread_id: 1,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
},
];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 0);
}
#[test]
fn test_detect_containers_unsorted_input() {
let allocs = vec![
make_heap_owner(0x2000, 64, 100500, 1), make_container(0, 64, 1000, 1), ];
let edges = detect_containers(&allocs, None);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].from, 1);
assert_eq!(edges[0].to, 0);
}
#[test]
fn test_config_default() {
let config = ContainerConfig::default();
assert_eq!(config.time_window_ns, 1_000_000);
assert_eq!(config.size_ratio, 10);
assert_eq!(config.lookahead, 5);
}
#[test]
fn test_config_custom() {
let config = ContainerConfig {
time_window_ns: 5_000_000,
size_ratio: 20,
lookahead: 10,
};
assert_eq!(config.time_window_ns, 5_000_000);
assert_eq!(config.size_ratio, 20);
assert_eq!(config.lookahead, 10);
}
}