Skip to main content

memscope_rs/capture/backends/
hotspot_analysis.rs

1//! Enhanced hotspot analysis for performance optimization
2//!
3//! This module provides advanced hotspot detection capabilities,
4//! including call stack analysis, frequency pattern detection,
5//! and memory peak detection.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Hot call stack information
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CallStackHotspot {
13    /// Hash of the call stack
14    pub call_stack_hash: u64,
15    /// Total frequency across all allocations
16    pub total_frequency: u64,
17    /// Total memory allocated by this call stack
18    pub total_size: usize,
19    /// Impact score (frequency * size)
20    pub impact_score: u64,
21    /// Tasks that use this call stack
22    pub tasks: Vec<u64>,
23    /// Average allocation size
24    pub average_size: f64,
25    /// Peak memory usage from this call stack
26    pub peak_memory: usize,
27}
28
29impl CallStackHotspot {
30    /// Create new hot call stack
31    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    /// Add allocation to this call stack
44    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    /// Get impact score
57    pub fn impact_score(&self) -> u64 {
58        self.impact_score
59    }
60}
61
62/// Frequency pattern analysis
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct FrequencyAnalysis {
65    /// Call stack hash
66    pub call_stack_hash: u64,
67    /// Allocation frequency (allocations per second)
68    pub frequency_per_sec: f64,
69    /// Pattern type
70    pub pattern: AllocationFrequencyPattern,
71    /// Time window analyzed
72    pub time_window_ms: u64,
73    /// Total allocations in window
74    pub total_allocations: u64,
75}
76
77/// Frequency pattern type
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum AllocationFrequencyPattern {
80    /// Constant allocation rate
81    Constant,
82    /// Increasing allocation rate (potential leak)
83    Increasing,
84    /// Decreasing allocation rate
85    Decreasing,
86    /// Bursty allocation pattern
87    Bursty,
88    /// Sporadic allocation pattern
89    Sporadic,
90}
91
92impl AllocationFrequencyPattern {
93    /// Get description of pattern
94    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/// Memory peak detection
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct MemoryUsagePeak {
108    /// Timestamp of the peak
109    pub timestamp_ms: u64,
110    /// Task ID that caused the peak
111    pub task_id: u64,
112    /// Task name
113    pub task_name: String,
114    /// Memory usage at peak (bytes)
115    pub memory_usage: usize,
116    /// Number of active allocations at peak
117    pub active_allocations: u64,
118    /// Call stack that triggered the peak
119    pub triggering_call_stack: u64,
120    /// Peak duration in milliseconds
121    pub duration_ms: u64,
122}
123
124/// Enhanced hotspot analyzer
125pub struct HotspotAnalyzer {
126    /// Hot call stacks detected
127    hot_call_stacks: HashMap<u64, CallStackHotspot>,
128    /// Frequency data for call stacks
129    frequency_data: HashMap<u64, FrequencyAnalysis>,
130    /// Memory peaks detected
131    memory_peaks: Vec<MemoryUsagePeak>,
132    /// Configuration
133    config: HotspotConfig,
134}
135
136/// Hotspot analyzer configuration
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct HotspotConfig {
139    /// Minimum frequency to consider a call stack as hot
140    pub min_hot_frequency: u64,
141    /// Minimum impact score to consider a call stack as hot
142    pub min_impact_score: u64,
143    /// Maximum number of hot call stacks to track
144    pub max_hot_call_stacks: usize,
145    /// Enable frequency pattern analysis
146    pub enable_frequency_analysis: bool,
147    /// Enable memory peak detection
148    pub enable_peak_detection: bool,
149    /// Peak detection threshold (percentage of peak memory)
150    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    /// Create new hotspot analyzer with default configuration
168    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    /// Create new hotspot analyzer with custom configuration
178    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    /// Analyze allocation for hotspot detection
188    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    /// Analyze frequency pattern for a call stack
207    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    /// Detect frequency pattern
237    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    /// Detect memory peak
256    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    /// Get hot call stacks sorted by impact score
290    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    /// Get frequency data for all call stacks
306    pub fn get_frequency_data(&self) -> Vec<&FrequencyAnalysis> {
307        self.frequency_data.values().collect()
308    }
309
310    /// Get memory peaks sorted by memory usage
311    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    /// Get top N hot call stacks by impact score
318    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    /// Get top N memory peaks by memory usage
325    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    /// Clear all collected data
332    pub fn clear(&mut self) {
333        self.hot_call_stacks.clear();
334        self.frequency_data.clear();
335        self.memory_peaks.clear();
336    }
337
338    /// Get analyzer configuration
339    pub fn config(&self) -> &HotspotConfig {
340        &self.config
341    }
342
343    /// Update analyzer configuration
344    pub fn set_config(&mut self, config: HotspotConfig) {
345        self.config = config;
346    }
347
348    /// Get statistics
349    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/// Hotspot analysis statistics
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct HotspotStatistics {
374    /// Total number of call stacks analyzed
375    pub total_call_stacks: usize,
376    /// Number of hot call stacks detected
377    pub hot_call_stacks: usize,
378    /// Total number of memory peaks detected
379    pub total_memory_peaks: usize,
380    /// Total frequency data collected
381    pub total_frequency_data: usize,
382    /// Total allocations analyzed
383    pub total_allocations_analyzed: u64,
384    /// Total memory analyzed (bytes)
385    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}