1use 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
14pub struct Profiler {
16 name: String,
18 sessions: Vec<ProfileSession>,
20 current_session: Option<ProfileSession>,
22 system: System,
24 pid: Pid,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ProfileSession {
31 pub name: String,
33 pub start_time: DateTime<Utc>,
35 pub end_time: Option<DateTime<Utc>>,
37 pub duration_ms: Option<i64>,
39 pub memory_start: u64,
41 pub memory_end: Option<u64>,
43 pub memory_delta: Option<i64>,
45 pub cpu_usage: Option<f32>,
47 pub metrics: HashMap<String, f64>,
49}
50
51impl Profiler {
52 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 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 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 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 pub fn sessions(&self) -> &[ProfileSession] {
140 &self.sessions
141 }
142
143 pub fn current_session(&self) -> Option<&ProfileSession> {
145 self.current_session.as_ref()
146 }
147
148 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 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 pub fn export_json(&self) -> Result<String> {
215 Ok(serde_json::to_string_pretty(&self.sessions)?)
216 }
217
218 pub fn clear(&mut self) {
220 self.sessions.clear();
221 self.current_session = None;
222 }
223}
224
225fn 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
241pub struct MemoryProfiler {
243 snapshots: Vec<MemorySnapshot>,
245 system: System,
247 pid: Pid,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct MemorySnapshot {
254 pub name: String,
256 pub timestamp: DateTime<Utc>,
258 pub total_memory: u64,
260 pub virtual_memory: u64,
262}
263
264impl MemoryProfiler {
265 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 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 pub fn snapshots(&self) -> &[MemorySnapshot] {
298 &self.snapshots
299 }
300
301 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 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}