Skip to main content

trustformers_debug/utilities/
performance.rs

1//! Performance monitoring and profiling utilities
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::{Duration, Instant};
7
8/// Performance monitoring utilities
9#[derive(Debug)]
10pub struct PerformanceMonitor {
11    start_time: Instant,
12    checkpoints: HashMap<String, Instant>,
13    durations: HashMap<String, Duration>,
14}
15
16impl PerformanceMonitor {
17    pub fn new() -> Self {
18        Self {
19            start_time: Instant::now(),
20            checkpoints: HashMap::new(),
21            durations: HashMap::new(),
22        }
23    }
24
25    pub fn checkpoint(&mut self, name: &str) {
26        self.checkpoints.insert(name.to_string(), Instant::now());
27    }
28
29    pub fn end_checkpoint(&mut self, name: &str) -> Option<Duration> {
30        if let Some(start) = self.checkpoints.remove(name) {
31            let duration = start.elapsed();
32            self.durations.insert(name.to_string(), duration);
33            Some(duration)
34        } else {
35            None
36        }
37    }
38
39    pub fn total_elapsed(&self) -> Duration {
40        self.start_time.elapsed()
41    }
42
43    pub fn get_durations(&self) -> &HashMap<String, Duration> {
44        &self.durations
45    }
46
47    pub fn performance_report(&self) -> String {
48        let mut report = format!(
49            "Performance Report - Total: {:.2}ms\n",
50            self.total_elapsed().as_millis()
51        );
52
53        for (name, duration) in &self.durations {
54            report.push_str(&format!("  {}: {:.2}ms\n", name, duration.as_millis()));
55        }
56
57        report
58    }
59
60    /// Get detailed performance metrics
61    pub fn get_detailed_metrics(&self) -> PerformanceMetrics {
62        let total_duration = self.total_elapsed();
63        let checkpoint_count = self.durations.len();
64
65        let avg_checkpoint_duration = if checkpoint_count > 0 {
66            self.durations.values().map(|d| d.as_millis() as f64).sum::<f64>()
67                / checkpoint_count as f64
68        } else {
69            0.0
70        };
71
72        let slowest_checkpoint = self
73            .durations
74            .iter()
75            .max_by_key(|(_, duration)| *duration)
76            .map(|(name, duration)| (name.clone(), *duration));
77
78        let fastest_checkpoint = self
79            .durations
80            .iter()
81            .min_by_key(|(_, duration)| *duration)
82            .map(|(name, duration)| (name.clone(), *duration));
83
84        PerformanceMetrics {
85            total_duration,
86            checkpoint_count,
87            avg_checkpoint_duration,
88            slowest_checkpoint,
89            fastest_checkpoint,
90            durations: self.durations.clone(),
91        }
92    }
93
94    /// Analyze performance bottlenecks
95    pub fn analyze_bottlenecks(&self, threshold_percentile: f64) -> BottleneckAnalysis {
96        let mut duration_values: Vec<u128> =
97            self.durations.values().map(|d| d.as_millis()).collect();
98        duration_values.sort();
99
100        let threshold_index = ((duration_values.len() as f64 * threshold_percentile) as usize)
101            .min(duration_values.len().saturating_sub(1));
102        let threshold = duration_values.get(threshold_index).copied().unwrap_or(0);
103
104        let bottlenecks: Vec<PerformanceBottleneck> = self
105            .durations
106            .iter()
107            .filter(|(_, duration)| duration.as_millis() >= threshold)
108            .map(|(name, duration)| PerformanceBottleneck {
109                checkpoint_name: name.clone(),
110                duration: *duration,
111                severity: Self::classify_bottleneck_severity(
112                    duration.as_millis(),
113                    &duration_values,
114                ),
115                recommendation: Self::generate_bottleneck_recommendation(name, *duration),
116            })
117            .collect();
118
119        let total_bottleneck_time: Duration = bottlenecks.iter().map(|b| b.duration).sum();
120
121        BottleneckAnalysis {
122            threshold_ms: threshold,
123            bottlenecks,
124            total_bottleneck_time,
125            bottleneck_percentage: if self.total_elapsed().as_millis() > 0 {
126                (total_bottleneck_time.as_millis() as f64 / self.total_elapsed().as_millis() as f64)
127                    * 100.0
128            } else {
129                0.0
130            },
131        }
132    }
133
134    fn classify_bottleneck_severity(
135        duration_ms: u128,
136        all_durations: &[u128],
137    ) -> BottleneckSeverity {
138        if all_durations.is_empty() {
139            return BottleneckSeverity::Low;
140        }
141
142        let max_duration = all_durations.iter().max().copied().unwrap_or(0);
143        let avg_duration = all_durations.iter().sum::<u128>() / all_durations.len() as u128;
144
145        if duration_ms >= max_duration {
146            BottleneckSeverity::Critical
147        } else if duration_ms > avg_duration * 3 {
148            BottleneckSeverity::High
149        } else if duration_ms > avg_duration * 2 {
150            BottleneckSeverity::Medium
151        } else {
152            BottleneckSeverity::Low
153        }
154    }
155
156    fn generate_bottleneck_recommendation(checkpoint_name: &str, duration: Duration) -> String {
157        let duration_ms = duration.as_millis();
158
159        match checkpoint_name {
160            name if name.contains("forward") => {
161                if duration_ms > 1000 {
162                    "Consider model pruning or quantization to reduce forward pass time".to_string()
163                } else {
164                    "Monitor forward pass efficiency".to_string()
165                }
166            },
167            name if name.contains("backward") => {
168                if duration_ms > 2000 {
169                    "Consider gradient accumulation or mixed precision training".to_string()
170                } else {
171                    "Monitor backward pass efficiency".to_string()
172                }
173            },
174            name if name.contains("data") => {
175                "Consider data loading optimization or caching".to_string()
176            },
177            name if name.contains("io") => {
178                "Consider I/O optimization or async processing".to_string()
179            },
180            _ => {
181                format!(
182                    "Optimize '{}' operation - duration: {}ms",
183                    checkpoint_name, duration_ms
184                )
185            },
186        }
187    }
188}
189
190impl Default for PerformanceMonitor {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196/// Detailed performance metrics
197#[derive(Debug, Serialize, Deserialize)]
198pub struct PerformanceMetrics {
199    pub total_duration: Duration,
200    pub checkpoint_count: usize,
201    pub avg_checkpoint_duration: f64,
202    pub slowest_checkpoint: Option<(String, Duration)>,
203    pub fastest_checkpoint: Option<(String, Duration)>,
204    pub durations: HashMap<String, Duration>,
205}
206
207/// Performance bottleneck analysis
208#[derive(Debug, Serialize, Deserialize)]
209pub struct BottleneckAnalysis {
210    pub threshold_ms: u128,
211    pub bottlenecks: Vec<PerformanceBottleneck>,
212    pub total_bottleneck_time: Duration,
213    pub bottleneck_percentage: f64,
214}
215
216/// Individual performance bottleneck
217#[derive(Debug, Serialize, Deserialize)]
218pub struct PerformanceBottleneck {
219    pub checkpoint_name: String,
220    pub duration: Duration,
221    pub severity: BottleneckSeverity,
222    pub recommendation: String,
223}
224
225/// Bottleneck severity levels
226#[derive(Debug, Serialize, Deserialize)]
227pub enum BottleneckSeverity {
228    Low,
229    Medium,
230    High,
231    Critical,
232}
233
234/// Memory performance monitoring
235#[derive(Debug)]
236pub struct SystemMemoryProfiler {
237    /// RSS at construction; `None` when the platform gave no reading.
238    baseline_memory: Option<usize>,
239    /// Largest RSS observed across [`SystemMemoryProfiler::checkpoint`] calls;
240    /// `None` until at least one real reading has been taken.
241    peak_memory: Option<usize>,
242    /// Insertion-ordered so the delta chain in
243    /// [`SystemMemoryProfiler::memory_report`] follows the order the caller
244    /// actually took the checkpoints in. With a `HashMap` (the previous type)
245    /// every delta depended on hash-seed iteration order and so varied run to
246    /// run for the same measurements.
247    checkpoints: IndexMap<String, usize>,
248}
249
250impl Default for SystemMemoryProfiler {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256impl SystemMemoryProfiler {
257    pub fn new() -> Self {
258        Self {
259            baseline_memory: Self::current_memory_usage(),
260            peak_memory: None,
261            checkpoints: IndexMap::new(),
262        }
263    }
264
265    /// Record this process's current RSS under `name`.
266    ///
267    /// Silently skips the checkpoint when the platform did not return a
268    /// reading, rather than recording a fabricated zero.
269    pub fn checkpoint(&mut self, name: &str) {
270        let Some(current_memory) = Self::current_memory_usage() else {
271            tracing::debug!(
272                checkpoint = name,
273                "no process memory reading available; checkpoint not recorded"
274            );
275            return;
276        };
277        self.checkpoints.insert(name.to_string(), current_memory);
278
279        if self.peak_memory.is_none_or(|peak| current_memory > peak) {
280            self.peak_memory = Some(current_memory);
281        }
282    }
283
284    pub fn memory_report(&self) -> MemoryReport {
285        let current_memory = Self::current_memory_usage();
286        let memory_growth = match (current_memory, self.baseline_memory) {
287            (Some(current), Some(baseline)) => Some(current as i64 - baseline as i64),
288            _ => None,
289        };
290
291        // Deltas are chained through the checkpoints in insertion order, so the
292        // sequence is the real one the caller recorded.
293        let mut memory_deltas = IndexMap::new();
294        let mut prev_memory = self.baseline_memory;
295        for (name, memory) in &self.checkpoints {
296            if let Some(prev) = prev_memory {
297                memory_deltas.insert(name.clone(), *memory as i64 - prev as i64);
298            }
299            prev_memory = Some(*memory);
300        }
301
302        MemoryReport {
303            baseline_memory: self.baseline_memory,
304            current_memory,
305            peak_memory: self.peak_memory,
306            memory_growth,
307            checkpoints: self.checkpoints.clone(),
308            memory_deltas,
309        }
310    }
311
312    /// Resident set size of THIS process, in bytes, read from `sysinfo`.
313    ///
314    /// Returns `None` when the platform's process table does not list this
315    /// PID (which `sysinfo` supports on every tier-1 target, but not on every
316    /// sandbox). It used to return a hardcoded `0`, which made every field of
317    /// every [`MemoryReport`] a fabricated zero.
318    pub fn current_memory_usage() -> Option<usize> {
319        let pid = sysinfo::Pid::from_u32(std::process::id());
320        let mut system = sysinfo::System::new();
321        system.refresh_processes_specifics(
322            sysinfo::ProcessesToUpdate::Some(&[pid]),
323            true,
324            sysinfo::ProcessRefreshKind::nothing().with_memory(),
325        );
326        system.process(pid).map(|p| p.memory() as usize)
327    }
328}
329
330/// Memory profiling report
331#[derive(Debug, Serialize, Deserialize)]
332pub struct MemoryReport {
333    /// Process RSS in bytes when the profiler was created.
334    pub baseline_memory: Option<usize>,
335    /// Process RSS in bytes when the report was taken.
336    pub current_memory: Option<usize>,
337    /// Largest RSS seen at any checkpoint.
338    pub peak_memory: Option<usize>,
339    /// `current_memory - baseline_memory`, signed: memory can genuinely fall.
340    /// (It used to be a `usize` produced with `saturating_sub`, so every
341    /// release of memory was reported as zero growth.)
342    pub memory_growth: Option<i64>,
343    /// Real RSS reading recorded at each named checkpoint, in the order the
344    /// checkpoints were taken.
345    pub checkpoints: IndexMap<String, usize>,
346    /// Signed byte delta between consecutive checkpoints, in the same order.
347    pub memory_deltas: IndexMap<String, i64>,
348}
349
350/// Combined performance and memory profiler
351#[derive(Debug)]
352pub struct SystemProfiler {
353    performance_monitor: PerformanceMonitor,
354    memory_profiler: SystemMemoryProfiler,
355}
356
357impl Default for SystemProfiler {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363impl SystemProfiler {
364    pub fn new() -> Self {
365        Self {
366            performance_monitor: PerformanceMonitor::new(),
367            memory_profiler: SystemMemoryProfiler::new(),
368        }
369    }
370
371    pub fn checkpoint(&mut self, name: &str) {
372        self.performance_monitor.checkpoint(name);
373        self.memory_profiler.checkpoint(name);
374    }
375
376    pub fn end_checkpoint(&mut self, name: &str) -> Option<Duration> {
377        self.performance_monitor.end_checkpoint(name)
378    }
379
380    pub fn generate_system_report(&self) -> SystemReport {
381        let performance_metrics = self.performance_monitor.get_detailed_metrics();
382        let memory_report = self.memory_profiler.memory_report();
383        let bottleneck_analysis = self.performance_monitor.analyze_bottlenecks(0.8);
384
385        SystemReport {
386            performance_metrics,
387            memory_report,
388            bottleneck_analysis,
389            timestamp: chrono::Utc::now(),
390        }
391    }
392}
393
394/// Comprehensive system profiling report
395#[derive(Debug, Serialize, Deserialize)]
396pub struct SystemReport {
397    pub performance_metrics: PerformanceMetrics,
398    pub memory_report: MemoryReport,
399    pub bottleneck_analysis: BottleneckAnalysis,
400    pub timestamp: chrono::DateTime<chrono::Utc>,
401}
402
403#[cfg(test)]
404mod memory_profiler_tests {
405    use super::*;
406
407    #[test]
408    fn current_memory_usage_reports_a_real_non_zero_rss() {
409        // The old `get_current_memory_usage` returned a hardcoded 0.
410        let reading = SystemMemoryProfiler::current_memory_usage();
411        // On every platform this crate is tested on, `sysinfo` lists our own
412        // PID, so a `None` here is a real regression, not an acceptable
413        // absence.
414        #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
415        let bytes = reading.expect("sysinfo must list this process on a tier-1 target");
416        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
417        let Some(bytes) = reading
418        else {
419            return;
420        };
421        assert!(
422            bytes > 0,
423            "a live process must have non-zero RSS, got {bytes} (the old placeholder was 0)"
424        );
425        // A test binary is comfortably over 1 MiB resident; a fabricated
426        // constant would not scale with the real process.
427        assert!(bytes > 1024 * 1024, "implausibly small RSS: {bytes} bytes");
428    }
429
430    #[test]
431    fn checkpoints_and_deltas_follow_the_real_recording_order() {
432        let mut profiler = SystemMemoryProfiler::new();
433        if SystemMemoryProfiler::current_memory_usage().is_none() {
434            return; // no readings available on this platform; nothing to assert
435        }
436        profiler.checkpoint("first");
437        // Force a measurable allocation so the second reading is meaningful.
438        let ballast: Vec<u8> = vec![7u8; 8 * 1024 * 1024];
439        assert_eq!(ballast.len(), 8 * 1024 * 1024);
440        profiler.checkpoint("second");
441
442        let report = profiler.memory_report();
443        assert_eq!(
444            report.checkpoints.keys().collect::<Vec<_>>(),
445            vec!["first", "second"],
446            "checkpoints must keep recording order"
447        );
448        assert_eq!(
449            report.memory_deltas.keys().collect::<Vec<_>>(),
450            vec!["first", "second"],
451            "deltas must keep the same order as the checkpoints"
452        );
453        assert!(
454            report.peak_memory.is_some(),
455            "a real peak must have been recorded"
456        );
457        assert!(report.baseline_memory.is_some());
458        assert!(report.current_memory.is_some());
459    }
460
461    #[test]
462    fn memory_growth_is_signed_so_a_release_is_not_reported_as_zero() {
463        let profiler = SystemMemoryProfiler::new();
464        let report = profiler.memory_report();
465        if let Some(growth) = report.memory_growth {
466            // Signed type: this compiles and is meaningful only because the
467            // field is `i64` now. `usize` + `saturating_sub` reported every
468            // memory release as "zero growth".
469            let _: i64 = growth;
470        }
471    }
472}