Skip to main content

jugar_probar/perf/
trace.rs

1//! Performance Trace Collection
2//!
3//! Collects and manages performance trace data.
4
5use super::span::{ActiveSpan, SharedTracerState, Span, SpanGuard, TracerState};
6use serde::{Deserialize, Serialize};
7use std::cell::RefCell;
8use std::rc::Rc;
9use std::time::{Duration, Instant};
10
11/// Trace configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TraceConfig {
14    /// Sample rate in Hz
15    pub sample_rate: u32,
16    /// Capture memory usage
17    pub capture_memory: bool,
18    /// Capture frame times
19    pub capture_frames: bool,
20    /// Maximum spans to store
21    pub max_spans: usize,
22}
23
24impl Default for TraceConfig {
25    fn default() -> Self {
26        Self {
27            sample_rate: super::DEFAULT_SAMPLE_RATE,
28            capture_memory: false,
29            capture_frames: true,
30            max_spans: 100_000,
31        }
32    }
33}
34
35impl TraceConfig {
36    /// Set sample rate
37    #[must_use]
38    pub fn with_sample_rate(mut self, rate: u32) -> Self {
39        self.sample_rate = rate;
40        self
41    }
42
43    /// Enable/disable memory tracking
44    #[must_use]
45    pub fn with_memory_tracking(mut self, enabled: bool) -> Self {
46        self.capture_memory = enabled;
47        self
48    }
49
50    /// Enable/disable frame time capture
51    #[must_use]
52    pub fn with_frame_capture(mut self, enabled: bool) -> Self {
53        self.capture_frames = enabled;
54        self
55    }
56
57    /// Set maximum spans
58    #[must_use]
59    pub fn with_max_spans(mut self, max: usize) -> Self {
60        self.max_spans = max;
61        self
62    }
63}
64
65/// Performance tracer with interior mutability
66#[derive(Debug)]
67pub struct Tracer {
68    config: TraceConfig,
69    recording: bool,
70    state: SharedTracerState,
71}
72
73impl std::fmt::Debug for TracerState {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("TracerState")
76            .field("active_spans", &self.active_spans.len())
77            .field("completed_spans", &self.completed_spans.len())
78            .field("current_span", &self.current_span)
79            .finish()
80    }
81}
82
83impl Default for Tracer {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl Tracer {
90    /// Create a new tracer with default config
91    #[must_use]
92    pub fn new() -> Self {
93        Self::with_config(TraceConfig::default())
94    }
95
96    /// Create a tracer with custom config
97    #[must_use]
98    pub fn with_config(config: TraceConfig) -> Self {
99        Self {
100            config,
101            recording: false,
102            state: Rc::new(RefCell::new(TracerState::new())),
103        }
104    }
105
106    /// Get configuration
107    #[must_use]
108    pub fn config(&self) -> &TraceConfig {
109        &self.config
110    }
111
112    /// Check if recording
113    #[must_use]
114    pub fn is_recording(&self) -> bool {
115        self.recording
116    }
117
118    /// Start recording
119    pub fn start(&mut self) {
120        self.recording = true;
121        let mut state = self.state.borrow_mut();
122        state.trace_start = Some(Instant::now());
123        state.active_spans.clear();
124        state.completed_spans.clear();
125        state.current_span = None;
126    }
127
128    /// Stop recording and return trace
129    pub fn stop(&mut self) -> Trace {
130        self.recording = false;
131
132        let mut state = self.state.borrow_mut();
133
134        // Close any open spans
135        let now = state.elapsed_ns();
136        let active: Vec<_> = state.active_spans.drain().collect();
137        for (_id, mut active_span) in active {
138            active_span.span.close(now);
139            state.completed_spans.push(active_span.span);
140        }
141
142        let duration = state.trace_start.map(|s| s.elapsed());
143
144        Trace {
145            spans: std::mem::take(&mut state.completed_spans),
146            duration,
147            config: self.config.clone(),
148        }
149    }
150
151    /// Create a new span
152    pub fn span(&self, name: &str) -> SpanGuard {
153        let mut state = self.state.borrow_mut();
154        let start_ns = state.elapsed_ns();
155        let start_instant = Instant::now();
156
157        let mut active = ActiveSpan::new(name, start_ns, start_instant);
158
159        // Set parent
160        if let Some(parent_id) = state.current_span {
161            active.span.parent = Some(parent_id);
162        }
163
164        let span_id = active.span.id;
165        state.active_spans.insert(span_id, active);
166        state.current_span = Some(span_id);
167
168        drop(state); // Release borrow before creating guard
169
170        SpanGuard::new(Rc::clone(&self.state), span_id, self.config.max_spans)
171    }
172
173    /// Get current span count
174    #[must_use]
175    pub fn active_span_count(&self) -> usize {
176        self.state.borrow().active_spans.len()
177    }
178
179    /// Get completed span count
180    #[must_use]
181    pub fn completed_span_count(&self) -> usize {
182        self.state.borrow().completed_spans.len()
183    }
184}
185
186/// Collected trace data
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct Trace {
189    /// Collected spans
190    pub spans: Vec<Span>,
191    /// Total trace duration
192    #[serde(skip)]
193    pub duration: Option<Duration>,
194    /// Configuration used
195    pub config: TraceConfig,
196}
197
198impl Trace {
199    /// Get span count
200    #[must_use]
201    pub fn span_count(&self) -> usize {
202        self.spans.len()
203    }
204
205    /// Get total duration
206    #[must_use]
207    pub fn duration(&self) -> Option<Duration> {
208        self.duration
209    }
210
211    /// Get spans by name
212    #[must_use]
213    pub fn spans_by_name(&self, name: &str) -> Vec<&Span> {
214        self.spans.iter().filter(|s| s.name == name).collect()
215    }
216
217    /// Get root spans (no parent)
218    #[must_use]
219    pub fn root_spans(&self) -> Vec<&Span> {
220        self.spans.iter().filter(|s| s.parent.is_none()).collect()
221    }
222
223    /// Calculate statistics for a span name
224    #[must_use]
225    pub fn statistics_for(&self, name: &str) -> Option<super::metrics::Statistics> {
226        let durations: Vec<f64> = self
227            .spans_by_name(name)
228            .iter()
229            .filter_map(|s| s.duration_ns())
230            .map(|ns| ns as f64 / 1_000_000.0) // Convert to ms
231            .collect();
232
233        if durations.is_empty() {
234            None
235        } else {
236            Some(super::metrics::Statistics::from_values(&durations))
237        }
238    }
239
240    /// Check if empty
241    #[must_use]
242    pub fn is_empty(&self) -> bool {
243        self.spans.is_empty()
244    }
245}
246
247#[cfg(test)]
248#[allow(clippy::unwrap_used, clippy::expect_used)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_trace_config_default() {
254        let config = TraceConfig::default();
255        assert_eq!(config.sample_rate, super::super::DEFAULT_SAMPLE_RATE);
256        assert!(!config.capture_memory);
257        assert!(config.capture_frames);
258    }
259
260    #[test]
261    fn test_trace_config_builders() {
262        let config = TraceConfig::default()
263            .with_sample_rate(500)
264            .with_memory_tracking(true)
265            .with_max_spans(1000);
266
267        assert_eq!(config.sample_rate, 500);
268        assert!(config.capture_memory);
269        assert_eq!(config.max_spans, 1000);
270    }
271
272    #[test]
273    fn test_tracer_new() {
274        let tracer = Tracer::new();
275        assert!(!tracer.is_recording());
276    }
277
278    #[test]
279    fn test_tracer_start_stop() {
280        let mut tracer = Tracer::new();
281
282        tracer.start();
283        assert!(tracer.is_recording());
284
285        let trace = tracer.stop();
286        assert!(!tracer.is_recording());
287        assert!(trace.spans.is_empty());
288    }
289
290    #[test]
291    fn test_tracer_span() {
292        let mut tracer = Tracer::new();
293        tracer.start();
294
295        {
296            let _guard = tracer.span("test");
297            assert_eq!(tracer.active_span_count(), 1);
298        }
299
300        assert_eq!(tracer.active_span_count(), 0);
301        assert_eq!(tracer.completed_span_count(), 1);
302    }
303
304    #[test]
305    fn test_tracer_nested_spans() {
306        let mut tracer = Tracer::new();
307        tracer.start();
308
309        {
310            let _outer = tracer.span("outer");
311            {
312                let _inner = tracer.span("inner");
313            }
314        }
315
316        let trace = tracer.stop();
317        assert_eq!(trace.span_count(), 2);
318
319        // Verify parent-child relationship
320        let inner = trace.spans_by_name("inner")[0];
321        let outer = trace.spans_by_name("outer")[0];
322        assert_eq!(inner.parent, Some(outer.id));
323    }
324
325    #[test]
326    fn test_trace_spans_by_name() {
327        let mut tracer = Tracer::new();
328        tracer.start();
329
330        {
331            let _s1 = tracer.span("a");
332        }
333        {
334            let _s2 = tracer.span("b");
335        }
336        {
337            let _s3 = tracer.span("a");
338        }
339
340        let trace = tracer.stop();
341        assert_eq!(trace.spans_by_name("a").len(), 2);
342        assert_eq!(trace.spans_by_name("b").len(), 1);
343    }
344
345    #[test]
346    fn test_trace_root_spans() {
347        let mut tracer = Tracer::new();
348        tracer.start();
349
350        {
351            let _outer = tracer.span("outer");
352            {
353                let _inner = tracer.span("inner");
354            }
355        }
356        {
357            let _other = tracer.span("other");
358        }
359
360        let trace = tracer.stop();
361        let roots = trace.root_spans();
362        assert_eq!(roots.len(), 2); // "outer" and "other"
363    }
364
365    // =========================================================================
366    // Additional tests for 95% coverage
367    // =========================================================================
368
369    #[test]
370    fn test_trace_config_with_frame_capture() {
371        let config = TraceConfig::default().with_frame_capture(false);
372        assert!(!config.capture_frames);
373
374        let config2 = TraceConfig::default().with_frame_capture(true);
375        assert!(config2.capture_frames);
376    }
377
378    #[test]
379    fn test_tracer_default() {
380        let tracer = Tracer::default();
381        assert!(!tracer.is_recording());
382        assert_eq!(tracer.active_span_count(), 0);
383        assert_eq!(tracer.completed_span_count(), 0);
384    }
385
386    #[test]
387    fn test_tracer_config_getter() {
388        let config = TraceConfig::default().with_sample_rate(1000);
389        let tracer = Tracer::with_config(config);
390        assert_eq!(tracer.config().sample_rate, 1000);
391    }
392
393    #[test]
394    fn test_trace_is_empty() {
395        let mut tracer = Tracer::new();
396        tracer.start();
397        let trace = tracer.stop();
398        assert!(trace.is_empty());
399
400        tracer.start();
401        {
402            let _span = tracer.span("test");
403        }
404        let trace2 = tracer.stop();
405        assert!(!trace2.is_empty());
406    }
407
408    #[test]
409    fn test_trace_duration() {
410        let mut tracer = Tracer::new();
411        tracer.start();
412        std::thread::sleep(std::time::Duration::from_millis(10));
413        let trace = tracer.stop();
414
415        let duration = trace.duration();
416        assert!(duration.is_some());
417        assert!(duration.unwrap().as_millis() >= 10);
418    }
419
420    #[test]
421    fn test_trace_statistics_for_existing() {
422        let mut tracer = Tracer::new();
423        tracer.start();
424
425        for _ in 0..5 {
426            let _span = tracer.span("repeated");
427            std::thread::sleep(std::time::Duration::from_micros(100));
428        }
429
430        let trace = tracer.stop();
431        let stats = trace.statistics_for("repeated");
432        assert!(stats.is_some());
433        let stats = stats.unwrap();
434        assert!(stats.count >= 5);
435    }
436
437    #[test]
438    fn test_trace_statistics_for_nonexistent() {
439        let mut tracer = Tracer::new();
440        tracer.start();
441        {
442            let _span = tracer.span("exists");
443        }
444        let trace = tracer.stop();
445
446        let stats = trace.statistics_for("does_not_exist");
447        assert!(stats.is_none());
448    }
449
450    #[test]
451    fn test_tracer_state_debug() {
452        let tracer = Tracer::new();
453        let state = tracer.state.borrow();
454        let debug_str = format!("{:?}", *state);
455        assert!(debug_str.contains("TracerState"));
456        assert!(debug_str.contains("active_spans"));
457        assert!(debug_str.contains("completed_spans"));
458    }
459
460    #[test]
461    fn test_tracer_stop_closes_active_spans() {
462        let mut tracer = Tracer::new();
463        tracer.start();
464
465        // Create spans but don't drop them before stopping
466        let _outer = tracer.span("outer");
467        let _inner = tracer.span("inner");
468        drop(_inner);
469        drop(_outer);
470
471        let trace = tracer.stop();
472        // Both spans should be in completed list
473        assert_eq!(trace.span_count(), 2);
474    }
475
476    #[test]
477    fn test_tracer_multiple_sessions() {
478        let mut tracer = Tracer::new();
479
480        // First session
481        tracer.start();
482        {
483            let _span = tracer.span("first_session");
484        }
485        let trace1 = tracer.stop();
486        assert_eq!(trace1.span_count(), 1);
487
488        // Second session - should start fresh
489        tracer.start();
490        {
491            let _span = tracer.span("second_session");
492        }
493        let trace2 = tracer.stop();
494        assert_eq!(trace2.span_count(), 1);
495        assert_eq!(trace2.spans_by_name("first_session").len(), 0);
496    }
497
498    #[test]
499    fn test_trace_config_serialize_deserialize() {
500        let config = TraceConfig::default()
501            .with_sample_rate(500)
502            .with_memory_tracking(true)
503            .with_max_spans(5000);
504
505        let json = serde_json::to_string(&config).unwrap();
506        let deserialized: TraceConfig = serde_json::from_str(&json).unwrap();
507
508        assert_eq!(deserialized.sample_rate, 500);
509        assert!(deserialized.capture_memory);
510        assert_eq!(deserialized.max_spans, 5000);
511    }
512
513    #[test]
514    fn test_trace_serialize_deserialize() {
515        let mut tracer = Tracer::new();
516        tracer.start();
517        {
518            let _span = tracer.span("test_span");
519        }
520        let trace = tracer.stop();
521
522        let json = serde_json::to_string(&trace).unwrap();
523        let deserialized: Trace = serde_json::from_str(&json).unwrap();
524
525        assert_eq!(deserialized.span_count(), trace.span_count());
526        assert_eq!(deserialized.spans_by_name("test_span").len(), 1);
527    }
528
529    #[test]
530    fn test_deeply_nested_spans() {
531        let mut tracer = Tracer::new();
532        tracer.start();
533
534        {
535            let _l1 = tracer.span("level1");
536            {
537                let _l2 = tracer.span("level2");
538                {
539                    let _l3 = tracer.span("level3");
540                    {
541                        let _l4 = tracer.span("level4");
542                    }
543                }
544            }
545        }
546
547        let trace = tracer.stop();
548        assert_eq!(trace.span_count(), 4);
549
550        // Verify parent chain
551        let l4_spans = trace.spans_by_name("level4");
552        assert_eq!(l4_spans.len(), 1);
553        let l4 = l4_spans[0];
554        assert!(l4.parent.is_some());
555
556        let l3_spans = trace.spans_by_name("level3");
557        let l3 = l3_spans[0];
558        assert_eq!(l4.parent, Some(l3.id));
559    }
560
561    #[test]
562    fn test_sibling_spans() {
563        let mut tracer = Tracer::new();
564        tracer.start();
565
566        {
567            let _parent = tracer.span("parent");
568            {
569                let _child1 = tracer.span("child");
570            }
571            {
572                let _child2 = tracer.span("child");
573            }
574            {
575                let _child3 = tracer.span("child");
576            }
577        }
578
579        let trace = tracer.stop();
580        let children = trace.spans_by_name("child");
581        assert_eq!(children.len(), 3);
582
583        // All children should have the same parent
584        let parent_spans = trace.spans_by_name("parent");
585        let parent_id = parent_spans[0].id;
586        for child in children {
587            assert_eq!(child.parent, Some(parent_id));
588        }
589    }
590
591    #[test]
592    fn test_span_count_after_stop() {
593        let mut tracer = Tracer::new();
594        tracer.start();
595        {
596            let _s = tracer.span("a");
597        }
598        assert_eq!(tracer.completed_span_count(), 1);
599        assert_eq!(tracer.active_span_count(), 0);
600
601        tracer.stop();
602        // After stop, counts should be reset on next start
603        tracer.start();
604        assert_eq!(tracer.completed_span_count(), 0);
605    }
606}