profile_inspect/analysis/
heap_analysis.rs1use std::collections::HashMap;
2
3use crate::ir::{FrameCategory, FrameId, ProfileIR};
4
5#[derive(Debug, Clone)]
7pub struct AllocationStats {
8 pub frame_id: FrameId,
10 pub name: String,
12 pub location: String,
14 pub category: FrameCategory,
16 pub self_size: u64,
18 pub total_size: u64,
20 pub allocation_count: u32,
22}
23
24impl AllocationStats {
25 #[expect(clippy::cast_precision_loss)]
27 pub fn self_percent(&self, total: u64) -> f64 {
28 if total == 0 {
29 0.0
30 } else {
31 (self.self_size as f64 / total as f64) * 100.0
32 }
33 }
34
35 pub fn format_size(bytes: u64) -> String {
37 const KB: u64 = 1024;
38 const MB: u64 = KB * 1024;
39 const GB: u64 = MB * 1024;
40
41 if bytes >= GB {
42 format!("{:.2} GB", bytes as f64 / GB as f64)
43 } else if bytes >= MB {
44 format!("{:.2} MB", bytes as f64 / MB as f64)
45 } else if bytes >= KB {
46 format!("{:.2} KB", bytes as f64 / KB as f64)
47 } else {
48 format!("{bytes} B")
49 }
50 }
51}
52
53#[derive(Debug)]
55pub struct HeapAnalysis {
56 pub total_size: u64,
58 pub total_allocations: usize,
60 pub functions: Vec<AllocationStats>,
62 pub category_breakdown: CategorySizeBreakdown,
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct CategorySizeBreakdown {
69 pub app: u64,
70 pub deps: u64,
71 pub node_internal: u64,
72 pub v8_internal: u64,
73 pub native: u64,
74}
75
76impl CategorySizeBreakdown {
77 pub fn total(&self) -> u64 {
79 self.app + self.deps + self.node_internal + self.v8_internal + self.native
80 }
81}
82
83pub struct HeapAnalyzer {
85 min_percent: f64,
87 top_n: usize,
89 include_internals: bool,
91}
92
93impl HeapAnalyzer {
94 pub fn new() -> Self {
96 Self {
97 min_percent: 0.0,
98 top_n: 50,
99 include_internals: false,
100 }
101 }
102
103 pub fn min_percent(mut self, percent: f64) -> Self {
105 self.min_percent = percent;
106 self
107 }
108
109 pub fn top_n(mut self, n: usize) -> Self {
111 self.top_n = n;
112 self
113 }
114
115 pub fn include_internals(mut self, include: bool) -> Self {
117 self.include_internals = include;
118 self
119 }
120
121 #[expect(clippy::cast_precision_loss)]
123 pub fn analyze(&self, profile: &ProfileIR) -> HeapAnalysis {
124 let total_size = profile.total_weight();
125 let total_allocations = profile.sample_count();
126
127 let mut self_sizes: HashMap<FrameId, u64> = HashMap::new();
129 let mut total_sizes: HashMap<FrameId, u64> = HashMap::new();
130 let mut alloc_counts: HashMap<FrameId, u32> = HashMap::new();
131 let mut category_breakdown = CategorySizeBreakdown::default();
132
133 for sample in &profile.samples {
134 let size = sample.weight;
135
136 if let Some(stack) = profile.get_stack(sample.stack_id) {
137 if let Some(&leaf_frame) = stack.frames.last() {
139 *self_sizes.entry(leaf_frame).or_default() += size;
140 *alloc_counts.entry(leaf_frame).or_default() += 1;
141
142 if let Some(frame) = profile.get_frame(leaf_frame) {
144 match frame.category {
145 FrameCategory::App => category_breakdown.app += size,
146 FrameCategory::Deps => category_breakdown.deps += size,
147 FrameCategory::NodeInternal => {
148 category_breakdown.node_internal += size;
149 }
150 FrameCategory::V8Internal => category_breakdown.v8_internal += size,
151 FrameCategory::Native => category_breakdown.native += size,
152 }
153 }
154 }
155
156 for &frame_id in &stack.frames {
158 *total_sizes.entry(frame_id).or_default() += size;
159 }
160 }
161 }
162
163 let mut functions: Vec<AllocationStats> = profile
165 .frames
166 .iter()
167 .filter_map(|frame| {
168 let self_size = self_sizes.get(&frame.id).copied().unwrap_or(0);
169 let total_size = total_sizes.get(&frame.id).copied().unwrap_or(0);
170
171 if self_size == 0 && total_size == 0 {
173 return None;
174 }
175
176 if !self.include_internals && frame.category.is_internal() {
178 return None;
179 }
180
181 let self_pct = if total_size > 0 {
183 (self_size as f64 / total_size as f64) * 100.0
184 } else {
185 0.0
186 };
187 if self_pct < self.min_percent && self.min_percent > 0.0 {
188 return None;
189 }
190
191 Some(AllocationStats {
192 frame_id: frame.id,
193 name: frame.display_name().to_string(),
194 location: frame.location(),
195 category: frame.category,
196 self_size,
197 total_size,
198 allocation_count: alloc_counts.get(&frame.id).copied().unwrap_or(0),
199 })
200 })
201 .collect();
202
203 functions.sort_by(|a, b| b.self_size.cmp(&a.self_size));
205 functions.truncate(self.top_n);
206
207 HeapAnalysis {
208 total_size,
209 total_allocations,
210 functions,
211 category_breakdown,
212 }
213 }
214}
215
216impl Default for HeapAnalyzer {
217 fn default() -> Self {
218 Self::new()
219 }
220}