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
185            .iter()
186            .map(|s| s.rss_bytes)
187            .max()
188            .unwrap_or(0)
189    }
190
191    /// Get average RSS from samples.
192    pub fn avg_rss(&self) -> usize {
193        if self.samples.is_empty() {
194            return 0;
195        }
196        self.samples.iter().map(|s| s.rss_bytes).sum::<usize>() / self.samples.len()
197    }
198
199    /// Check for memory growth trend.
200    pub fn has_growth_trend(&self) -> bool {
201        if self.samples.len() < 3 {
202            return false;
203        }
204
205        // Compare first third vs last third
206        let third = self.samples.len() / 3;
207        let first_avg: usize =
208            self.samples[..third].iter().map(|s| s.rss_bytes).sum::<usize>() / third;
209        let last_avg: usize = self.samples[self.samples.len() - third..]
210            .iter()
211            .map(|s| s.rss_bytes)
212            .sum::<usize>()
213            / third;
214
215        // More than 20% growth indicates trend
216        last_avg > first_avg + (first_avg / 5)
217    }
218
219    /// Generate summary string.
220    pub fn summary(&self) -> String {
221        let mut s = String::new();
222        s.push_str(&format!(
223            "Heap Report (duration: {:?})\n",
224            self.duration
225        ));
226        s.push_str(&format!(
227            "  Current RSS: {} bytes ({:.2} MB)\n",
228            self.current_stats.rss_bytes,
229            self.current_stats.rss_bytes as f64 / 1_048_576.0
230        ));
231        s.push_str(&format!(
232            "  Peak RSS: {} bytes ({:.2} MB)\n",
233            self.peak_rss(),
234            self.peak_rss() as f64 / 1_048_576.0
235        ));
236        s.push_str(&format!(
237            "  Fragmentation: {:.1}%\n",
238            self.current_stats.fragmentation_ratio() * 100.0
239        ));
240
241        if self.has_growth_trend() {
242            s.push_str("  ⚠️  Memory growth trend detected\n");
243        }
244
245        s
246    }
247}
248
249// ============================================================================
250// Platform-specific memory usage
251// ============================================================================
252
253/// Get current memory usage (RSS, Virtual).
254#[cfg(target_os = "linux")]
255fn get_memory_usage() -> (usize, usize) {
256    use std::fs;
257
258    // Read /proc/self/statm
259    if let Ok(statm) = fs::read_to_string("/proc/self/statm") {
260        let parts: Vec<&str> = statm.split_whitespace().collect();
261        if parts.len() >= 2 {
262            let page_size = 4096; // Usually 4K pages
263            let virtual_pages: usize = parts[0].parse().unwrap_or(0);
264            let rss_pages: usize = parts[1].parse().unwrap_or(0);
265            return (rss_pages * page_size, virtual_pages * page_size);
266        }
267    }
268
269    (0, 0)
270}
271
272#[cfg(target_os = "macos")]
273fn get_memory_usage() -> (usize, usize) {
274    // Use mach APIs on macOS
275    // For simplicity, return 0 if memory-stats is not available
276    #[cfg(feature = "profiling")]
277    {
278        if let Some(usage) = memory_stats::memory_stats() {
279            return (usage.physical_mem, usage.virtual_mem);
280        }
281    }
282    (0, 0)
283}
284
285#[cfg(target_os = "windows")]
286fn get_memory_usage() -> (usize, usize) {
287    #[cfg(feature = "profiling")]
288    {
289        if let Some(usage) = memory_stats::memory_stats() {
290            return (usage.physical_mem, usage.virtual_mem);
291        }
292    }
293    (0, 0)
294}
295
296#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
297fn get_memory_usage() -> (usize, usize) {
298    (0, 0)
299}
300
301// ============================================================================
302// DHAT Integration
303// ============================================================================
304
305/// DHAT heap profiler wrapper (requires `dhat-heap` feature).
306#[cfg(feature = "dhat-heap")]
307pub mod dhat_profiler {
308    use dhat::{Profiler, ProfilerBuilder};
309
310    /// Guard that stops profiling when dropped.
311    pub struct DhatGuard {
312        #[allow(dead_code)]
313        profiler: Profiler,
314    }
315
316    /// Start DHAT heap profiling.
317    pub fn start_dhat() -> DhatGuard {
318        let profiler = dhat::Profiler::builder().build();
319        DhatGuard { profiler }
320    }
321
322    /// Start DHAT with custom options.
323    pub fn start_dhat_with_file(path: &str) -> DhatGuard {
324        let profiler = dhat::Profiler::builder()
325            .file_name(path)
326            .build();
327        DhatGuard { profiler }
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_heap_sample() {
337        let sample = HeapSample::capture();
338        assert!(sample.timestamp_ms > 0);
339    }
340
341    #[test]
342    fn test_heap_stats() {
343        let stats = HeapStats::capture();
344        // Basic sanity check
345        assert!(stats.fragmentation_ratio() >= 0.0);
346        assert!(stats.efficiency() >= 0.0 && stats.efficiency() <= 1.0);
347    }
348
349    #[test]
350    fn test_heap_profiler() {
351        let profiler = HeapProfiler::new();
352
353        // Take some samples
354        profiler.sample();
355        profiler.sample();
356        profiler.sample();
357
358        let report = profiler.report();
359        assert_eq!(report.samples.len(), 3);
360    }
361
362    #[test]
363    fn test_growth_detection() {
364        let report = HeapReport {
365            duration: std::time::Duration::from_secs(10),
366            samples: vec![
367                HeapSample {
368                    timestamp_ms: 0,
369                    rss_bytes: 1000,
370                    virtual_bytes: 2000,
371                    heap_used: None,
372                },
373                HeapSample {
374                    timestamp_ms: 100,
375                    rss_bytes: 1100,
376                    virtual_bytes: 2100,
377                    heap_used: None,
378                },
379                HeapSample {
380                    timestamp_ms: 200,
381                    rss_bytes: 1200,
382                    virtual_bytes: 2200,
383                    heap_used: None,
384                },
385                HeapSample {
386                    timestamp_ms: 300,
387                    rss_bytes: 2000,
388                    virtual_bytes: 3000,
389                    heap_used: None,
390                },
391                HeapSample {
392                    timestamp_ms: 400,
393                    rss_bytes: 2500,
394                    virtual_bytes: 3500,
395                    heap_used: None,
396                },
397                HeapSample {
398                    timestamp_ms: 500,
399                    rss_bytes: 3000,
400                    virtual_bytes: 4000,
401                    heap_used: None,
402                },
403            ],
404            current_stats: HeapStats::default(),
405        };
406
407        assert!(report.has_growth_trend());
408    }
409}
410