Skip to main content

oxigdal_dev_tools/
profiler.rs

1//! Performance profiling utilities
2//!
3//! This module provides tools for profiling OxiGDAL operations including
4//! timing, memory usage, and resource consumption tracking.
5
6use crate::{DevToolsError, Result};
7use chrono::{DateTime, Utc};
8use colored::Colorize;
9use comfy_table::{Cell, CellAlignment, Row, Table};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
13
14/// Performance profiler
15pub struct Profiler {
16    /// Profiler name
17    name: String,
18    /// Profiling sessions
19    sessions: Vec<ProfileSession>,
20    /// Current session
21    current_session: Option<ProfileSession>,
22    /// System information
23    system: System,
24    /// Process ID
25    pid: Pid,
26}
27
28/// Profile session
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ProfileSession {
31    /// Session name
32    pub name: String,
33    /// Start time
34    pub start_time: DateTime<Utc>,
35    /// End time
36    pub end_time: Option<DateTime<Utc>>,
37    /// Duration in milliseconds
38    pub duration_ms: Option<i64>,
39    /// Memory at start (bytes)
40    pub memory_start: u64,
41    /// Memory at end (bytes)
42    pub memory_end: Option<u64>,
43    /// Memory delta (bytes)
44    pub memory_delta: Option<i64>,
45    /// CPU usage percentage
46    pub cpu_usage: Option<f32>,
47    /// Custom metrics
48    pub metrics: HashMap<String, f64>,
49}
50
51impl Profiler {
52    /// Create a new profiler
53    pub fn new(name: impl Into<String>) -> Self {
54        let mut system = System::new();
55        system.refresh_all();
56        let pid = Pid::from_u32(std::process::id());
57
58        Self {
59            name: name.into(),
60            sessions: Vec::new(),
61            current_session: None,
62            system,
63            pid,
64        }
65    }
66
67    /// Start profiling
68    pub fn start(&mut self) {
69        self.system.refresh_processes_specifics(
70            ProcessesToUpdate::Some(&[self.pid]),
71            true,
72            ProcessRefreshKind::everything(),
73        );
74
75        let memory_start = self
76            .system
77            .process(self.pid)
78            .map(|p| p.memory())
79            .unwrap_or(0);
80
81        self.current_session = Some(ProfileSession {
82            name: self.name.clone(),
83            start_time: Utc::now(),
84            end_time: None,
85            duration_ms: None,
86            memory_start,
87            memory_end: None,
88            memory_delta: None,
89            cpu_usage: None,
90            metrics: HashMap::new(),
91        });
92    }
93
94    /// Stop profiling
95    pub fn stop(&mut self) {
96        if let Some(mut session) = self.current_session.take() {
97            self.system.refresh_processes_specifics(
98                ProcessesToUpdate::Some(&[self.pid]),
99                true,
100                ProcessRefreshKind::everything(),
101            );
102
103            let end_time = Utc::now();
104            let duration = end_time.signed_duration_since(session.start_time);
105
106            let memory_end = self
107                .system
108                .process(self.pid)
109                .map(|p| p.memory())
110                .unwrap_or(0);
111
112            let cpu_usage = self
113                .system
114                .process(self.pid)
115                .map(|p| p.cpu_usage())
116                .unwrap_or(0.0);
117
118            session.end_time = Some(end_time);
119            session.duration_ms = Some(duration.num_milliseconds());
120            session.memory_end = Some(memory_end);
121            session.memory_delta = Some(memory_end as i64 - session.memory_start as i64);
122            session.cpu_usage = Some(cpu_usage);
123
124            self.sessions.push(session);
125        }
126    }
127
128    /// Add a custom metric to current session
129    pub fn add_metric(&mut self, name: impl Into<String>, value: f64) -> Result<()> {
130        if let Some(session) = &mut self.current_session {
131            session.metrics.insert(name.into(), value);
132            Ok(())
133        } else {
134            Err(DevToolsError::Profiler("No active session".to_string()))
135        }
136    }
137
138    /// Get all sessions
139    pub fn sessions(&self) -> &[ProfileSession] {
140        &self.sessions
141    }
142
143    /// Get current session
144    pub fn current_session(&self) -> Option<&ProfileSession> {
145        self.current_session.as_ref()
146    }
147
148    /// Generate report
149    pub fn report(&self) -> String {
150        let mut report = String::new();
151        report.push_str(&format!(
152            "\n{}\n",
153            format!("Profile Report: {}", self.name).bold()
154        ));
155        report.push_str(&format!("{}\n\n", "=".repeat(60)));
156
157        if self.sessions.is_empty() {
158            report.push_str("No profiling sessions recorded\n");
159            return report;
160        }
161
162        let mut table = Table::new();
163        table.set_header(Row::from(vec![
164            Cell::new("Session").set_alignment(CellAlignment::Center),
165            Cell::new("Duration (ms)").set_alignment(CellAlignment::Center),
166            Cell::new("Memory Δ").set_alignment(CellAlignment::Center),
167            Cell::new("CPU %").set_alignment(CellAlignment::Center),
168        ]));
169
170        for (i, session) in self.sessions.iter().enumerate() {
171            let duration = session
172                .duration_ms
173                .map(|d| format!("{}", d))
174                .unwrap_or_else(|| "N/A".to_string());
175
176            let memory_delta = session
177                .memory_delta
178                .map(format_bytes)
179                .unwrap_or_else(|| "N/A".to_string());
180
181            let cpu = session
182                .cpu_usage
183                .map(|c| format!("{:.1}", c))
184                .unwrap_or_else(|| "N/A".to_string());
185
186            table.add_row(Row::from(vec![
187                Cell::new(format!("#{}", i + 1)),
188                Cell::new(duration).set_alignment(CellAlignment::Right),
189                Cell::new(memory_delta).set_alignment(CellAlignment::Right),
190                Cell::new(cpu).set_alignment(CellAlignment::Right),
191            ]));
192        }
193
194        report.push_str(&table.to_string());
195        report.push('\n');
196
197        // Statistics
198        let total_duration: i64 = self.sessions.iter().filter_map(|s| s.duration_ms).sum();
199        let avg_duration = if !self.sessions.is_empty() {
200            total_duration / self.sessions.len() as i64
201        } else {
202            0
203        };
204
205        report.push_str(&format!("\n{}\n", "Statistics:".bold()));
206        report.push_str(&format!("  Total sessions: {}\n", self.sessions.len()));
207        report.push_str(&format!("  Total duration: {} ms\n", total_duration));
208        report.push_str(&format!("  Average duration: {} ms\n", avg_duration));
209
210        report
211    }
212
213    /// Export sessions as JSON
214    pub fn export_json(&self) -> Result<String> {
215        Ok(serde_json::to_string_pretty(&self.sessions)?)
216    }
217
218    /// Clear all sessions
219    pub fn clear(&mut self) {
220        self.sessions.clear();
221        self.current_session = None;
222    }
223}
224
225/// Format bytes with appropriate unit
226fn format_bytes(bytes: i64) -> String {
227    let abs_bytes = bytes.abs() as f64;
228    let sign = if bytes < 0 { "-" } else { "+" };
229
230    if abs_bytes < 1024.0 {
231        format!("{}{} B", sign, bytes.abs())
232    } else if abs_bytes < 1024.0 * 1024.0 {
233        format!("{}{:.2} KB", sign, abs_bytes / 1024.0)
234    } else if abs_bytes < 1024.0 * 1024.0 * 1024.0 {
235        format!("{}{:.2} MB", sign, abs_bytes / (1024.0 * 1024.0))
236    } else {
237        format!("{}{:.2} GB", sign, abs_bytes / (1024.0 * 1024.0 * 1024.0))
238    }
239}
240
241/// Memory profiler for tracking allocations
242pub struct MemoryProfiler {
243    /// Snapshots
244    snapshots: Vec<MemorySnapshot>,
245    /// System
246    system: System,
247    /// Process ID
248    pid: Pid,
249}
250
251/// Memory snapshot
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct MemorySnapshot {
254    /// Snapshot name
255    pub name: String,
256    /// Timestamp
257    pub timestamp: DateTime<Utc>,
258    /// Total memory (bytes)
259    pub total_memory: u64,
260    /// Virtual memory (bytes)
261    pub virtual_memory: u64,
262}
263
264impl MemoryProfiler {
265    /// Create a new memory profiler
266    pub fn new() -> Self {
267        let mut system = System::new();
268        system.refresh_all();
269        let pid = Pid::from_u32(std::process::id());
270
271        Self {
272            snapshots: Vec::new(),
273            system,
274            pid,
275        }
276    }
277
278    /// Take a snapshot
279    pub fn snapshot(&mut self, name: impl Into<String>) {
280        self.system.refresh_processes_specifics(
281            ProcessesToUpdate::Some(&[self.pid]),
282            true,
283            ProcessRefreshKind::everything(),
284        );
285
286        if let Some(process) = self.system.process(self.pid) {
287            self.snapshots.push(MemorySnapshot {
288                name: name.into(),
289                timestamp: Utc::now(),
290                total_memory: process.memory(),
291                virtual_memory: process.virtual_memory(),
292            });
293        }
294    }
295
296    /// Get all snapshots
297    pub fn snapshots(&self) -> &[MemorySnapshot] {
298        &self.snapshots
299    }
300
301    /// Generate report
302    pub fn report(&self) -> String {
303        let mut report = String::new();
304        report.push_str(&format!("\n{}\n", "Memory Profile Report".bold()));
305        report.push_str(&format!("{}\n\n", "=".repeat(60)));
306
307        if self.snapshots.is_empty() {
308            report.push_str("No snapshots recorded\n");
309            return report;
310        }
311
312        let mut table = Table::new();
313        table.set_header(Row::from(vec![
314            Cell::new("Snapshot").set_alignment(CellAlignment::Center),
315            Cell::new("Total Memory").set_alignment(CellAlignment::Center),
316            Cell::new("Virtual Memory").set_alignment(CellAlignment::Center),
317            Cell::new("Delta").set_alignment(CellAlignment::Center),
318        ]));
319
320        for (i, snapshot) in self.snapshots.iter().enumerate() {
321            let delta = if i > 0 {
322                let prev = &self.snapshots[i - 1];
323                let delta = snapshot.total_memory as i64 - prev.total_memory as i64;
324                format_bytes(delta)
325            } else {
326                "N/A".to_string()
327            };
328
329            table.add_row(Row::from(vec![
330                Cell::new(&snapshot.name),
331                Cell::new(format_bytes(snapshot.total_memory as i64))
332                    .set_alignment(CellAlignment::Right),
333                Cell::new(format_bytes(snapshot.virtual_memory as i64))
334                    .set_alignment(CellAlignment::Right),
335                Cell::new(delta).set_alignment(CellAlignment::Right),
336            ]));
337        }
338
339        report.push_str(&table.to_string());
340        report.push('\n');
341
342        report
343    }
344
345    /// Clear snapshots
346    pub fn clear(&mut self) {
347        self.snapshots.clear();
348    }
349}
350
351impl Default for MemoryProfiler {
352    fn default() -> Self {
353        Self::new()
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use std::thread;
361    use std::time::Duration as StdDuration;
362
363    #[test]
364    fn test_profiler_creation() {
365        let profiler = Profiler::new("test");
366        assert!(profiler.sessions().is_empty());
367    }
368
369    #[test]
370    fn test_profiler_session() {
371        let mut profiler = Profiler::new("test");
372        profiler.start();
373        thread::sleep(StdDuration::from_millis(100));
374        profiler.stop();
375
376        assert_eq!(profiler.sessions().len(), 1);
377        let session = &profiler.sessions()[0];
378        assert!(session.duration_ms.is_some());
379    }
380
381    #[test]
382    fn test_profiler_metrics() -> Result<()> {
383        let mut profiler = Profiler::new("test");
384        profiler.start();
385        profiler.add_metric("test_metric", 42.0)?;
386        profiler.stop();
387
388        let session = &profiler.sessions()[0];
389        assert_eq!(session.metrics.get("test_metric"), Some(&42.0));
390        Ok(())
391    }
392
393    #[test]
394    fn test_format_bytes() {
395        assert_eq!(format_bytes(512), "+512 B");
396        assert_eq!(format_bytes(2048), "+2.00 KB");
397        assert_eq!(format_bytes(-1024), "-1.00 KB");
398    }
399
400    #[test]
401    fn test_memory_profiler() {
402        let mut profiler = MemoryProfiler::new();
403        profiler.snapshot("start");
404        profiler.snapshot("end");
405
406        assert_eq!(profiler.snapshots().len(), 2);
407    }
408
409    #[test]
410    fn test_profiler_report() {
411        let mut profiler = Profiler::new("test");
412        profiler.start();
413        thread::sleep(StdDuration::from_millis(50));
414        profiler.stop();
415
416        let report = profiler.report();
417        assert!(report.contains("Profile Report"));
418    }
419
420    #[test]
421    fn test_export_json() -> Result<()> {
422        let mut profiler = Profiler::new("test");
423        profiler.start();
424        profiler.stop();
425
426        let json = profiler.export_json()?;
427        assert!(json.contains("name"));
428        assert!(json.contains("test"));
429        Ok(())
430    }
431}