Skip to main content

prax_query/profiling/
heap.rs

1//! Heap profiling and analysis.
2//!
3//! Provides heap-level profiling using system APIs and optional DHAT integration.
4
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::time::Instant;
7
8// ============================================================================
9// Heap Profiler
10// ============================================================================
11
12/// Heap profiler for analyzing memory usage patterns.
13pub struct HeapProfiler {
14    /// Start time for profiling session.
15    start_time: Instant,
16    /// Samples collected.
17    samples: parking_lot::Mutex<Vec<HeapSample>>,
18    /// Sample interval in ms.
19    sample_interval_ms: AtomicUsize,
20}
21
22impl HeapProfiler {
23    /// Create a new heap profiler.
24    pub fn new() -> Self {
25        Self {
26            start_time: Instant::now(),
27            samples: parking_lot::Mutex::new(Vec::new()),
28            sample_interval_ms: AtomicUsize::new(100),
29        }
30    }
31
32    /// Set the sample interval.
33    pub fn set_sample_interval(&self, ms: usize) {
34        self.sample_interval_ms.store(ms, Ordering::Relaxed);
35    }
36
37    /// Take a heap sample.
38    pub fn sample(&self) -> HeapSample {
39        let sample = HeapSample::capture();
40        self.samples.lock().push(sample.clone());
41        sample
42    }
43
44    /// Get current heap statistics.
45    pub fn stats(&self) -> HeapStats {
46        HeapStats::capture()
47    }
48
49    /// Get all samples.
50    pub fn samples(&self) -> Vec<HeapSample> {
51        self.samples.lock().clone()
52    }
53
54    /// Generate a heap report.
55    pub fn report(&self) -> HeapReport {
56        let samples = self.samples.lock().clone();
57        let current = HeapStats::capture();
58
59        HeapReport {
60            duration: self.start_time.elapsed(),
61            samples,
62            current_stats: current,
63        }
64    }
65
66    /// Clear all samples.
67    pub fn clear(&self) {
68        self.samples.lock().clear();
69    }
70}
71
72impl Default for HeapProfiler {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78// ============================================================================
79// Heap Sample
80// ============================================================================
81
82/// A point-in-time sample of heap state.
83#[derive(Debug, Clone)]
84pub struct HeapSample {
85    /// Timestamp (ms since profiler start).
86    pub timestamp_ms: u64,
87    /// Resident set size (RSS).
88    pub rss_bytes: usize,
89    /// Virtual memory size.
90    pub virtual_bytes: usize,
91    /// Heap used (if available).
92    pub heap_used: Option<usize>,
93}
94
95impl HeapSample {
96    /// Capture a heap sample.
97    pub fn capture() -> Self {
98        let (rss, virtual_mem) = get_memory_usage();
99
100        Self {
101            timestamp_ms: std::time::SystemTime::now()
102                .duration_since(std::time::UNIX_EPOCH)
103                .map(|d| d.as_millis() as u64)
104                .unwrap_or(0),
105            rss_bytes: rss,
106            virtual_bytes: virtual_mem,
107            heap_used: None,
108        }
109    }
110}
111
112// ============================================================================
113// Heap Statistics
114// ============================================================================
115
116/// Heap statistics.
117#[derive(Debug, Clone, Default)]
118pub struct HeapStats {
119    /// Bytes currently used.
120    pub used_bytes: usize,
121    /// Bytes currently allocated from OS.
122    pub allocated_bytes: usize,
123    /// Resident set size.
124    pub rss_bytes: usize,
125    /// Virtual memory size.
126    pub virtual_bytes: usize,
127    /// Peak RSS.
128    pub peak_rss_bytes: usize,
129    /// Number of heap segments.
130    pub segments: usize,
131}
132
133impl HeapStats {
134    /// Capture current heap statistics.
135    pub fn capture() -> Self {
136        let (rss, virtual_mem) = get_memory_usage();
137
138        Self {
139            used_bytes: rss, // Approximation
140            allocated_bytes: virtual_mem,
141            rss_bytes: rss,
142            virtual_bytes: virtual_mem,
143            peak_rss_bytes: rss, // Would need tracking for true peak
144            segments: 0,
145        }
146    }
147
148    /// Calculate fragmentation ratio (0.0 = no fragmentation, 1.0 = highly fragmented).
149    pub fn fragmentation_ratio(&self) -> f64 {
150        if self.allocated_bytes == 0 {
151            return 0.0;
152        }
153        let unused = self.allocated_bytes.saturating_sub(self.used_bytes);
154        unused as f64 / self.allocated_bytes as f64
155    }
156
157    /// Get memory efficiency (used / allocated).
158    pub fn efficiency(&self) -> f64 {
159        if self.allocated_bytes == 0 {
160            return 1.0;
161        }
162        self.used_bytes as f64 / self.allocated_bytes as f64
163    }
164}
165
166// ============================================================================
167// Heap Report
168// ============================================================================
169
170/// Comprehensive heap report.
171#[derive(Debug, Clone)]
172pub struct HeapReport {
173    /// Duration of profiling.
174    pub duration: std::time::Duration,
175    /// Collected samples.
176    pub samples: Vec<HeapSample>,
177    /// Current heap stats.
178    pub current_stats: HeapStats,
179}
180
181impl HeapReport {
182    /// Get peak RSS from samples.
183    pub fn peak_rss(&self) -> usize {
184        self.samples.iter().map(|s| s.rss_bytes).max().unwrap_or(0)
185    }
186
187    /// Get average RSS from samples.
188    pub fn avg_rss(&self) -> usize {
189        if self.samples.is_empty() {
190            return 0;
191        }
192        self.samples.iter().map(|s| s.rss_bytes).sum::<usize>() / self.samples.len()
193    }
194
195    /// Check for memory growth trend.
196    pub fn has_growth_trend(&self) -> bool {
197        if self.samples.len() < 3 {
198            return false;
199        }
200
201        // Compare first third vs last third
202        let third = self.samples.len() / 3;
203        let first_avg: usize = self.samples[..third]
204            .iter()
205            .map(|s| s.rss_bytes)
206            .sum::<usize>()
207            / third;
208        let last_avg: usize = self.samples[self.samples.len() - third..]
209            .iter()
210            .map(|s| s.rss_bytes)
211            .sum::<usize>()
212            / third;
213
214        // More than 20% growth indicates trend
215        last_avg > first_avg + (first_avg / 5)
216    }
217
218    /// Generate summary string.
219    pub fn summary(&self) -> String {
220        let mut s = String::new();
221        s.push_str(&format!("Heap Report (duration: {:?})\n", self.duration));
222        s.push_str(&format!(
223            "  Current RSS: {} bytes ({:.2} MB)\n",
224            self.current_stats.rss_bytes,
225            self.current_stats.rss_bytes as f64 / 1_048_576.0
226        ));
227        s.push_str(&format!(
228            "  Peak RSS: {} bytes ({:.2} MB)\n",
229            self.peak_rss(),
230            self.peak_rss() as f64 / 1_048_576.0
231        ));
232        s.push_str(&format!(
233            "  Fragmentation: {:.1}%\n",
234            self.current_stats.fragmentation_ratio() * 100.0
235        ));
236
237        if self.has_growth_trend() {
238            s.push_str("  ⚠️  Memory growth trend detected\n");
239        }
240
241        s
242    }
243}
244
245// ============================================================================
246// Platform-specific memory usage
247// ============================================================================
248
249/// Get current memory usage (RSS, Virtual).
250#[cfg(target_os = "linux")]
251fn get_memory_usage() -> (usize, usize) {
252    use std::fs;
253
254    // Read /proc/self/statm
255    if let Ok(statm) = fs::read_to_string("/proc/self/statm") {
256        let parts: Vec<&str> = statm.split_whitespace().collect();
257        if parts.len() >= 2 {
258            let page_size = 4096; // Usually 4K pages
259            let virtual_pages: usize = parts[0].parse().unwrap_or(0);
260            let rss_pages: usize = parts[1].parse().unwrap_or(0);
261            return (rss_pages * page_size, virtual_pages * page_size);
262        }
263    }
264
265    (0, 0)
266}
267
268#[cfg(target_os = "macos")]
269fn get_memory_usage() -> (usize, usize) {
270    // Use mach APIs on macOS
271    // For simplicity, return 0 if memory-stats is not available
272    #[cfg(feature = "profiling")]
273    {
274        if let Some(usage) = memory_stats::memory_stats() {
275            return (usage.physical_mem, usage.virtual_mem);
276        }
277    }
278    (0, 0)
279}
280
281#[cfg(target_os = "windows")]
282fn get_memory_usage() -> (usize, usize) {
283    #[cfg(feature = "profiling")]
284    {
285        if let Some(usage) = memory_stats::memory_stats() {
286            return (usage.physical_mem, usage.virtual_mem);
287        }
288    }
289    (0, 0)
290}
291
292#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
293fn get_memory_usage() -> (usize, usize) {
294    (0, 0)
295}
296
297// ============================================================================
298// DHAT Integration
299// ============================================================================
300
301/// DHAT heap profiler wrapper (requires `dhat-heap` feature).
302#[cfg(feature = "dhat-heap")]
303pub mod dhat_profiler {
304    use dhat::{Profiler, ProfilerBuilder};
305
306    /// Guard that stops profiling when dropped.
307    pub struct DhatGuard {
308        #[allow(dead_code)]
309        profiler: Profiler,
310    }
311
312    /// Start DHAT heap profiling.
313    pub fn start_dhat() -> DhatGuard {
314        let profiler = dhat::Profiler::builder().build();
315        DhatGuard { profiler }
316    }
317
318    /// Start DHAT with custom options.
319    pub fn start_dhat_with_file(path: &str) -> DhatGuard {
320        let profiler = dhat::Profiler::builder().file_name(path).build();
321        DhatGuard { profiler }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_heap_sample() {
331        let sample = HeapSample::capture();
332        assert!(sample.timestamp_ms > 0);
333    }
334
335    #[test]
336    fn test_heap_stats() {
337        let stats = HeapStats::capture();
338        // Basic sanity check
339        assert!(stats.fragmentation_ratio() >= 0.0);
340        assert!(stats.efficiency() >= 0.0 && stats.efficiency() <= 1.0);
341    }
342
343    #[test]
344    fn test_heap_profiler() {
345        let profiler = HeapProfiler::new();
346
347        // Take some samples
348        profiler.sample();
349        profiler.sample();
350        profiler.sample();
351
352        let report = profiler.report();
353        assert_eq!(report.samples.len(), 3);
354    }
355
356    #[test]
357    fn test_growth_detection() {
358        let report = HeapReport {
359            duration: std::time::Duration::from_secs(10),
360            samples: vec![
361                HeapSample {
362                    timestamp_ms: 0,
363                    rss_bytes: 1000,
364                    virtual_bytes: 2000,
365                    heap_used: None,
366                },
367                HeapSample {
368                    timestamp_ms: 100,
369                    rss_bytes: 1100,
370                    virtual_bytes: 2100,
371                    heap_used: None,
372                },
373                HeapSample {
374                    timestamp_ms: 200,
375                    rss_bytes: 1200,
376                    virtual_bytes: 2200,
377                    heap_used: None,
378                },
379                HeapSample {
380                    timestamp_ms: 300,
381                    rss_bytes: 2000,
382                    virtual_bytes: 3000,
383                    heap_used: None,
384                },
385                HeapSample {
386                    timestamp_ms: 400,
387                    rss_bytes: 2500,
388                    virtual_bytes: 3500,
389                    heap_used: None,
390                },
391                HeapSample {
392                    timestamp_ms: 500,
393                    rss_bytes: 3000,
394                    virtual_bytes: 4000,
395                    heap_used: None,
396                },
397            ],
398            current_stats: HeapStats::default(),
399        };
400
401        assert!(report.has_growth_trend());
402    }
403}