relay_event_normalization/normalize/span/
exclusive_time.rs

1//! Span attribute materialization.
2
3use std::collections::HashMap;
4use std::time::Duration;
5
6use relay_event_schema::protocol::{Contexts, Event, Span, SpanId, TraceContext};
7use relay_protocol::Annotated;
8
9use crate::normalize::breakdowns::TimeWindowSpan;
10
11/// Computes the exclusive time of the source interval after subtracting the
12/// list of intervals.
13///
14/// Assumes that the input intervals are sorted by start time.
15fn interval_exclusive_time(mut parent: TimeWindowSpan, intervals: &[TimeWindowSpan]) -> Duration {
16    let mut exclusive_time = Duration::new(0, 0);
17
18    for interval in intervals {
19        // Exit early to avoid adding zeros
20        if interval.start >= parent.end {
21            break;
22        }
23
24        // Add time in the parent before the start of the current interval to the exclusive time
25        if let Ok(start_offset) = (interval.start - parent.start).to_std() {
26            exclusive_time += start_offset;
27        }
28
29        parent.start = interval.end.clamp(parent.start, parent.end);
30    }
31
32    // Add the remaining duration after the last interval ended
33    exclusive_time + parent.duration()
34}
35
36fn set_event_exclusive_time(
37    event_interval: TimeWindowSpan,
38    contexts: &mut Contexts,
39    span_map: &HashMap<SpanId, Vec<TimeWindowSpan>>,
40) {
41    let Some(trace_context) = contexts.get_mut::<TraceContext>() else {
42        return;
43    };
44
45    if trace_context.exclusive_time.value().is_some() {
46        // Exclusive time already set, respect it.
47        return;
48    }
49
50    let Some(span_id) = trace_context.span_id.value() else {
51        return;
52    };
53
54    let child_intervals = span_map
55        .get(span_id)
56        .map(|vec| vec.as_slice())
57        .unwrap_or_default();
58
59    let exclusive_time = interval_exclusive_time(event_interval, child_intervals);
60    trace_context.exclusive_time =
61        Annotated::new(relay_common::time::duration_to_millis(exclusive_time));
62}
63
64fn set_span_exclusive_time(
65    span: &mut Annotated<Span>,
66    span_map: &HashMap<SpanId, Vec<TimeWindowSpan>>,
67) {
68    let span = match span.value_mut() {
69        None => return,
70        Some(span) => span,
71    };
72
73    let span_id = match span.span_id.value() {
74        None => return,
75        Some(span_id) => span_id,
76    };
77
78    let span_interval = match (span.start_timestamp.value(), span.timestamp.value()) {
79        (Some(start), Some(end)) => TimeWindowSpan::new(*start, *end),
80        _ => return,
81    };
82
83    let child_intervals = span_map
84        .get(span_id)
85        .map(|vec| vec.as_slice())
86        .unwrap_or_default();
87
88    let exclusive_time = interval_exclusive_time(span_interval, child_intervals);
89    span.exclusive_time = Annotated::new(relay_common::time::duration_to_millis(exclusive_time));
90}
91
92/// Computes the exclusive time for all spans in the event.
93pub fn compute_span_exclusive_time(event: &mut Event) {
94    let contexts = match event.contexts.value_mut() {
95        Some(contexts) => contexts,
96        _ => return,
97    };
98
99    let event_interval = match (event.start_timestamp.value(), event.timestamp.value()) {
100        (Some(start), Some(end)) => TimeWindowSpan::new(*start, *end),
101        _ => return,
102    };
103
104    let spans = event.spans.value_mut().get_or_insert_with(Vec::new);
105
106    let mut span_map = HashMap::new();
107    for span in spans.iter() {
108        let span = match span.value() {
109            None => continue,
110            Some(span) => span,
111        };
112
113        let parent_span_id = match span.parent_span_id.value() {
114            None => continue,
115            Some(parent_span_id) => parent_span_id.clone(),
116        };
117
118        let interval = match (span.start_timestamp.value(), span.timestamp.value()) {
119            (Some(start), Some(end)) => TimeWindowSpan::new(*start, *end),
120            _ => continue,
121        };
122
123        span_map
124            .entry(parent_span_id)
125            .or_insert_with(Vec::new)
126            .push(interval)
127    }
128
129    // Sort intervals to fulfill precondition of `interval_exclusive_time`
130    for intervals in span_map.values_mut() {
131        intervals.sort_unstable_by_key(|interval| interval.start);
132    }
133
134    set_event_exclusive_time(event_interval, contexts, &span_map);
135
136    for span in spans.iter_mut() {
137        set_span_exclusive_time(span, &span_map);
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use chrono::{TimeZone, Utc};
144    use relay_event_schema::protocol::{EventType, Timestamp};
145    use similar_asserts::assert_eq;
146
147    use super::*;
148
149    fn make_event(
150        start: Timestamp,
151        end: Timestamp,
152        span_id: &str,
153        spans: Vec<Annotated<Span>>,
154    ) -> Event {
155        Event {
156            ty: EventType::Transaction.into(),
157            start_timestamp: Annotated::new(start),
158            timestamp: Annotated::new(end),
159            contexts: {
160                let mut contexts = Contexts::new();
161                contexts.add(TraceContext {
162                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
163                    span_id: Annotated::new(SpanId(span_id.into())),
164                    ..Default::default()
165                });
166                Annotated::new(contexts)
167            },
168            spans: spans.into(),
169            ..Default::default()
170        }
171    }
172
173    fn make_span(
174        op: &str,
175        description: &str,
176        start: Timestamp,
177        end: Timestamp,
178        span_id: &str,
179        parent_span_id: &str,
180    ) -> Annotated<Span> {
181        Annotated::new(Span {
182            op: Annotated::new(op.into()),
183            description: Annotated::new(description.into()),
184            start_timestamp: Annotated::new(start),
185            timestamp: Annotated::new(end),
186            trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
187            span_id: Annotated::new(SpanId(span_id.into())),
188            parent_span_id: Annotated::new(SpanId(parent_span_id.into())),
189            ..Default::default()
190        })
191    }
192
193    fn extract_exclusive_time(span: &Span) -> (&SpanId, f64) {
194        (
195            span.span_id.value().unwrap(),
196            *span.exclusive_time.value().unwrap(),
197        )
198    }
199
200    fn extract_span_exclusive_times(event: &Event) -> HashMap<&SpanId, f64> {
201        let spans = event.spans.value().unwrap();
202        let mut exclusive_times: HashMap<_, _> = spans
203            .iter()
204            .filter_map(Annotated::value)
205            .map(extract_exclusive_time)
206            .collect();
207
208        let trace_context = event.context::<TraceContext>().unwrap();
209        let transaction_span_id = trace_context.span_id.value().unwrap();
210        let transaction_exclusive_time = *trace_context.exclusive_time.value().unwrap();
211        exclusive_times.insert(transaction_span_id, transaction_exclusive_time);
212
213        exclusive_times
214    }
215
216    #[test]
217    fn test_childless_spans() {
218        let mut event = make_event(
219            Utc.timestamp_opt(1609455600, 0).unwrap().into(),
220            Utc.timestamp_opt(1609455605, 0).unwrap().into(),
221            "aaaaaaaaaaaaaaaa",
222            vec![
223                make_span(
224                    "db",
225                    "SELECT * FROM table;",
226                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
227                    Utc.timestamp_opt(1609455604, 0).unwrap().into(),
228                    "bbbbbbbbbbbbbbbb",
229                    "aaaaaaaaaaaaaaaa",
230                ),
231                make_span(
232                    "db",
233                    "SELECT * FROM table;",
234                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
235                    Utc.timestamp_opt(1609455603, 500_000_000).unwrap().into(),
236                    "cccccccccccccccc",
237                    "aaaaaaaaaaaaaaaa",
238                ),
239                make_span(
240                    "db",
241                    "SELECT * FROM table;",
242                    Utc.timestamp_opt(1609455603, 0).unwrap().into(),
243                    Utc.timestamp_opt(1609455604, 877_000_000).unwrap().into(),
244                    "dddddddddddddddd",
245                    "aaaaaaaaaaaaaaaa",
246                ),
247            ],
248        );
249
250        compute_span_exclusive_time(&mut event);
251
252        assert_eq!(
253            extract_span_exclusive_times(&event),
254            HashMap::from_iter([
255                (&SpanId("aaaaaaaaaaaaaaaa".to_string()), 1123.0),
256                (&SpanId("bbbbbbbbbbbbbbbb".to_string()), 3000.0),
257                (&SpanId("cccccccccccccccc".to_string()), 2500.0),
258                (&SpanId("dddddddddddddddd".to_string()), 1877.0)
259            ]),
260        );
261    }
262
263    #[test]
264    fn test_nested_spans() {
265        let mut event = make_event(
266            Utc.timestamp_opt(1609455600, 0).unwrap().into(),
267            Utc.timestamp_opt(1609455605, 0).unwrap().into(),
268            "aaaaaaaaaaaaaaaa",
269            vec![
270                make_span(
271                    "db",
272                    "SELECT * FROM table;",
273                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
274                    Utc.timestamp_opt(1609455602, 0).unwrap().into(),
275                    "bbbbbbbbbbbbbbbb",
276                    "aaaaaaaaaaaaaaaa",
277                ),
278                make_span(
279                    "db",
280                    "SELECT * FROM table;",
281                    Utc.timestamp_opt(1609455601, 200_000_000).unwrap().into(),
282                    Utc.timestamp_opt(1609455601, 800_000_000).unwrap().into(),
283                    "cccccccccccccccc",
284                    "bbbbbbbbbbbbbbbb",
285                ),
286                make_span(
287                    "db",
288                    "SELECT * FROM table;",
289                    Utc.timestamp_opt(1609455601, 400_000_000).unwrap().into(),
290                    Utc.timestamp_opt(1609455601, 600_000_000).unwrap().into(),
291                    "dddddddddddddddd",
292                    "cccccccccccccccc",
293                ),
294            ],
295        );
296
297        compute_span_exclusive_time(&mut event);
298
299        assert_eq!(
300            extract_span_exclusive_times(&event),
301            HashMap::from_iter([
302                (&SpanId("aaaaaaaaaaaaaaaa".to_string()), 4000.0),
303                (&SpanId("bbbbbbbbbbbbbbbb".to_string()), 400.0),
304                (&SpanId("cccccccccccccccc".to_string()), 400.0),
305                (&SpanId("dddddddddddddddd".to_string()), 200.0),
306            ])
307        );
308    }
309
310    #[test]
311    fn test_overlapping_child_spans() {
312        let mut event = make_event(
313            Utc.timestamp_opt(1609455600, 0).unwrap().into(),
314            Utc.timestamp_opt(1609455605, 0).unwrap().into(),
315            "aaaaaaaaaaaaaaaa",
316            vec![
317                make_span(
318                    "db",
319                    "SELECT * FROM table;",
320                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
321                    Utc.timestamp_opt(1609455602, 0).unwrap().into(),
322                    "bbbbbbbbbbbbbbbb",
323                    "aaaaaaaaaaaaaaaa",
324                ),
325                make_span(
326                    "db",
327                    "SELECT * FROM table;",
328                    Utc.timestamp_opt(1609455601, 200_000_000).unwrap().into(),
329                    Utc.timestamp_opt(1609455601, 600_000_000).unwrap().into(),
330                    "cccccccccccccccc",
331                    "bbbbbbbbbbbbbbbb",
332                ),
333                make_span(
334                    "db",
335                    "SELECT * FROM table;",
336                    Utc.timestamp_opt(1609455601, 400_000_000).unwrap().into(),
337                    Utc.timestamp_opt(1609455601, 800_000_000).unwrap().into(),
338                    "dddddddddddddddd",
339                    "bbbbbbbbbbbbbbbb",
340                ),
341            ],
342        );
343
344        compute_span_exclusive_time(&mut event);
345
346        assert_eq!(
347            extract_span_exclusive_times(&event),
348            HashMap::from_iter([
349                (&SpanId("aaaaaaaaaaaaaaaa".to_string()), 4000.0),
350                (&SpanId("bbbbbbbbbbbbbbbb".to_string()), 400.0),
351                (&SpanId("cccccccccccccccc".to_string()), 400.0),
352                (&SpanId("dddddddddddddddd".to_string()), 400.0),
353            ])
354        );
355    }
356
357    #[test]
358    fn test_child_spans_dont_intersect_parent() {
359        let mut event = make_event(
360            Utc.timestamp_opt(1609455600, 0).unwrap().into(),
361            Utc.timestamp_opt(1609455605, 0).unwrap().into(),
362            "aaaaaaaaaaaaaaaa",
363            vec![
364                make_span(
365                    "db",
366                    "SELECT * FROM table;",
367                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
368                    Utc.timestamp_opt(1609455602, 0).unwrap().into(),
369                    "bbbbbbbbbbbbbbbb",
370                    "aaaaaaaaaaaaaaaa",
371                ),
372                make_span(
373                    "db",
374                    "SELECT * FROM table;",
375                    Utc.timestamp_opt(1609455600, 400_000_000).unwrap().into(),
376                    Utc.timestamp_opt(1609455600, 800_000_000).unwrap().into(),
377                    "cccccccccccccccc",
378                    "bbbbbbbbbbbbbbbb",
379                ),
380                make_span(
381                    "db",
382                    "SELECT * FROM table;",
383                    Utc.timestamp_opt(1609455602, 200_000_000).unwrap().into(),
384                    Utc.timestamp_opt(1609455602, 600_000_000).unwrap().into(),
385                    "dddddddddddddddd",
386                    "bbbbbbbbbbbbbbbb",
387                ),
388            ],
389        );
390
391        compute_span_exclusive_time(&mut event);
392
393        assert_eq!(
394            extract_span_exclusive_times(&event),
395            HashMap::from_iter([
396                (&SpanId("aaaaaaaaaaaaaaaa".to_string()), 4000.0),
397                (&SpanId("bbbbbbbbbbbbbbbb".to_string()), 1000.0),
398                (&SpanId("cccccccccccccccc".to_string()), 400.0),
399                (&SpanId("dddddddddddddddd".to_string()), 400.0),
400            ])
401        );
402    }
403
404    #[test]
405    fn test_child_spans_extend_beyond_parent() {
406        let mut event = make_event(
407            Utc.timestamp_opt(1609455600, 0).unwrap().into(),
408            Utc.timestamp_opt(1609455605, 0).unwrap().into(),
409            "aaaaaaaaaaaaaaaa",
410            vec![
411                make_span(
412                    "db",
413                    "SELECT * FROM table;",
414                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
415                    Utc.timestamp_opt(1609455602, 0).unwrap().into(),
416                    "bbbbbbbbbbbbbbbb",
417                    "aaaaaaaaaaaaaaaa",
418                ),
419                make_span(
420                    "db",
421                    "SELECT * FROM table;",
422                    Utc.timestamp_opt(1609455600, 800_000_000).unwrap().into(),
423                    Utc.timestamp_opt(1609455601, 400_000_000).unwrap().into(),
424                    "cccccccccccccccc",
425                    "bbbbbbbbbbbbbbbb",
426                ),
427                make_span(
428                    "db",
429                    "SELECT * FROM table;",
430                    Utc.timestamp_opt(1609455601, 600_000_000).unwrap().into(),
431                    Utc.timestamp_opt(1609455602, 200_000_000).unwrap().into(),
432                    "dddddddddddddddd",
433                    "bbbbbbbbbbbbbbbb",
434                ),
435            ],
436        );
437
438        compute_span_exclusive_time(&mut event);
439
440        assert_eq!(
441            extract_span_exclusive_times(&event),
442            HashMap::from_iter([
443                (&SpanId("aaaaaaaaaaaaaaaa".to_string()), 4000.0),
444                (&SpanId("bbbbbbbbbbbbbbbb".to_string()), 200.0),
445                (&SpanId("cccccccccccccccc".to_string()), 600.0),
446                (&SpanId("dddddddddddddddd".to_string()), 600.0),
447            ])
448        );
449    }
450
451    #[test]
452    fn test_child_spans_consumes_all_of_parent() {
453        let mut event = make_event(
454            Utc.timestamp_opt(1609455600, 0).unwrap().into(),
455            Utc.timestamp_opt(1609455605, 0).unwrap().into(),
456            "aaaaaaaaaaaaaaaa",
457            vec![
458                make_span(
459                    "db",
460                    "SELECT * FROM table;",
461                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
462                    Utc.timestamp_opt(1609455602, 0).unwrap().into(),
463                    "bbbbbbbbbbbbbbbb",
464                    "aaaaaaaaaaaaaaaa",
465                ),
466                make_span(
467                    "db",
468                    "SELECT * FROM table;",
469                    Utc.timestamp_opt(1609455600, 800_000_000).unwrap().into(),
470                    Utc.timestamp_opt(1609455601, 600_000_000).unwrap().into(),
471                    "cccccccccccccccc",
472                    "bbbbbbbbbbbbbbbb",
473                ),
474                make_span(
475                    "db",
476                    "SELECT * FROM table;",
477                    Utc.timestamp_opt(1609455601, 400_000_000).unwrap().into(),
478                    Utc.timestamp_opt(1609455602, 200_000_000).unwrap().into(),
479                    "dddddddddddddddd",
480                    "bbbbbbbbbbbbbbbb",
481                ),
482            ],
483        );
484
485        compute_span_exclusive_time(&mut event);
486
487        assert_eq!(
488            extract_span_exclusive_times(&event),
489            HashMap::from_iter([
490                (&SpanId("aaaaaaaaaaaaaaaa".to_string()), 4000.0),
491                (&SpanId("bbbbbbbbbbbbbbbb".to_string()), 0.0),
492                (&SpanId("cccccccccccccccc".to_string()), 800.0),
493                (&SpanId("dddddddddddddddd".to_string()), 800.0),
494            ])
495        );
496    }
497
498    #[test]
499    fn test_only_immediate_child_spans_affect_calculation() {
500        let mut event = make_event(
501            Utc.timestamp_opt(1609455600, 0).unwrap().into(),
502            Utc.timestamp_opt(1609455605, 0).unwrap().into(),
503            "aaaaaaaaaaaaaaaa",
504            vec![
505                make_span(
506                    "db",
507                    "SELECT * FROM table;",
508                    Utc.timestamp_opt(1609455601, 0).unwrap().into(),
509                    Utc.timestamp_opt(1609455602, 0).unwrap().into(),
510                    "bbbbbbbbbbbbbbbb",
511                    "aaaaaaaaaaaaaaaa",
512                ),
513                make_span(
514                    "db",
515                    "SELECT * FROM table;",
516                    Utc.timestamp_opt(1609455601, 600_000_000).unwrap().into(),
517                    Utc.timestamp_opt(1609455602, 200_000_000).unwrap().into(),
518                    "cccccccccccccccc",
519                    "bbbbbbbbbbbbbbbb",
520                ),
521                // this should only affect the calculation for it's immediate parent
522                // which is `cccccccccccccccc` and not `bbbbbbbbbbbbbbbb`
523                make_span(
524                    "db",
525                    "SELECT * FROM table;",
526                    Utc.timestamp_opt(1609455601, 400_000_000).unwrap().into(),
527                    Utc.timestamp_opt(1609455601, 800_000_000).unwrap().into(),
528                    "dddddddddddddddd",
529                    "cccccccccccccccc",
530                ),
531            ],
532        );
533
534        compute_span_exclusive_time(&mut event);
535
536        assert_eq!(
537            extract_span_exclusive_times(&event),
538            HashMap::from_iter([
539                (&SpanId("aaaaaaaaaaaaaaaa".to_string()), 4000.0),
540                (&SpanId("bbbbbbbbbbbbbbbb".to_string()), 600.0),
541                (&SpanId("cccccccccccccccc".to_string()), 400.0),
542                (&SpanId("dddddddddddddddd".to_string()), 400.0),
543            ])
544        );
545    }
546}