trustformers_debug/utilities/
performance.rs1use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::{Duration, Instant};
7
8#[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 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 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#[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#[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#[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#[derive(Debug, Serialize, Deserialize)]
227pub enum BottleneckSeverity {
228 Low,
229 Medium,
230 High,
231 Critical,
232}
233
234#[derive(Debug)]
236pub struct SystemMemoryProfiler {
237 baseline_memory: Option<usize>,
239 peak_memory: Option<usize>,
242 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 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 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 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#[derive(Debug, Serialize, Deserialize)]
332pub struct MemoryReport {
333 pub baseline_memory: Option<usize>,
335 pub current_memory: Option<usize>,
337 pub peak_memory: Option<usize>,
339 pub memory_growth: Option<i64>,
343 pub checkpoints: IndexMap<String, usize>,
346 pub memory_deltas: IndexMap<String, i64>,
348}
349
350#[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#[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 let reading = SystemMemoryProfiler::current_memory_usage();
411 #[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 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; }
436 profiler.checkpoint("first");
437 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 let _: i64 = growth;
470 }
471 }
472}