1use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, VecDeque};
9use std::fmt;
10use std::sync::Arc;
11use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12use tokio::sync::RwLock;
13
14#[derive(Debug, Clone)]
16pub struct ProfilingConfig {
17 pub enable_cpu_profiling: bool,
19 pub enable_memory_profiling: bool,
21 pub sampling_frequency: u32,
23 pub max_profile_duration: Duration,
25 pub retention_period: Duration,
27 pub enable_flamegraph: bool,
29}
30
31impl Default for ProfilingConfig {
32 fn default() -> Self {
33 Self {
34 enable_cpu_profiling: true,
35 enable_memory_profiling: true,
36 sampling_frequency: 100, max_profile_duration: Duration::from_secs(60),
38 retention_period: Duration::from_secs(3600), enable_flamegraph: true,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
46pub struct StackFrame {
47 pub function: String,
49 pub file: Option<String>,
51 pub line: Option<u32>,
53 pub module: Option<String>,
55}
56
57impl StackFrame {
58 pub fn new(function: String) -> Self {
60 Self {
61 function,
62 file: None,
63 line: None,
64 module: None,
65 }
66 }
67
68 pub fn with_file(mut self, file: String) -> Self {
70 self.file = Some(file);
71 self
72 }
73
74 pub fn with_line(mut self, line: u32) -> Self {
76 self.line = Some(line);
77 self
78 }
79
80 pub fn with_module(mut self, module: String) -> Self {
82 self.module = Some(module);
83 self
84 }
85
86 pub fn display_name(&self) -> String {
88 if let (Some(file), Some(line)) = (&self.file, self.line) {
89 format!("{} ({}:{})", self.function, file, line)
90 } else {
91 self.function.clone()
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
98pub struct StackTrace {
99 pub frames: Vec<StackFrame>,
101}
102
103impl StackTrace {
104 pub fn new() -> Self {
106 Self { frames: Vec::new() }
107 }
108
109 pub fn push_frame(mut self, frame: StackFrame) -> Self {
111 self.frames.push(frame);
112 self
113 }
114
115 pub fn depth(&self) -> usize {
117 self.frames.len()
118 }
119}
120
121impl Default for StackTrace {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl fmt::Display for StackTrace {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 let stack_str = self
130 .frames
131 .iter()
132 .map(|frame| frame.display_name())
133 .collect::<Vec<_>>()
134 .join(";");
135 write!(f, "{}", stack_str)
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ProfileSample {
142 pub stack_trace: StackTrace,
144 pub count: u64,
146 pub cpu_time_ns: u64,
148 pub memory_bytes: u64,
150 pub first_seen: u64,
152 pub last_seen: u64,
154}
155
156impl ProfileSample {
157 pub fn new(stack_trace: StackTrace) -> Self {
159 let now = SystemTime::now()
160 .duration_since(UNIX_EPOCH)
161 .expect("SystemTime should be after UNIX_EPOCH")
162 .as_secs();
163
164 Self {
165 stack_trace,
166 count: 1,
167 cpu_time_ns: 0,
168 memory_bytes: 0,
169 first_seen: now,
170 last_seen: now,
171 }
172 }
173
174 pub fn merge(&mut self, other: &ProfileSample) {
176 self.count += other.count;
177 self.cpu_time_ns += other.cpu_time_ns;
178 self.memory_bytes += other.memory_bytes;
179 self.last_seen = self.last_seen.max(other.last_seen);
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ProfileData {
186 pub id: String,
188 pub profile_type: ProfileType,
190 pub start_time: u64,
192 pub end_time: u64,
194 pub samples: Vec<ProfileSample>,
196 pub total_samples: u64,
198 pub metadata: HashMap<String, String>,
200}
201
202impl ProfileData {
203 pub fn new(id: String, profile_type: ProfileType) -> Self {
205 let now = SystemTime::now()
206 .duration_since(UNIX_EPOCH)
207 .expect("SystemTime should be after UNIX_EPOCH")
208 .as_secs();
209
210 Self {
211 id,
212 profile_type,
213 start_time: now,
214 end_time: now,
215 samples: Vec::new(),
216 total_samples: 0,
217 metadata: HashMap::new(),
218 }
219 }
220
221 pub fn add_sample(&mut self, sample: ProfileSample) {
223 self.total_samples += sample.count;
224 self.samples.push(sample);
225 }
226
227 pub fn finish(&mut self) {
229 self.end_time = SystemTime::now()
230 .duration_since(UNIX_EPOCH)
231 .expect("SystemTime should be after UNIX_EPOCH")
232 .as_secs();
233 }
234
235 pub fn duration_secs(&self) -> u64 {
237 self.end_time - self.start_time
238 }
239
240 pub fn to_pprof(&self) -> String {
242 let mut output = String::new();
243
244 output.push_str(&format!("--- profile: {}\n", self.profile_type.as_str()));
246 output.push_str(&format!("duration: {} seconds\n", self.duration_secs()));
247 output.push_str(&format!("samples: {}\n", self.total_samples));
248 output.push_str("---\n");
249
250 for sample in &self.samples {
252 output.push_str(&format!(
253 "{} {} # {}\n",
254 sample.count, sample.cpu_time_ns, sample.stack_trace
255 ));
256 }
257
258 output
259 }
260
261 pub fn to_flamegraph(&self) -> String {
263 let mut output = String::new();
264
265 for sample in &self.samples {
266 output.push_str(&format!("{} {}\n", sample.stack_trace, sample.count));
267 }
268
269 output
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
275pub enum ProfileType {
276 Cpu,
278 Memory,
280 Wall,
282}
283
284impl ProfileType {
285 pub fn as_str(&self) -> &'static str {
287 match self {
288 ProfileType::Cpu => "cpu",
289 ProfileType::Memory => "memory",
290 ProfileType::Wall => "wall",
291 }
292 }
293}
294
295pub struct Profiler {
297 config: ProfilingConfig,
299 active_profiles: Arc<RwLock<HashMap<String, ProfileData>>>,
301 completed_profiles: Arc<RwLock<VecDeque<ProfileData>>>,
303 start_time: Instant,
305}
306
307impl Profiler {
308 pub fn new(config: ProfilingConfig) -> Self {
310 Self {
311 config,
312 active_profiles: Arc::new(RwLock::new(HashMap::new())),
313 completed_profiles: Arc::new(RwLock::new(VecDeque::new())),
314 start_time: Instant::now(),
315 }
316 }
317
318 pub async fn start_profile(&self, id: String, profile_type: ProfileType) -> Result<()> {
320 let mut profiles = self.active_profiles.write().await;
321
322 if profiles.contains_key(&id) {
323 anyhow::bail!("Profile with ID {} already exists", id);
324 }
325
326 let profile = ProfileData::new(id.clone(), profile_type);
327 profiles.insert(id, profile);
328
329 Ok(())
330 }
331
332 pub async fn record_sample(&self, profile_id: &str, sample: ProfileSample) -> Result<()> {
334 let mut profiles = self.active_profiles.write().await;
335
336 let profile = profiles.get_mut(profile_id).context("Profile not found")?;
337
338 profile.add_sample(sample);
339
340 Ok(())
341 }
342
343 pub async fn stop_profile(&self, id: &str) -> Result<ProfileData> {
345 let mut active = self.active_profiles.write().await;
346
347 let mut profile = active.remove(id).context("Profile not found")?;
348
349 profile.finish();
350
351 let mut completed = self.completed_profiles.write().await;
353 completed.push_back(profile.clone());
354
355 self.cleanup_old_profiles(&mut completed).await;
357
358 Ok(profile)
359 }
360
361 pub async fn get_active_profile(&self, id: &str) -> Option<ProfileData> {
363 let profiles = self.active_profiles.read().await;
364 profiles.get(id).cloned()
365 }
366
367 pub async fn list_active_profiles(&self) -> Vec<String> {
369 let profiles = self.active_profiles.read().await;
370 profiles.keys().cloned().collect()
371 }
372
373 pub async fn get_completed_profile(&self, id: &str) -> Option<ProfileData> {
375 let profiles = self.completed_profiles.read().await;
376 profiles.iter().find(|p| p.id == id).cloned()
377 }
378
379 pub async fn list_completed_profiles(&self) -> Vec<ProfileData> {
381 let profiles = self.completed_profiles.read().await;
382 profiles.iter().cloned().collect()
383 }
384
385 async fn cleanup_old_profiles(&self, completed: &mut VecDeque<ProfileData>) {
387 let retention_secs = self.config.retention_period.as_secs();
388 let now = SystemTime::now()
389 .duration_since(UNIX_EPOCH)
390 .expect("SystemTime should be after UNIX_EPOCH")
391 .as_secs();
392
393 while let Some(front) = completed.front() {
394 if now - front.end_time > retention_secs {
395 completed.pop_front();
396 } else {
397 break;
398 }
399 }
400
401 while completed.len() > 100 {
403 completed.pop_front();
404 }
405 }
406
407 pub async fn export_pprof(&self, profile_id: &str) -> Result<String> {
409 if let Some(profile) = self.get_active_profile(profile_id).await {
411 return Ok(profile.to_pprof());
412 }
413
414 if let Some(profile) = self.get_completed_profile(profile_id).await {
416 return Ok(profile.to_pprof());
417 }
418
419 anyhow::bail!("Profile not found")
420 }
421
422 pub async fn export_flamegraph(&self, profile_id: &str) -> Result<String> {
424 if let Some(profile) = self.get_active_profile(profile_id).await {
426 return Ok(profile.to_flamegraph());
427 }
428
429 if let Some(profile) = self.get_completed_profile(profile_id).await {
431 return Ok(profile.to_flamegraph());
432 }
433
434 anyhow::bail!("Profile not found")
435 }
436
437 pub async fn get_statistics(&self) -> ProfilerStatistics {
439 let active = self.active_profiles.read().await;
440 let completed = self.completed_profiles.read().await;
441
442 let mut total_samples = 0;
443 for profile in completed.iter() {
444 total_samples += profile.total_samples;
445 }
446
447 ProfilerStatistics {
448 active_profiles: active.len(),
449 completed_profiles: completed.len(),
450 total_samples,
451 uptime_secs: self.start_time.elapsed().as_secs(),
452 }
453 }
454}
455
456impl Default for Profiler {
457 fn default() -> Self {
458 Self::new(ProfilingConfig::default())
459 }
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct ProfilerStatistics {
465 pub active_profiles: usize,
467 pub completed_profiles: usize,
469 pub total_samples: u64,
471 pub uptime_secs: u64,
473}
474
475pub struct StackTraceBuilder {
477 frames: Vec<StackFrame>,
478}
479
480impl StackTraceBuilder {
481 pub fn new() -> Self {
483 Self { frames: Vec::new() }
484 }
485
486 pub fn push(mut self, frame: StackFrame) -> Self {
488 self.frames.push(frame);
489 self
490 }
491
492 pub fn push_function(mut self, function: String) -> Self {
494 self.frames.push(StackFrame::new(function));
495 self
496 }
497
498 pub fn build(self) -> StackTrace {
500 StackTrace {
501 frames: self.frames,
502 }
503 }
504}
505
506impl Default for StackTraceBuilder {
507 fn default() -> Self {
508 Self::new()
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn test_stack_frame_creation() {
518 let frame = StackFrame::new("test_function".to_string())
519 .with_file("test.rs".to_string())
520 .with_line(42)
521 .with_module("test_module".to_string());
522
523 assert_eq!(frame.function, "test_function");
524 assert_eq!(frame.file, Some("test.rs".to_string()));
525 assert_eq!(frame.line, Some(42));
526 assert_eq!(frame.module, Some("test_module".to_string()));
527 assert_eq!(frame.display_name(), "test_function (test.rs:42)");
528 }
529
530 #[test]
531 fn test_stack_frame_display_name() {
532 let frame1 = StackFrame::new("func1".to_string());
533 assert_eq!(frame1.display_name(), "func1");
534
535 let frame2 = StackFrame::new("func2".to_string())
536 .with_file("test.rs".to_string())
537 .with_line(10);
538 assert_eq!(frame2.display_name(), "func2 (test.rs:10)");
539 }
540
541 #[test]
542 fn test_stack_trace() {
543 let trace = StackTrace::new()
544 .push_frame(StackFrame::new("main".to_string()))
545 .push_frame(StackFrame::new("process".to_string()))
546 .push_frame(StackFrame::new("execute".to_string()));
547
548 assert_eq!(trace.depth(), 3);
549 assert_eq!(trace.to_string(), "main;process;execute");
550 assert_eq!(format!("{}", trace), "main;process;execute");
551 }
552
553 #[test]
554 fn test_stack_trace_builder() {
555 let trace = StackTraceBuilder::new()
556 .push_function("main".to_string())
557 .push_function("process".to_string())
558 .push(
559 StackFrame::new("execute".to_string())
560 .with_file("app.rs".to_string())
561 .with_line(100),
562 )
563 .build();
564
565 assert_eq!(trace.depth(), 3);
566 let trace_str = trace.to_string();
567 assert!(trace_str.contains("execute (app.rs:100)"));
568 }
569
570 #[test]
571 fn test_profile_sample() {
572 let trace = StackTrace::new().push_frame(StackFrame::new("test".to_string()));
573
574 let mut sample1 = ProfileSample::new(trace.clone());
575 sample1.cpu_time_ns = 1000;
576 sample1.memory_bytes = 2000;
577
578 let mut sample2 = ProfileSample::new(trace.clone());
579 sample2.cpu_time_ns = 500;
580 sample2.memory_bytes = 1000;
581
582 sample1.merge(&sample2);
583
584 assert_eq!(sample1.count, 2);
585 assert_eq!(sample1.cpu_time_ns, 1500);
586 assert_eq!(sample1.memory_bytes, 3000);
587 }
588
589 #[test]
590 fn test_profile_data() {
591 let mut profile = ProfileData::new("test-profile".to_string(), ProfileType::Cpu);
592
593 let trace = StackTrace::new().push_frame(StackFrame::new("test_fn".to_string()));
594 let sample = ProfileSample::new(trace);
595
596 profile.add_sample(sample);
597 assert_eq!(profile.total_samples, 1);
598
599 profile.finish();
600 let _duration = profile.duration_secs();
602 }
603
604 #[test]
605 fn test_profile_type() {
606 assert_eq!(ProfileType::Cpu.as_str(), "cpu");
607 assert_eq!(ProfileType::Memory.as_str(), "memory");
608 assert_eq!(ProfileType::Wall.as_str(), "wall");
609 }
610
611 #[test]
612 fn test_pprof_export() {
613 let mut profile = ProfileData::new("test".to_string(), ProfileType::Cpu);
614
615 let trace = StackTrace::new()
616 .push_frame(StackFrame::new("main".to_string()))
617 .push_frame(StackFrame::new("process".to_string()));
618
619 let mut sample = ProfileSample::new(trace);
620 sample.count = 100;
621 sample.cpu_time_ns = 5000000;
622
623 profile.add_sample(sample);
624
625 let pprof = profile.to_pprof();
626 assert!(pprof.contains("profile: cpu"));
627 assert!(pprof.contains("samples: 100"));
628 assert!(pprof.contains("main;process"));
629 }
630
631 #[test]
632 fn test_flamegraph_export() {
633 let mut profile = ProfileData::new("test".to_string(), ProfileType::Cpu);
634
635 let trace = StackTrace::new()
636 .push_frame(StackFrame::new("main".to_string()))
637 .push_frame(StackFrame::new("execute".to_string()));
638
639 let mut sample = ProfileSample::new(trace);
640 sample.count = 50;
641
642 profile.add_sample(sample);
643
644 let flamegraph = profile.to_flamegraph();
645 assert!(flamegraph.contains("main;execute 50"));
646 }
647
648 #[tokio::test]
649 async fn test_profiler_start_stop() {
650 let profiler = Profiler::default();
651
652 profiler
653 .start_profile("test-1".to_string(), ProfileType::Cpu)
654 .await
655 .expect("should succeed");
656
657 assert!(profiler.get_active_profile("test-1").await.is_some());
658
659 let profile = profiler
660 .stop_profile("test-1")
661 .await
662 .expect("should succeed");
663 assert_eq!(profile.id, "test-1");
664
665 assert!(profiler.get_active_profile("test-1").await.is_none());
666 assert!(profiler.get_completed_profile("test-1").await.is_some());
667 }
668
669 #[tokio::test]
670 async fn test_profiler_duplicate_id() {
671 let profiler = Profiler::default();
672
673 profiler
674 .start_profile("test".to_string(), ProfileType::Cpu)
675 .await
676 .expect("should succeed");
677
678 let result = profiler
679 .start_profile("test".to_string(), ProfileType::Cpu)
680 .await;
681
682 assert!(result.is_err());
683 }
684
685 #[tokio::test]
686 async fn test_profiler_record_sample() {
687 let profiler = Profiler::default();
688
689 profiler
690 .start_profile("test".to_string(), ProfileType::Cpu)
691 .await
692 .expect("should succeed");
693
694 let trace = StackTrace::new().push_frame(StackFrame::new("test_fn".to_string()));
695 let sample = ProfileSample::new(trace);
696
697 profiler
698 .record_sample("test", sample)
699 .await
700 .expect("should succeed");
701
702 let profile = profiler
703 .get_active_profile("test")
704 .await
705 .expect("should succeed");
706 assert_eq!(profile.total_samples, 1);
707 }
708
709 #[tokio::test]
710 async fn test_profiler_list_profiles() {
711 let profiler = Profiler::default();
712
713 profiler
714 .start_profile("test-1".to_string(), ProfileType::Cpu)
715 .await
716 .expect("should succeed");
717 profiler
718 .start_profile("test-2".to_string(), ProfileType::Memory)
719 .await
720 .expect("should succeed");
721
722 let active = profiler.list_active_profiles().await;
723 assert_eq!(active.len(), 2);
724 assert!(active.contains(&"test-1".to_string()));
725 assert!(active.contains(&"test-2".to_string()));
726 }
727
728 #[tokio::test]
729 async fn test_profiler_export_pprof() {
730 let profiler = Profiler::default();
731
732 profiler
733 .start_profile("test".to_string(), ProfileType::Cpu)
734 .await
735 .expect("should succeed");
736
737 let trace = StackTrace::new().push_frame(StackFrame::new("test_fn".to_string()));
738 let sample = ProfileSample::new(trace);
739 profiler
740 .record_sample("test", sample)
741 .await
742 .expect("should succeed");
743
744 let pprof = profiler.export_pprof("test").await.expect("should succeed");
745 assert!(pprof.contains("profile: cpu"));
746 }
747
748 #[tokio::test]
749 async fn test_profiler_export_flamegraph() {
750 let profiler = Profiler::default();
751
752 profiler
753 .start_profile("test".to_string(), ProfileType::Cpu)
754 .await
755 .expect("should succeed");
756
757 let trace = StackTrace::new().push_frame(StackFrame::new("test_fn".to_string()));
758 let sample = ProfileSample::new(trace);
759 profiler
760 .record_sample("test", sample)
761 .await
762 .expect("should succeed");
763
764 let flamegraph = profiler
765 .export_flamegraph("test")
766 .await
767 .expect("should succeed");
768 assert!(flamegraph.contains("test_fn"));
769 }
770
771 #[tokio::test]
772 async fn test_profiler_statistics() {
773 let profiler = Profiler::default();
774
775 profiler
776 .start_profile("test-1".to_string(), ProfileType::Cpu)
777 .await
778 .expect("should succeed");
779 profiler
780 .start_profile("test-2".to_string(), ProfileType::Memory)
781 .await
782 .expect("should succeed");
783
784 let stats = profiler.get_statistics().await;
785 assert_eq!(stats.active_profiles, 2);
786 assert_eq!(stats.completed_profiles, 0);
787
788 profiler
789 .stop_profile("test-1")
790 .await
791 .expect("should succeed");
792
793 let stats = profiler.get_statistics().await;
794 assert_eq!(stats.active_profiles, 1);
795 assert_eq!(stats.completed_profiles, 1);
796 }
797
798 #[tokio::test]
799 async fn test_profiler_not_found() {
800 let profiler = Profiler::default();
801
802 let result = profiler.stop_profile("nonexistent").await;
803 assert!(result.is_err());
804
805 let result = profiler.export_pprof("nonexistent").await;
806 assert!(result.is_err());
807 }
808
809 #[tokio::test]
810 async fn test_profiling_config() {
811 let config = ProfilingConfig::default();
812
813 assert!(config.enable_cpu_profiling);
814 assert!(config.enable_memory_profiling);
815 assert_eq!(config.sampling_frequency, 100);
816 assert!(config.enable_flamegraph);
817 }
818
819 #[test]
820 fn test_profile_data_metadata() {
821 let mut profile = ProfileData::new("test".to_string(), ProfileType::Cpu);
822
823 profile
824 .metadata
825 .insert("version".to_string(), "1.0".to_string());
826 profile
827 .metadata
828 .insert("env".to_string(), "production".to_string());
829
830 assert_eq!(profile.metadata.get("version"), Some(&"1.0".to_string()));
831 assert_eq!(profile.metadata.get("env"), Some(&"production".to_string()));
832 }
833
834 #[tokio::test]
835 async fn test_completed_profiles_list() {
836 let profiler = Profiler::default();
837
838 profiler
839 .start_profile("test-1".to_string(), ProfileType::Cpu)
840 .await
841 .expect("should succeed");
842 profiler
843 .start_profile("test-2".to_string(), ProfileType::Memory)
844 .await
845 .expect("should succeed");
846
847 profiler
848 .stop_profile("test-1")
849 .await
850 .expect("should succeed");
851 profiler
852 .stop_profile("test-2")
853 .await
854 .expect("should succeed");
855
856 let completed = profiler.list_completed_profiles().await;
857 assert_eq!(completed.len(), 2);
858 }
859}