Skip to main content

oxirs_gql/observability/
profiling.rs

1//! Continuous Profiling Integration
2//!
3//! Provides continuous profiling capabilities with pprof and flamegraph support
4//! for identifying performance bottlenecks in GraphQL operations.
5
6use 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/// Profiling configuration
15#[derive(Debug, Clone)]
16pub struct ProfilingConfig {
17    /// Enable CPU profiling
18    pub enable_cpu_profiling: bool,
19    /// Enable memory profiling
20    pub enable_memory_profiling: bool,
21    /// Sampling frequency (Hz)
22    pub sampling_frequency: u32,
23    /// Maximum profile duration
24    pub max_profile_duration: Duration,
25    /// Profile retention period
26    pub retention_period: Duration,
27    /// Enable flamegraph generation
28    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, // 100 Hz
37            max_profile_duration: Duration::from_secs(60),
38            retention_period: Duration::from_secs(3600), // 1 hour
39            enable_flamegraph: true,
40        }
41    }
42}
43
44/// Stack frame information
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
46pub struct StackFrame {
47    /// Function name
48    pub function: String,
49    /// File name
50    pub file: Option<String>,
51    /// Line number
52    pub line: Option<u32>,
53    /// Module path
54    pub module: Option<String>,
55}
56
57impl StackFrame {
58    /// Create a new stack frame
59    pub fn new(function: String) -> Self {
60        Self {
61            function,
62            file: None,
63            line: None,
64            module: None,
65        }
66    }
67
68    /// Set file name
69    pub fn with_file(mut self, file: String) -> Self {
70        self.file = Some(file);
71        self
72    }
73
74    /// Set line number
75    pub fn with_line(mut self, line: u32) -> Self {
76        self.line = Some(line);
77        self
78    }
79
80    /// Set module path
81    pub fn with_module(mut self, module: String) -> Self {
82        self.module = Some(module);
83        self
84    }
85
86    /// Get display name
87    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/// Stack trace (call stack)
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
98pub struct StackTrace {
99    /// Stack frames (bottom to top)
100    pub frames: Vec<StackFrame>,
101}
102
103impl StackTrace {
104    /// Create a new empty stack trace
105    pub fn new() -> Self {
106        Self { frames: Vec::new() }
107    }
108
109    /// Add a frame to the stack
110    pub fn push_frame(mut self, frame: StackFrame) -> Self {
111        self.frames.push(frame);
112        self
113    }
114
115    /// Get the depth of the stack
116    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/// Profile sample
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ProfileSample {
142    /// Stack trace
143    pub stack_trace: StackTrace,
144    /// Sample count (number of times this stack was observed)
145    pub count: u64,
146    /// CPU time in nanoseconds
147    pub cpu_time_ns: u64,
148    /// Memory allocated in bytes
149    pub memory_bytes: u64,
150    /// Timestamp of first observation
151    pub first_seen: u64,
152    /// Timestamp of last observation
153    pub last_seen: u64,
154}
155
156impl ProfileSample {
157    /// Create a new profile sample
158    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    /// Merge another sample into this one
175    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/// Profile data
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ProfileData {
186    /// Profile ID
187    pub id: String,
188    /// Profile type
189    pub profile_type: ProfileType,
190    /// Start time
191    pub start_time: u64,
192    /// End time
193    pub end_time: u64,
194    /// Samples
195    pub samples: Vec<ProfileSample>,
196    /// Total sample count
197    pub total_samples: u64,
198    /// Metadata
199    pub metadata: HashMap<String, String>,
200}
201
202impl ProfileData {
203    /// Create a new profile
204    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    /// Add a sample
222    pub fn add_sample(&mut self, sample: ProfileSample) {
223        self.total_samples += sample.count;
224        self.samples.push(sample);
225    }
226
227    /// Finish profiling
228    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    /// Get duration in seconds
236    pub fn duration_secs(&self) -> u64 {
237        self.end_time - self.start_time
238    }
239
240    /// Export as pprof format (simplified)
241    pub fn to_pprof(&self) -> String {
242        let mut output = String::new();
243
244        // Header
245        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        // Samples
251        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    /// Export as flamegraph format
262    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/// Profile type
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
275pub enum ProfileType {
276    /// CPU profiling
277    Cpu,
278    /// Memory profiling (allocations)
279    Memory,
280    /// Wall clock time profiling
281    Wall,
282}
283
284impl ProfileType {
285    /// Get the profile type as a string
286    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
295/// Profiler for continuous profiling
296pub struct Profiler {
297    /// Configuration
298    config: ProfilingConfig,
299    /// Active profiles
300    active_profiles: Arc<RwLock<HashMap<String, ProfileData>>>,
301    /// Completed profiles
302    completed_profiles: Arc<RwLock<VecDeque<ProfileData>>>,
303    /// Start time
304    start_time: Instant,
305}
306
307impl Profiler {
308    /// Create a new profiler
309    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    /// Start a new profiling session
319    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    /// Record a sample
333    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    /// Stop a profiling session
344    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        // Add to completed profiles
352        let mut completed = self.completed_profiles.write().await;
353        completed.push_back(profile.clone());
354
355        // Limit retention
356        self.cleanup_old_profiles(&mut completed).await;
357
358        Ok(profile)
359    }
360
361    /// Get active profile
362    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    /// List all active profiles
368    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    /// Get completed profile
374    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    /// List all completed profiles
380    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    /// Cleanup old profiles
386    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        // Also limit by count (keep max 100 profiles)
402        while completed.len() > 100 {
403            completed.pop_front();
404        }
405    }
406
407    /// Export profile to pprof format
408    pub async fn export_pprof(&self, profile_id: &str) -> Result<String> {
409        // Try active profiles first
410        if let Some(profile) = self.get_active_profile(profile_id).await {
411            return Ok(profile.to_pprof());
412        }
413
414        // Try completed profiles
415        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    /// Export profile to flamegraph format
423    pub async fn export_flamegraph(&self, profile_id: &str) -> Result<String> {
424        // Try active profiles first
425        if let Some(profile) = self.get_active_profile(profile_id).await {
426            return Ok(profile.to_flamegraph());
427        }
428
429        // Try completed profiles
430        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    /// Get profiler statistics
438    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/// Profiler statistics
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct ProfilerStatistics {
465    /// Number of active profiles
466    pub active_profiles: usize,
467    /// Number of completed profiles
468    pub completed_profiles: usize,
469    /// Total samples across all profiles
470    pub total_samples: u64,
471    /// Profiler uptime in seconds
472    pub uptime_secs: u64,
473}
474
475/// Helper for building stack traces
476pub struct StackTraceBuilder {
477    frames: Vec<StackFrame>,
478}
479
480impl StackTraceBuilder {
481    /// Create a new stack trace builder
482    pub fn new() -> Self {
483        Self { frames: Vec::new() }
484    }
485
486    /// Add a frame
487    pub fn push(mut self, frame: StackFrame) -> Self {
488        self.frames.push(frame);
489        self
490    }
491
492    /// Add a simple function frame
493    pub fn push_function(mut self, function: String) -> Self {
494        self.frames.push(StackFrame::new(function));
495        self
496    }
497
498    /// Build the stack trace
499    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        // Duration should be valid (>= 0 is implicit for u64)
601        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}