Skip to main content

memscope_rs/analysis/
top_n.rs

1//! Top N report generation
2//!
3//! This module provides analysis to identify the top N allocation sites,
4//! leaked bytes, and temporary churn for quick performance insights.
5
6use crate::capture::types::AllocationInfo;
7use std::collections::HashMap;
8
9/// Top N analyzer
10pub struct TopNAnalyzer {
11    allocations: Vec<AllocationInfo>,
12}
13
14impl TopNAnalyzer {
15    /// Create a new Top N analyzer
16    pub fn new(allocations: Vec<AllocationInfo>) -> Self {
17        Self { allocations }
18    }
19
20    /// Get top N allocation sites by total bytes allocated
21    pub fn top_allocation_sites(&self, n: usize) -> Vec<AllocationSite> {
22        let mut sites = HashMap::new();
23        for alloc in &self.allocations {
24            let key = alloc
25                .stack_trace
26                .as_ref()
27                .and_then(|s| s.first().cloned())
28                .unwrap_or_else(|| "unknown".to_string());
29            let entry = sites.entry(key.clone()).or_insert(AllocationSite {
30                name: key,
31                total_bytes: 0,
32                allocation_count: 0,
33            });
34            entry.total_bytes += alloc.size;
35            entry.allocation_count += 1;
36        }
37
38        let mut result: Vec<_> = sites.into_values().collect();
39        result.sort_by_key(|b| std::cmp::Reverse(b.total_bytes));
40        result.truncate(n);
41        result
42    }
43
44    /// Get top N leaked allocations by bytes
45    pub fn top_leaked_bytes(&self, n: usize) -> Vec<LeakedAllocation> {
46        let mut leaked: Vec<LeakedAllocation> = self
47            .allocations
48            .iter()
49            .filter(|alloc| alloc.timestamp_dealloc.is_none())
50            .map(|alloc| LeakedAllocation {
51                ptr: alloc.ptr,
52                size: alloc.size,
53                type_name: alloc.type_name.clone(),
54                stack_trace: alloc.stack_trace.clone(),
55                timestamp_alloc: alloc.timestamp_alloc,
56            })
57            .collect();
58
59        leaked.sort_by_key(|b| std::cmp::Reverse(b.size));
60        leaked.truncate(n);
61        leaked
62    }
63
64    /// Get top N temporary churn (short-lived allocations) by count
65    pub fn top_temporary_churn(&self, n: usize, threshold_ms: u64) -> Vec<TemporaryChurn> {
66        let mut churn = HashMap::new();
67        for alloc in &self.allocations {
68            if let Some(dealloc_ts) = alloc.timestamp_dealloc {
69                let lifetime_ms = dealloc_ts - alloc.timestamp_alloc;
70                if lifetime_ms < threshold_ms {
71                    let key = alloc
72                        .stack_trace
73                        .as_ref()
74                        .and_then(|s| s.first().cloned())
75                        .unwrap_or_else(|| "unknown".to_string());
76                    let entry = churn.entry(key.clone()).or_insert(TemporaryChurn {
77                        name: key,
78                        allocation_count: 0,
79                        total_bytes: 0,
80                        average_lifetime_ms: 0.0,
81                    });
82                    entry.allocation_count += 1;
83                    entry.total_bytes += alloc.size;
84                }
85            }
86        }
87
88        let mut result: Vec<_> = churn.into_values().collect();
89        result.sort_by_key(|b| std::cmp::Reverse(b.allocation_count));
90        result.truncate(n);
91        result
92    }
93
94    /// Get summary statistics
95    pub fn summary(&self) -> TopNSummary {
96        let total_allocations = self.allocations.len();
97        let total_bytes: usize = self.allocations.iter().map(|a| a.size).sum();
98        let leaked_count = self
99            .allocations
100            .iter()
101            .filter(|a| a.timestamp_dealloc.is_none())
102            .count();
103        let leaked_bytes: usize = self
104            .allocations
105            .iter()
106            .filter(|a| a.timestamp_dealloc.is_none())
107            .map(|a| a.size)
108            .sum();
109
110        TopNSummary {
111            total_allocations,
112            total_bytes,
113            leaked_count,
114            leaked_bytes,
115        }
116    }
117}
118
119/// Allocation site information
120#[derive(Debug, Clone)]
121pub struct AllocationSite {
122    pub name: String,
123    pub total_bytes: usize,
124    pub allocation_count: usize,
125}
126
127/// Leaked allocation information
128#[derive(Debug, Clone)]
129pub struct LeakedAllocation {
130    pub ptr: usize,
131    pub size: usize,
132    pub type_name: Option<String>,
133    pub stack_trace: Option<Vec<String>>,
134    pub timestamp_alloc: u64,
135}
136
137/// Temporary churn information
138#[derive(Debug, Clone)]
139pub struct TemporaryChurn {
140    pub name: String,
141    pub allocation_count: usize,
142    pub total_bytes: usize,
143    pub average_lifetime_ms: f64,
144}
145
146/// Top N summary statistics
147#[derive(Debug, Clone)]
148pub struct TopNSummary {
149    pub total_allocations: usize,
150    pub total_bytes: usize,
151    pub leaked_count: usize,
152    pub leaked_bytes: usize,
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_top_allocation_sites_empty() {
161        let analyzer = TopNAnalyzer::new(vec![]);
162        let top = analyzer.top_allocation_sites(10);
163        assert!(top.is_empty());
164    }
165
166    #[test]
167    fn test_top_leaked_bytes_empty() {
168        let analyzer = TopNAnalyzer::new(vec![]);
169        let leaked = analyzer.top_leaked_bytes(10);
170        assert!(leaked.is_empty());
171    }
172
173    #[test]
174    fn test_top_temporary_churn_empty() {
175        let analyzer = TopNAnalyzer::new(vec![]);
176        let churn = analyzer.top_temporary_churn(10, 100);
177        assert!(churn.is_empty());
178    }
179
180    #[test]
181    fn test_summary_empty() {
182        let analyzer = TopNAnalyzer::new(vec![]);
183        let summary = analyzer.summary();
184        assert_eq!(summary.total_allocations, 0);
185        assert_eq!(summary.total_bytes, 0);
186        assert_eq!(summary.leaked_count, 0);
187        assert_eq!(summary.leaked_bytes, 0);
188    }
189}