1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CallStackHotspot {
13 pub call_stack_hash: u64,
15 pub total_frequency: u64,
17 pub total_size: usize,
19 pub impact_score: u64,
21 pub tasks: Vec<u64>,
23 pub average_size: f64,
25 pub peak_memory: usize,
27}
28
29impl CallStackHotspot {
30 pub fn new(call_stack_hash: u64) -> Self {
32 Self {
33 call_stack_hash,
34 total_frequency: 0,
35 total_size: 0,
36 impact_score: 0,
37 tasks: Vec::new(),
38 average_size: 0.0,
39 peak_memory: 0,
40 }
41 }
42
43 pub fn add_allocation(&mut self, size: usize, task_id: u64) {
45 self.total_frequency += 1;
46 self.total_size += size;
47 self.impact_score = self.total_frequency.saturating_mul(self.total_size as u64);
48 self.average_size = self.total_size as f64 / self.total_frequency as f64;
49 self.peak_memory = self.peak_memory.max(size);
50
51 if !self.tasks.contains(&task_id) {
52 self.tasks.push(task_id);
53 }
54 }
55
56 pub fn impact_score(&self) -> u64 {
58 self.impact_score
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct FrequencyAnalysis {
65 pub call_stack_hash: u64,
67 pub frequency_per_sec: f64,
69 pub pattern: AllocationFrequencyPattern,
71 pub time_window_ms: u64,
73 pub total_allocations: u64,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum AllocationFrequencyPattern {
80 Constant,
82 Increasing,
84 Decreasing,
86 Bursty,
88 Sporadic,
90}
91
92impl AllocationFrequencyPattern {
93 pub fn description(&self) -> &'static str {
95 match self {
96 Self::Constant => "Constant allocation rate",
97 Self::Increasing => "Increasing allocation rate (potential memory leak)",
98 Self::Decreasing => "Decreasing allocation rate",
99 Self::Bursty => "Bursty allocation pattern",
100 Self::Sporadic => "Sporadic allocation pattern",
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct MemoryUsagePeak {
108 pub timestamp_ms: u64,
110 pub task_id: u64,
112 pub task_name: String,
114 pub memory_usage: usize,
116 pub active_allocations: u64,
118 pub triggering_call_stack: u64,
120 pub duration_ms: u64,
122}
123
124pub struct HotspotAnalyzer {
126 hot_call_stacks: HashMap<u64, CallStackHotspot>,
128 frequency_data: HashMap<u64, FrequencyAnalysis>,
130 memory_peaks: Vec<MemoryUsagePeak>,
132 config: HotspotConfig,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct HotspotConfig {
139 pub min_hot_frequency: u64,
141 pub min_impact_score: u64,
143 pub max_hot_call_stacks: usize,
145 pub enable_frequency_analysis: bool,
147 pub enable_peak_detection: bool,
149 pub peak_threshold_percent: f64,
151}
152
153impl Default for HotspotConfig {
154 fn default() -> Self {
155 Self {
156 min_hot_frequency: 10,
157 min_impact_score: 1000,
158 max_hot_call_stacks: 100,
159 enable_frequency_analysis: true,
160 enable_peak_detection: true,
161 peak_threshold_percent: 90.0,
162 }
163 }
164}
165
166impl HotspotAnalyzer {
167 pub fn new() -> Self {
169 Self {
170 hot_call_stacks: HashMap::new(),
171 frequency_data: HashMap::new(),
172 memory_peaks: Vec::new(),
173 config: HotspotConfig::default(),
174 }
175 }
176
177 pub fn with_config(config: HotspotConfig) -> Self {
179 Self {
180 hot_call_stacks: HashMap::new(),
181 frequency_data: HashMap::new(),
182 memory_peaks: Vec::new(),
183 config,
184 }
185 }
186
187 pub fn analyze_allocation(
189 &mut self,
190 call_stack_hash: u64,
191 size: usize,
192 task_id: u64,
193 timestamp_ms: u64,
194 ) {
195 let hot_stack = self
196 .hot_call_stacks
197 .entry(call_stack_hash)
198 .or_insert_with(|| CallStackHotspot::new(call_stack_hash));
199 hot_stack.add_allocation(size, task_id);
200
201 if self.config.enable_peak_detection {
202 self.detect_memory_peak(task_id, size, call_stack_hash, timestamp_ms);
203 }
204 }
205
206 pub fn analyze_frequency_pattern(
208 &mut self,
209 call_stack_hash: u64,
210 allocations_in_window: u64,
211 time_window_ms: u64,
212 ) {
213 if !self.config.enable_frequency_analysis {
214 return;
215 }
216
217 let frequency_per_sec = if time_window_ms > 0 {
218 (allocations_in_window as f64 * 1000.0) / time_window_ms as f64
219 } else {
220 0.0
221 };
222
223 let pattern = self.detect_pattern(allocations_in_window, time_window_ms);
224
225 let frequency_data = FrequencyAnalysis {
226 call_stack_hash,
227 frequency_per_sec,
228 pattern,
229 time_window_ms,
230 total_allocations: allocations_in_window,
231 };
232
233 self.frequency_data.insert(call_stack_hash, frequency_data);
234 }
235
236 fn detect_pattern(&self, allocations: u64, time_window_ms: u64) -> AllocationFrequencyPattern {
238 let frequency_per_sec = if time_window_ms > 0 {
239 (allocations as f64 * 1000.0) / time_window_ms as f64
240 } else {
241 0.0
242 };
243
244 if frequency_per_sec < 1.0 {
245 AllocationFrequencyPattern::Sporadic
246 } else if frequency_per_sec > 100.0 {
247 AllocationFrequencyPattern::Bursty
248 } else if allocations > 1000 && time_window_ms > 10000 {
249 AllocationFrequencyPattern::Increasing
250 } else {
251 AllocationFrequencyPattern::Constant
252 }
253 }
254
255 fn detect_memory_peak(
257 &mut self,
258 task_id: u64,
259 memory_usage: usize,
260 call_stack_hash: u64,
261 timestamp_ms: u64,
262 ) {
263 let is_peak = if self.memory_peaks.is_empty() {
264 true
265 } else {
266 let max_peak = self
267 .memory_peaks
268 .iter()
269 .map(|p| p.memory_usage)
270 .max()
271 .unwrap_or(0);
272 memory_usage as f64 > max_peak as f64 * (self.config.peak_threshold_percent / 100.0)
273 };
274
275 if is_peak {
276 let peak = MemoryUsagePeak {
277 timestamp_ms,
278 task_id,
279 task_name: String::from("unknown"),
280 memory_usage,
281 active_allocations: 0,
282 triggering_call_stack: call_stack_hash,
283 duration_ms: 0,
284 };
285 self.memory_peaks.push(peak);
286 }
287 }
288
289 pub fn get_hot_call_stacks(&self) -> Vec<&CallStackHotspot> {
291 let mut stacks: Vec<&CallStackHotspot> = self
292 .hot_call_stacks
293 .values()
294 .filter(|s| {
295 s.total_frequency >= self.config.min_hot_frequency
296 || s.impact_score >= self.config.min_impact_score
297 })
298 .collect();
299
300 stacks.sort_by_key(|b| std::cmp::Reverse(b.impact_score));
301 stacks.truncate(self.config.max_hot_call_stacks);
302 stacks
303 }
304
305 pub fn get_frequency_data(&self) -> Vec<&FrequencyAnalysis> {
307 self.frequency_data.values().collect()
308 }
309
310 pub fn get_memory_peaks(&self) -> Vec<&MemoryUsagePeak> {
312 let mut peaks: Vec<&MemoryUsagePeak> = self.memory_peaks.iter().collect();
313 peaks.sort_by_key(|b| std::cmp::Reverse(b.memory_usage));
314 peaks
315 }
316
317 pub fn get_top_hot_call_stacks(&self, n: usize) -> Vec<&CallStackHotspot> {
319 let mut stacks = self.get_hot_call_stacks();
320 stacks.truncate(n);
321 stacks
322 }
323
324 pub fn get_top_memory_peaks(&self, n: usize) -> Vec<&MemoryUsagePeak> {
326 let mut peaks = self.get_memory_peaks();
327 peaks.truncate(n);
328 peaks
329 }
330
331 pub fn clear(&mut self) {
333 self.hot_call_stacks.clear();
334 self.frequency_data.clear();
335 self.memory_peaks.clear();
336 }
337
338 pub fn config(&self) -> &HotspotConfig {
340 &self.config
341 }
342
343 pub fn set_config(&mut self, config: HotspotConfig) {
345 self.config = config;
346 }
347
348 pub fn get_statistics(&self) -> HotspotStatistics {
350 HotspotStatistics {
351 total_call_stacks: self.hot_call_stacks.len(),
352 hot_call_stacks: self.get_hot_call_stacks().len(),
353 total_memory_peaks: self.memory_peaks.len(),
354 total_frequency_data: self.frequency_data.len(),
355 total_allocations_analyzed: self
356 .hot_call_stacks
357 .values()
358 .map(|s| s.total_frequency)
359 .sum(),
360 total_memory_analyzed: self.hot_call_stacks.values().map(|s| s.total_size).sum(),
361 }
362 }
363}
364
365impl Default for HotspotAnalyzer {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct HotspotStatistics {
374 pub total_call_stacks: usize,
376 pub hot_call_stacks: usize,
378 pub total_memory_peaks: usize,
380 pub total_frequency_data: usize,
382 pub total_allocations_analyzed: u64,
384 pub total_memory_analyzed: usize,
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_hot_call_stack_creation() {
394 let stack = CallStackHotspot::new(12345);
395 assert_eq!(stack.call_stack_hash, 12345);
396 assert_eq!(stack.total_frequency, 0);
397 assert_eq!(stack.total_size, 0);
398 }
399
400 #[test]
401 fn test_hot_call_stack_add_allocation() {
402 let mut stack = CallStackHotspot::new(12345);
403 stack.add_allocation(1024, 1);
404 stack.add_allocation(2048, 1);
405
406 assert_eq!(stack.total_frequency, 2);
407 assert_eq!(stack.total_size, 3072);
408 assert_eq!(stack.impact_score, 6144);
409 assert_eq!(stack.average_size, 1536.0);
410 assert_eq!(stack.peak_memory, 2048);
411 assert_eq!(stack.tasks.len(), 1);
412 }
413
414 #[test]
415 fn test_hotspot_analyzer_creation() {
416 let analyzer = HotspotAnalyzer::new();
417 assert!(analyzer.hot_call_stacks.is_empty());
418 assert!(analyzer.memory_peaks.is_empty());
419 }
420
421 #[test]
422 fn test_analyze_allocation() {
423 let mut analyzer = HotspotAnalyzer::new();
424 analyzer.analyze_allocation(12345, 1024, 1, 1000);
425
426 let stacks = analyzer.get_hot_call_stacks();
427 assert!(!stacks.is_empty());
428 assert_eq!(stacks[0].call_stack_hash, 12345);
429 assert_eq!(stacks[0].total_frequency, 1);
430 }
431
432 #[test]
433 fn test_frequency_pattern_detection() {
434 let mut analyzer = HotspotAnalyzer::new();
435
436 analyzer.analyze_frequency_pattern(12345, 50, 1000);
437 let data = analyzer.get_frequency_data();
438 assert_eq!(data[0].pattern, AllocationFrequencyPattern::Constant);
439 }
440
441 #[test]
442 fn test_memory_peak_detection() {
443 let mut analyzer = HotspotAnalyzer::new();
444 analyzer.detect_memory_peak(1, 1024, 12345, 1000);
445 analyzer.detect_memory_peak(1, 2048, 12345, 2000);
446 analyzer.detect_memory_peak(1, 4096, 12345, 3000);
447
448 let peaks = analyzer.get_memory_peaks();
449 assert!(!peaks.is_empty());
450 assert_eq!(peaks[0].memory_usage, 4096);
451 }
452
453 #[test]
454 fn test_get_top_hot_call_stacks() {
455 let mut analyzer = HotspotAnalyzer::new();
456 analyzer.analyze_allocation(1, 1024, 1, 1000);
457 analyzer.analyze_allocation(2, 2048, 1, 1000);
458 analyzer.analyze_allocation(3, 4096, 1, 1000);
459
460 let top = analyzer.get_top_hot_call_stacks(2);
461 assert!(top.len() <= 2);
462 }
463
464 #[test]
465 fn test_clear() {
466 let mut analyzer = HotspotAnalyzer::new();
467 analyzer.analyze_allocation(12345, 1024, 1, 1000);
468 analyzer.clear();
469
470 assert!(analyzer.hot_call_stacks.is_empty());
471 assert!(analyzer.memory_peaks.is_empty());
472 }
473
474 #[test]
475 fn test_get_statistics() {
476 let mut analyzer = HotspotAnalyzer::new();
477 analyzer.analyze_allocation(12345, 1024, 1, 1000);
478 analyzer.analyze_allocation(12345, 2048, 1, 1000);
479
480 let stats = analyzer.get_statistics();
481 assert_eq!(stats.total_call_stacks, 1);
482 assert_eq!(stats.total_allocations_analyzed, 2);
483 assert_eq!(stats.total_memory_analyzed, 3072);
484 }
485
486 #[test]
487 fn test_frequency_pattern_description() {
488 assert_eq!(
489 AllocationFrequencyPattern::Constant.description(),
490 "Constant allocation rate"
491 );
492 assert_eq!(
493 AllocationFrequencyPattern::Increasing.description(),
494 "Increasing allocation rate (potential memory leak)"
495 );
496 }
497
498 #[test]
499 fn test_custom_config() {
500 let config = HotspotConfig {
501 min_hot_frequency: 100,
502 min_impact_score: 10000,
503 max_hot_call_stacks: 50,
504 ..Default::default()
505 };
506 let analyzer = HotspotAnalyzer::with_config(config);
507
508 assert_eq!(analyzer.config().min_hot_frequency, 100);
509 assert_eq!(analyzer.config().max_hot_call_stacks, 50);
510 }
511}