Skip to main content

jugar_probar/perf/
span.rs

1//! Performance Tracing Spans
2//!
3//! Hierarchical span-based performance measurement.
4
5use serde::{Deserialize, Serialize};
6use std::cell::RefCell;
7use std::rc::Rc;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11static NEXT_SPAN_ID: AtomicU64 = AtomicU64::new(1);
12
13/// Unique span identifier
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct SpanId(u64);
16
17impl SpanId {
18    /// Create a new unique span ID
19    #[must_use]
20    pub fn new() -> Self {
21        Self(NEXT_SPAN_ID.fetch_add(1, Ordering::Relaxed))
22    }
23
24    /// Get the raw ID value
25    #[must_use]
26    pub fn as_u64(&self) -> u64 {
27        self.0
28    }
29}
30
31impl Default for SpanId {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37/// A performance measurement span
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Span {
40    /// Unique identifier
41    pub id: SpanId,
42    /// Span name
43    pub name: String,
44    /// Start timestamp (nanoseconds from trace start)
45    pub start_ns: u64,
46    /// End timestamp (nanoseconds from trace start)
47    pub end_ns: Option<u64>,
48    /// Parent span ID
49    pub parent: Option<SpanId>,
50    /// Span category
51    pub category: Option<String>,
52    /// Additional metadata
53    #[serde(default)]
54    pub metadata: std::collections::HashMap<String, String>,
55}
56
57impl Span {
58    /// Create a new span
59    #[must_use]
60    pub fn new(name: impl Into<String>, start_ns: u64) -> Self {
61        Self {
62            id: SpanId::new(),
63            name: name.into(),
64            start_ns,
65            end_ns: None,
66            parent: None,
67            category: None,
68            metadata: std::collections::HashMap::new(),
69        }
70    }
71
72    /// Create span with parent
73    #[must_use]
74    pub fn with_parent(mut self, parent: SpanId) -> Self {
75        self.parent = Some(parent);
76        self
77    }
78
79    /// Set category
80    #[must_use]
81    pub fn with_category(mut self, category: impl Into<String>) -> Self {
82        self.category = Some(category.into());
83        self
84    }
85
86    /// Add metadata
87    pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
88        self.metadata.insert(key.into(), value.into());
89    }
90
91    /// Close the span
92    pub fn close(&mut self, end_ns: u64) {
93        self.end_ns = Some(end_ns);
94    }
95
96    /// Get duration in nanoseconds
97    #[must_use]
98    pub fn duration_ns(&self) -> Option<u64> {
99        self.end_ns.map(|end| end.saturating_sub(self.start_ns))
100    }
101
102    /// Get duration as Duration
103    #[must_use]
104    pub fn duration(&self) -> Option<Duration> {
105        self.duration_ns().map(Duration::from_nanos)
106    }
107
108    /// Check if span is closed
109    #[must_use]
110    pub fn is_closed(&self) -> bool {
111        self.end_ns.is_some()
112    }
113}
114
115/// Internal tracer state for RefCell-based interior mutability
116pub(crate) struct TracerState {
117    pub active_spans: std::collections::HashMap<SpanId, ActiveSpan>,
118    pub completed_spans: Vec<Span>,
119    pub current_span: Option<SpanId>,
120    pub trace_start: Option<Instant>,
121}
122
123impl TracerState {
124    pub fn new() -> Self {
125        Self {
126            active_spans: std::collections::HashMap::new(),
127            completed_spans: Vec::new(),
128            current_span: None,
129            trace_start: None,
130        }
131    }
132
133    pub fn elapsed_ns(&self) -> u64 {
134        self.trace_start
135            .map(|start| start.elapsed().as_nanos() as u64)
136            .unwrap_or(0)
137    }
138}
139
140/// Shared reference to tracer state
141pub(crate) type SharedTracerState = Rc<RefCell<TracerState>>;
142
143/// RAII guard for automatic span closure
144pub struct SpanGuard {
145    state: SharedTracerState,
146    span_id: SpanId,
147    max_spans: usize,
148}
149
150impl std::fmt::Debug for SpanGuard {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("SpanGuard")
153            .field("span_id", &self.span_id)
154            .field("max_spans", &self.max_spans)
155            .finish_non_exhaustive()
156    }
157}
158
159impl SpanGuard {
160    /// Create a new span guard
161    pub(crate) fn new(state: SharedTracerState, span_id: SpanId, max_spans: usize) -> Self {
162        Self {
163            state,
164            span_id,
165            max_spans,
166        }
167    }
168
169    /// Get the span ID
170    #[must_use]
171    pub fn id(&self) -> SpanId {
172        self.span_id
173    }
174}
175
176impl Drop for SpanGuard {
177    fn drop(&mut self) {
178        let mut state = self.state.borrow_mut();
179        if let Some(mut active) = state.active_spans.remove(&self.span_id) {
180            let end_ns = state.elapsed_ns();
181            active.span.close(end_ns);
182
183            // Update current span to parent
184            state.current_span = active.span.parent;
185
186            // Store completed span
187            if state.completed_spans.len() < self.max_spans {
188                state.completed_spans.push(active.span);
189            }
190        }
191    }
192}
193
194/// Internal span with timing information
195#[derive(Debug)]
196pub(crate) struct ActiveSpan {
197    pub span: Span,
198    #[allow(dead_code)]
199    pub start_instant: Instant,
200}
201
202impl ActiveSpan {
203    pub fn new(name: impl Into<String>, start_ns: u64, start_instant: Instant) -> Self {
204        Self {
205            span: Span::new(name, start_ns),
206            start_instant,
207        }
208    }
209}
210
211#[cfg(test)]
212#[allow(clippy::unwrap_used, clippy::expect_used)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_span_id_unique() {
218        let id1 = SpanId::new();
219        let id2 = SpanId::new();
220        assert_ne!(id1, id2);
221    }
222
223    #[test]
224    fn test_span_id_as_u64() {
225        let id = SpanId::new();
226        assert!(id.as_u64() > 0);
227    }
228
229    #[test]
230    fn test_span_id_default() {
231        let id1 = SpanId::default();
232        let id2 = SpanId::default();
233        // Default should call new(), producing unique IDs
234        assert_ne!(id1, id2);
235        assert!(id1.as_u64() > 0);
236    }
237
238    #[test]
239    fn test_span_id_equality() {
240        let id = SpanId::new();
241        let id_copy = id;
242        assert_eq!(id, id_copy);
243    }
244
245    #[test]
246    fn test_span_id_hash() {
247        use std::collections::HashSet;
248        let mut set = HashSet::new();
249        let id1 = SpanId::new();
250        let id2 = SpanId::new();
251        set.insert(id1);
252        set.insert(id2);
253        assert_eq!(set.len(), 2);
254        assert!(set.contains(&id1));
255        assert!(set.contains(&id2));
256    }
257
258    #[test]
259    fn test_span_new() {
260        let span = Span::new("test", 1000);
261        assert_eq!(span.name, "test");
262        assert_eq!(span.start_ns, 1000);
263        assert!(!span.is_closed());
264    }
265
266    #[test]
267    fn test_span_with_parent() {
268        let parent_id = SpanId::new();
269        let span = Span::new("child", 2000).with_parent(parent_id);
270        assert_eq!(span.parent, Some(parent_id));
271    }
272
273    #[test]
274    fn test_span_with_category() {
275        let span = Span::new("test", 0).with_category("render");
276        assert_eq!(span.category, Some("render".to_string()));
277    }
278
279    #[test]
280    fn test_span_close() {
281        let mut span = Span::new("test", 1000);
282        span.close(2000);
283        assert!(span.is_closed());
284        assert_eq!(span.duration_ns(), Some(1000));
285    }
286
287    #[test]
288    fn test_span_duration() {
289        let mut span = Span::new("test", 0);
290        span.close(1_000_000); // 1ms in ns
291        let duration = span.duration().unwrap();
292        assert_eq!(duration, Duration::from_nanos(1_000_000));
293    }
294
295    #[test]
296    fn test_span_metadata() {
297        let mut span = Span::new("test", 0);
298        span.add_metadata("key", "value");
299        assert_eq!(span.metadata.get("key"), Some(&"value".to_string()));
300    }
301
302    #[test]
303    fn test_span_duration_ns_unclosed() {
304        let span = Span::new("test", 1000);
305        // Unclosed span should return None for duration
306        assert_eq!(span.duration_ns(), None);
307    }
308
309    #[test]
310    fn test_span_duration_unclosed() {
311        let span = Span::new("test", 1000);
312        // Unclosed span should return None for duration
313        assert!(span.duration().is_none());
314    }
315
316    #[test]
317    fn test_span_duration_saturating_sub() {
318        // Edge case: end_ns < start_ns (shouldn't happen normally, but we handle it)
319        let mut span = Span::new("test", 5000);
320        span.close(1000); // End before start
321                          // saturating_sub should return 0 instead of underflowing
322        assert_eq!(span.duration_ns(), Some(0));
323        assert_eq!(span.duration(), Some(Duration::from_nanos(0)));
324    }
325
326    #[test]
327    fn test_span_multiple_metadata() {
328        let mut span = Span::new("test", 0);
329        span.add_metadata("key1", "value1");
330        span.add_metadata("key2", "value2");
331        span.add_metadata("key1", "updated"); // Overwrite existing key
332        assert_eq!(span.metadata.get("key1"), Some(&"updated".to_string()));
333        assert_eq!(span.metadata.get("key2"), Some(&"value2".to_string()));
334        assert_eq!(span.metadata.len(), 2);
335    }
336
337    #[test]
338    fn test_span_is_closed_initially_false() {
339        let span = Span::new("test", 0);
340        assert!(!span.is_closed());
341        assert!(span.end_ns.is_none());
342    }
343
344    // TracerState tests
345    #[test]
346    fn test_tracer_state_new() {
347        let state = TracerState::new();
348        assert!(state.active_spans.is_empty());
349        assert!(state.completed_spans.is_empty());
350        assert!(state.current_span.is_none());
351        assert!(state.trace_start.is_none());
352    }
353
354    #[test]
355    fn test_tracer_state_elapsed_ns_no_start() {
356        let state = TracerState::new();
357        // When trace_start is None, elapsed_ns returns 0
358        assert_eq!(state.elapsed_ns(), 0);
359    }
360
361    #[test]
362    fn test_tracer_state_elapsed_ns_with_start() {
363        let mut state = TracerState::new();
364        state.trace_start = Some(Instant::now());
365        // Sleep briefly to ensure elapsed time > 0
366        std::thread::sleep(Duration::from_micros(100));
367        let elapsed = state.elapsed_ns();
368        assert!(elapsed > 0);
369    }
370
371    // ActiveSpan tests
372    #[test]
373    fn test_active_span_new() {
374        let instant = Instant::now();
375        let active = ActiveSpan::new("test_span", 12345, instant);
376        assert_eq!(active.span.name, "test_span");
377        assert_eq!(active.span.start_ns, 12345);
378        assert!(!active.span.is_closed());
379    }
380
381    #[test]
382    fn test_active_span_debug() {
383        let instant = Instant::now();
384        let active = ActiveSpan::new("debug_test", 0, instant);
385        let debug_str = format!("{:?}", active);
386        assert!(debug_str.contains("ActiveSpan"));
387        assert!(debug_str.contains("span"));
388    }
389
390    // SpanGuard tests
391    #[test]
392    fn test_span_guard_new_and_id() {
393        let state = Rc::new(RefCell::new(TracerState::new()));
394        let span_id = SpanId::new();
395        let guard = SpanGuard::new(Rc::clone(&state), span_id, 100);
396        assert_eq!(guard.id(), span_id);
397    }
398
399    #[test]
400    fn test_span_guard_debug() {
401        let state = Rc::new(RefCell::new(TracerState::new()));
402        let span_id = SpanId::new();
403        let guard = SpanGuard::new(Rc::clone(&state), span_id, 100);
404        let debug_str = format!("{:?}", guard);
405        assert!(debug_str.contains("SpanGuard"));
406        assert!(debug_str.contains("span_id"));
407        assert!(debug_str.contains("max_spans"));
408    }
409
410    #[test]
411    fn test_span_guard_drop_closes_span() {
412        let state = Rc::new(RefCell::new(TracerState::new()));
413        {
414            let mut s = state.borrow_mut();
415            s.trace_start = Some(Instant::now());
416        }
417
418        let span_id = SpanId::new();
419        let active_span = ActiveSpan::new("test", 0, Instant::now());
420
421        {
422            let mut s = state.borrow_mut();
423            s.active_spans.insert(span_id, active_span);
424            s.current_span = Some(span_id);
425        }
426
427        // Create guard and let it drop
428        {
429            let _guard = SpanGuard::new(Rc::clone(&state), span_id, 100);
430        } // guard drops here
431
432        let s = state.borrow();
433        // Span should be removed from active_spans
434        assert!(!s.active_spans.contains_key(&span_id));
435        // Span should be in completed_spans
436        assert_eq!(s.completed_spans.len(), 1);
437        assert!(s.completed_spans[0].is_closed());
438        // current_span should be updated to parent (None in this case)
439        assert!(s.current_span.is_none());
440    }
441
442    #[test]
443    fn test_span_guard_drop_respects_max_spans() {
444        let state = Rc::new(RefCell::new(TracerState::new()));
445        {
446            let mut s = state.borrow_mut();
447            s.trace_start = Some(Instant::now());
448            // Pre-fill completed_spans to max capacity
449            for i in 0..5 {
450                let mut span = Span::new(format!("existing_{}", i), i as u64 * 100);
451                span.close(i as u64 * 100 + 50);
452                s.completed_spans.push(span);
453            }
454        }
455
456        let span_id = SpanId::new();
457        let active_span = ActiveSpan::new("overflow_test", 500, Instant::now());
458
459        {
460            let mut s = state.borrow_mut();
461            s.active_spans.insert(span_id, active_span);
462        }
463
464        // Create guard with max_spans = 5 (already at capacity)
465        {
466            let _guard = SpanGuard::new(Rc::clone(&state), span_id, 5);
467        } // guard drops here
468
469        let s = state.borrow();
470        // Span should be removed from active_spans
471        assert!(!s.active_spans.contains_key(&span_id));
472        // completed_spans should NOT grow beyond max (5 items)
473        assert_eq!(s.completed_spans.len(), 5);
474    }
475
476    #[test]
477    fn test_span_guard_drop_updates_parent() {
478        let state = Rc::new(RefCell::new(TracerState::new()));
479        {
480            let mut s = state.borrow_mut();
481            s.trace_start = Some(Instant::now());
482        }
483
484        let parent_id = SpanId::new();
485        let child_id = SpanId::new();
486
487        let mut child_span = ActiveSpan::new("child", 100, Instant::now());
488        child_span.span.parent = Some(parent_id);
489
490        {
491            let mut s = state.borrow_mut();
492            s.active_spans.insert(child_id, child_span);
493            s.current_span = Some(child_id);
494        }
495
496        // Drop the child guard
497        {
498            let _guard = SpanGuard::new(Rc::clone(&state), child_id, 100);
499        }
500
501        let s = state.borrow();
502        // current_span should now be the parent
503        assert_eq!(s.current_span, Some(parent_id));
504    }
505
506    #[test]
507    fn test_span_guard_drop_missing_span() {
508        // Test dropping a guard when the span has already been removed
509        let state = Rc::new(RefCell::new(TracerState::new()));
510        {
511            let mut s = state.borrow_mut();
512            s.trace_start = Some(Instant::now());
513        }
514
515        let span_id = SpanId::new();
516        // Note: we don't add the span to active_spans
517
518        // Create and drop guard for non-existent span
519        {
520            let _guard = SpanGuard::new(Rc::clone(&state), span_id, 100);
521        } // guard drops here - should not panic
522
523        let s = state.borrow();
524        // Nothing should have been added
525        assert!(s.completed_spans.is_empty());
526    }
527
528    // Serialization tests
529    #[test]
530    fn test_span_id_serde() {
531        let id = SpanId::new();
532        let serialized = serde_json::to_string(&id).unwrap();
533        let deserialized: SpanId = serde_json::from_str(&serialized).unwrap();
534        assert_eq!(id, deserialized);
535    }
536
537    #[test]
538    fn test_span_serde() {
539        let mut span = Span::new("serde_test", 1000);
540        span.close(2000);
541        span.category = Some("test_category".to_string());
542        span.add_metadata("key", "value");
543
544        let serialized = serde_json::to_string(&span).unwrap();
545        let deserialized: Span = serde_json::from_str(&serialized).unwrap();
546
547        assert_eq!(deserialized.name, "serde_test");
548        assert_eq!(deserialized.start_ns, 1000);
549        assert_eq!(deserialized.end_ns, Some(2000));
550        assert_eq!(deserialized.category, Some("test_category".to_string()));
551        assert_eq!(deserialized.metadata.get("key"), Some(&"value".to_string()));
552    }
553
554    #[test]
555    fn test_span_serde_empty_metadata() {
556        let span = Span::new("minimal", 500);
557        let serialized = serde_json::to_string(&span).unwrap();
558        let deserialized: Span = serde_json::from_str(&serialized).unwrap();
559        assert!(deserialized.metadata.is_empty());
560    }
561
562    #[test]
563    fn test_span_clone() {
564        let mut original = Span::new("original", 100);
565        original.close(200);
566        original.category = Some("cat".to_string());
567        original.add_metadata("k", "v");
568
569        let cloned = original.clone();
570        assert_eq!(cloned.name, original.name);
571        assert_eq!(cloned.start_ns, original.start_ns);
572        assert_eq!(cloned.end_ns, original.end_ns);
573        assert_eq!(cloned.category, original.category);
574        assert_eq!(cloned.metadata, original.metadata);
575        // IDs should be the same (clone, not new)
576        assert_eq!(cloned.id, original.id);
577    }
578
579    #[test]
580    fn test_span_id_clone_copy() {
581        let id = SpanId::new();
582        let cloned = id;
583        let copied = id;
584        assert_eq!(id, cloned);
585        assert_eq!(id, copied);
586    }
587
588    #[test]
589    fn test_span_zero_duration() {
590        let mut span = Span::new("instant", 1000);
591        span.close(1000); // Same start and end
592        assert_eq!(span.duration_ns(), Some(0));
593        assert_eq!(span.duration(), Some(Duration::ZERO));
594    }
595
596    #[test]
597    fn test_span_large_timestamps() {
598        let start = u64::MAX - 1000;
599        let end = u64::MAX;
600        let mut span = Span::new("large", start);
601        span.close(end);
602        assert_eq!(span.duration_ns(), Some(1000));
603    }
604
605    #[test]
606    fn test_tracer_state_active_spans_operations() {
607        let mut state = TracerState::new();
608        let id1 = SpanId::new();
609        let id2 = SpanId::new();
610
611        let span1 = ActiveSpan::new("span1", 0, Instant::now());
612        let span2 = ActiveSpan::new("span2", 100, Instant::now());
613
614        state.active_spans.insert(id1, span1);
615        state.active_spans.insert(id2, span2);
616
617        assert_eq!(state.active_spans.len(), 2);
618        assert!(state.active_spans.contains_key(&id1));
619        assert!(state.active_spans.contains_key(&id2));
620
621        state.active_spans.remove(&id1);
622        assert_eq!(state.active_spans.len(), 1);
623        assert!(!state.active_spans.contains_key(&id1));
624    }
625}