Skip to main content

turbo_trace_server/
span.rs

1use std::{
2    num::{NonZeroU64, NonZeroUsize},
3    sync::{Arc, OnceLock},
4};
5
6use hashbrown::HashMap;
7use smallvec::SmallVec;
8use turbo_rcstr::RcStr;
9
10use crate::{lazy_sorted_vec::LazySortedVec, timestamp::Timestamp};
11
12pub type SpanIndex = NonZeroUsize;
13
14/// Storage for `Span::args` ~32% of spans have <=1 arg (typically just the
15/// `name` key for `turbo_tasks::function` spans), so inlining one entry
16/// avoids a heap allocation in this common case.
17pub type SpanArgs = SmallVec<[(RcStr, RcStr); 1]>;
18
19pub struct Span {
20    // These values won't change after creation:
21    pub parent: Option<SpanIndex>,
22    pub depth: u32,
23    pub start: Timestamp,
24    pub category: RcStr,
25    pub name: RcStr,
26    pub args: SpanArgs,
27
28    // This might change during writing:
29    /// The list of events sorted by start time. Backed by a SmallVec so leaf
30    /// spans (~69%, typically just one self-time event) don't pay a heap
31    /// allocation.
32    pub events: LazySortedVec<SpanEvent>,
33    pub is_complete: bool,
34
35    // These values are computed automatically:
36    pub self_allocations: u64,
37    pub self_allocation_count: u64,
38    pub self_deallocations: u64,
39    pub self_deallocation_count: u64,
40
41    // These values are computed when accessed (and maybe deleted during writing).
42    // Bundling the subtree totals into a single OnceLock pays a small cost on
43    // partial reads in exchange for a much-reduced lock count per Span.
44    pub totals: OnceLock<SpanTotals>,
45    pub time_data: SpanTimeData,
46    pub extra: OnceLock<Box<SpanExtra>>,
47    /// Lazy first-touch via `OnceLock`, but inline rather than boxed: ~96% of
48    /// spans get names populated after browsing, never invalidated, so the box
49    /// indirection is pure overhead.
50    pub names: OnceLock<SpanNames>,
51}
52
53#[derive(Default)]
54pub struct SpanTotals {
55    pub max_depth: u32,
56    pub allocations: u64,
57    pub deallocations: u64,
58    pub persistent_allocations: u64,
59    pub allocation_count: u64,
60    pub span_count: u64,
61}
62
63#[derive(Default)]
64pub struct SpanTimeData {
65    // These values won't change after creation:
66    pub ignore_self_time: bool,
67
68    // This might change during writing:
69    pub self_end: Timestamp,
70
71    // These values are computed automatically:
72    pub self_time: Timestamp,
73
74    // These values are computed when accessed (and maybe deleted during writing):
75    pub end: OnceLock<Timestamp>,
76    pub total_time: OnceLock<Timestamp>,
77    pub corrected_self_time: OnceLock<Timestamp>,
78    pub corrected_total_time: OnceLock<Timestamp>,
79}
80
81#[derive(Default)]
82pub struct SpanExtra {
83    pub graph: OnceLock<Vec<SpanGraphEvent>>,
84    pub bottom_up: OnceLock<Vec<Arc<SpanBottomUp>>>,
85    pub search_index: OnceLock<HashMap<RcStr, Vec<SpanIndex>>>,
86}
87
88#[derive(Clone)]
89pub struct SpanName {
90    pub category: RcStr,
91    pub title: RcStr,
92}
93
94pub struct SpanNames {
95    pub nice_name: SpanName,
96    pub group_name: SpanName,
97}
98
99impl Span {
100    pub fn extra(&self) -> &SpanExtra {
101        self.extra.get_or_init(Default::default)
102    }
103
104    pub fn names(&self) -> &SpanNames {
105        self.names.get_or_init(|| self.compute_names())
106    }
107
108    fn compute_names(&self) -> SpanNames {
109        // Classify the span. `turbo_tasks::function` and the resolve-call spans
110        // get special-cased rendering when they carry a `name` arg; everything
111        // else is rendered generically.
112        enum Kind {
113            Function,
114            Resolve,
115            Other,
116        }
117        let kind = match self.name.as_str() {
118            "turbo_tasks::function" => Kind::Function,
119            "turbo_tasks::resolve_call" | "turbo_tasks::resolve_trait_call" => Kind::Resolve,
120            _ => Kind::Other,
121        };
122        let arg_name = self.args.iter().find(|&(k, _)| k == "name").map(|(_, v)| v);
123
124        // Generic fallback used by both names whenever no special case applies.
125        let generic = || SpanName {
126            category: self.category.clone(),
127            title: self.name.clone(),
128        };
129
130        // Each arm constructs the full `SpanNames` so the relationship between
131        // `nice_name` and `group_name` is visible at a glance. The `Some(n)`
132        // rows handle the "this span carries a `name` arg" case; the `None`
133        // arm falls back to the generic shape for both names — including for
134        // function/resolve spans, which (in practice) always carry a name arg,
135        // so the fallback is mostly defensive.
136        match (kind, arg_name) {
137            (Kind::Function, Some(n)) => {
138                let pretty = SpanName {
139                    category: self.name.clone(),
140                    title: n.clone(),
141                };
142                SpanNames {
143                    nice_name: pretty.clone(),
144                    group_name: pretty,
145                }
146            }
147            (Kind::Resolve, Some(n)) => SpanNames {
148                nice_name: SpanName {
149                    category: self.name.clone(),
150                    title: format!("*{n}").into(),
151                },
152                group_name: SpanName {
153                    category: self.category.clone(),
154                    title: format!("{} *{n}", self.name).into(),
155                },
156            },
157            (Kind::Other, Some(n)) => SpanNames {
158                nice_name: SpanName {
159                    category: self.category.clone(),
160                    title: format!("{} {n}", self.name).into(),
161                },
162                group_name: generic(),
163            },
164            (_, None) => SpanNames {
165                nice_name: generic(),
166                group_name: generic(),
167            },
168        }
169    }
170}
171
172/// Stores `duration` as `NonZeroU64` so the variant has a niche; combined with
173/// `Child`'s `NonZeroUsize` index, this lets the compiler pack `SpanEvent`
174/// without a separate discriminant byte (saving 8 bytes per event vs. an
175/// `end: Timestamp` layout). Callers must filter zero-duration self-time
176/// events before constructing — see [`SpanEvent::self_time`].
177pub struct SpanEventSelfTime {
178    pub start: Timestamp,
179    pub duration: NonZeroU64,
180    pub corrected_self_time: OnceLock<Timestamp>,
181}
182
183impl SpanEventSelfTime {
184    pub fn end(&self) -> Timestamp {
185        Timestamp::from_value(*self.start + self.duration.get())
186    }
187}
188
189pub enum SpanEvent {
190    SelfTime(SpanEventSelfTime),
191    Child { start: Timestamp, index: SpanIndex },
192}
193
194// 32 bytes = 8 (start) + 8 (duration) + 16 (OnceLock<Timestamp>) for the
195// SelfTime variant; the Child variant fits in 16 and uses the niche, so no
196// extra discriminant byte is needed.
197const _: () = assert!(std::mem::size_of::<SpanEvent>() == 32);
198
199impl SpanEvent {
200    /// Constructs a `SelfTime` event from start and end timestamps. Returns `None`
201    /// if `end <= start` (zero or negative duration).
202    pub fn self_time(start: Timestamp, end: Timestamp) -> Option<Self> {
203        let duration = NonZeroU64::new(*end.saturating_sub(start))?;
204        Some(SpanEvent::SelfTime(SpanEventSelfTime {
205            start,
206            duration,
207            corrected_self_time: OnceLock::new(),
208        }))
209    }
210
211    pub fn start(&self) -> Timestamp {
212        match self {
213            SpanEvent::SelfTime(self_time) => self_time.start,
214            SpanEvent::Child { start, .. } => *start,
215        }
216    }
217}
218
219impl PartialEq for SpanEvent {
220    fn eq(&self, other: &Self) -> bool {
221        self.cmp(other) == std::cmp::Ordering::Equal
222    }
223}
224
225impl Eq for SpanEvent {}
226
227impl PartialOrd for SpanEvent {
228    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
229        Some(self.cmp(other))
230    }
231}
232
233impl Ord for SpanEvent {
234    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
235        self.start()
236            .cmp(&other.start())
237            .then_with(|| match (self, other) {
238                (SpanEvent::SelfTime(_), SpanEvent::Child { .. }) => std::cmp::Ordering::Less,
239                (SpanEvent::Child { .. }, SpanEvent::SelfTime(_)) => std::cmp::Ordering::Greater,
240                (SpanEvent::SelfTime(a), SpanEvent::SelfTime(b)) => a.duration.cmp(&b.duration),
241                (
242                    SpanEvent::Child { start: _, index: a },
243                    SpanEvent::Child { start: _, index: b },
244                ) => a.cmp(b),
245            })
246    }
247}
248
249#[derive(Clone)]
250pub enum SpanGraphEvent {
251    // TODO(sokra) use events instead of children for visualizing span graphs
252    #[allow(dead_code)]
253    SelfTime {
254        duration: Timestamp,
255    },
256    Child {
257        child: Arc<SpanGraph>,
258    },
259}
260
261pub struct SpanGraph {
262    // These values won't change after creation:
263    pub root_spans: Vec<SpanIndex>,
264    pub recursive_spans: Vec<SpanIndex>,
265
266    // These values are computed when accessed:
267    pub max_depth: OnceLock<u32>,
268    pub events: OnceLock<Vec<SpanGraphEvent>>,
269    pub self_time: OnceLock<Timestamp>,
270    pub self_allocations: OnceLock<u64>,
271    pub self_deallocations: OnceLock<u64>,
272    pub self_persistent_allocations: OnceLock<u64>,
273    pub self_allocation_count: OnceLock<u64>,
274    pub total_time: OnceLock<Timestamp>,
275    pub total_allocations: OnceLock<u64>,
276    pub total_deallocations: OnceLock<u64>,
277    pub total_persistent_allocations: OnceLock<u64>,
278    pub total_allocation_count: OnceLock<u64>,
279    pub total_span_count: OnceLock<u64>,
280    pub corrected_self_time: OnceLock<Timestamp>,
281    pub corrected_total_time: OnceLock<Timestamp>,
282    pub bottom_up: OnceLock<Vec<Arc<SpanBottomUp>>>,
283}
284
285pub struct SpanBottomUp {
286    // These values won't change after creation:
287    pub self_spans: Vec<SpanIndex>,
288    pub children: Vec<Arc<SpanBottomUp>>,
289    pub example_span: SpanIndex,
290
291    // These values are computed when accessed:
292    pub max_depth: OnceLock<u32>,
293    pub events: OnceLock<Vec<SpanGraphEvent>>,
294    pub self_time: OnceLock<Timestamp>,
295    pub corrected_self_time: OnceLock<Timestamp>,
296    pub self_allocations: OnceLock<u64>,
297    pub self_deallocations: OnceLock<u64>,
298    pub self_persistent_allocations: OnceLock<u64>,
299    pub self_allocation_count: OnceLock<u64>,
300}
301
302impl SpanBottomUp {
303    pub fn new(
304        self_spans: Vec<SpanIndex>,
305        example_span: SpanIndex,
306        children: Vec<Arc<SpanBottomUp>>,
307    ) -> Self {
308        Self {
309            self_spans,
310            children,
311            example_span,
312            max_depth: OnceLock::new(),
313            events: OnceLock::new(),
314            self_time: OnceLock::new(),
315            corrected_self_time: OnceLock::new(),
316            self_allocations: OnceLock::new(),
317            self_deallocations: OnceLock::new(),
318            self_persistent_allocations: OnceLock::new(),
319            self_allocation_count: OnceLock::new(),
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn span_event_self_time_filters_zero_duration() {
330        let t = Timestamp::from_micros(100);
331        assert!(SpanEvent::self_time(t, t).is_none());
332        // end < start should also return None (saturating_sub clamps to 0).
333        assert!(SpanEvent::self_time(t, Timestamp::from_micros(50)).is_none());
334    }
335
336    #[test]
337    fn span_event_self_time_constructs_positive_duration() {
338        let start = Timestamp::from_micros(100);
339        let end = Timestamp::from_micros(150);
340        let event = SpanEvent::self_time(start, end).unwrap();
341        match event {
342            SpanEvent::SelfTime(self_time) => {
343                assert_eq!(self_time.start, start);
344                assert_eq!(self_time.duration.get(), *end - *start);
345                assert_eq!(self_time.end(), end);
346            }
347            SpanEvent::Child { .. } => panic!("expected SelfTime"),
348        }
349    }
350
351    #[test]
352    fn span_event_size_is_packed() {
353        // Backstop for the const assert; if this fails the const assert above
354        // would also fail, but having a test gives a clearer error message.
355        assert_eq!(std::mem::size_of::<SpanEvent>(), 32);
356    }
357}