next.js/turbopack/crates/turbopack-trace-server/src/span.rs
span.rs358 lines11.6 KB
use std::{
    num::{NonZeroU64, NonZeroUsize},
    sync::{Arc, OnceLock},
};

use hashbrown::HashMap;
use smallvec::SmallVec;
use turbo_rcstr::RcStr;

use crate::{lazy_sorted_vec::LazySortedVec, timestamp::Timestamp};

pub type SpanIndex = NonZeroUsize;

/// Storage for `Span::args` ~32% of spans have <=1 arg (typically just the
/// `name` key for `turbo_tasks::function` spans), so inlining one entry
/// avoids a heap allocation in this common case.
pub type SpanArgs = SmallVec<[(RcStr, RcStr); 1]>;

pub struct Span {
    // These values won't change after creation:
    pub parent: Option<SpanIndex>,
    pub depth: u32,
    pub start: Timestamp,
    pub category: RcStr,
    pub name: RcStr,
    pub args: SpanArgs,

    // This might change during writing:
    /// The list of events sorted by start time. Backed by a SmallVec so leaf
    /// spans (~69%, typically just one self-time event) don't pay a heap
    /// allocation.
    pub events: LazySortedVec<SpanEvent>,
    pub is_complete: bool,

    // These values are computed automatically:
    pub self_allocations: u64,
    pub self_allocation_count: u64,
    pub self_deallocations: u64,
    pub self_deallocation_count: u64,

    // These values are computed when accessed (and maybe deleted during writing).
    // Bundling the subtree totals into a single OnceLock pays a small cost on
    // partial reads in exchange for a much-reduced lock count per Span.
    pub totals: OnceLock<SpanTotals>,
    pub time_data: SpanTimeData,
    pub extra: OnceLock<Box<SpanExtra>>,
    /// Lazy first-touch via `OnceLock`, but inline rather than boxed: ~96% of
    /// spans get names populated after browsing, never invalidated, so the box
    /// indirection is pure overhead.
    pub names: OnceLock<SpanNames>,
}

#[derive(Default)]
pub struct SpanTotals {
    pub max_depth: u32,
    pub allocations: u64,
    pub deallocations: u64,
    pub persistent_allocations: u64,
    pub allocation_count: u64,
    pub span_count: u64,
}

#[derive(Default)]
pub struct SpanTimeData {
    // These values won't change after creation:
    pub ignore_self_time: bool,

    // This might change during writing:
    pub self_end: Timestamp,

    // These values are computed automatically:
    pub self_time: Timestamp,

    // These values are computed when accessed (and maybe deleted during writing):
    pub end: OnceLock<Timestamp>,
    pub total_time: OnceLock<Timestamp>,
    pub corrected_self_time: OnceLock<Timestamp>,
    pub corrected_total_time: OnceLock<Timestamp>,
}

#[derive(Default)]
pub struct SpanExtra {
    pub graph: OnceLock<Vec<SpanGraphEvent>>,
    pub bottom_up: OnceLock<Vec<Arc<SpanBottomUp>>>,
    pub search_index: OnceLock<HashMap<RcStr, Vec<SpanIndex>>>,
}

#[derive(Clone)]
pub struct SpanName {
    pub category: RcStr,
    pub title: RcStr,
}

pub struct SpanNames {
    pub nice_name: SpanName,
    pub group_name: SpanName,
}

impl Span {
    pub fn extra(&self) -> &SpanExtra {
        self.extra.get_or_init(Default::default)
    }

    pub fn names(&self) -> &SpanNames {
        self.names.get_or_init(|| self.compute_names())
    }

    fn compute_names(&self) -> SpanNames {
        // Classify the span. `turbo_tasks::function` and the resolve-call spans
        // get special-cased rendering when they carry a `name` arg; everything
        // else is rendered generically.
        enum Kind {
            Function,
            Resolve,
            Other,
        }
        let kind = match self.name.as_str() {
            "turbo_tasks::function" => Kind::Function,
            "turbo_tasks::resolve_call" | "turbo_tasks::resolve_trait_call" => Kind::Resolve,
            _ => Kind::Other,
        };
        let arg_name = self.args.iter().find(|&(k, _)| k == "name").map(|(_, v)| v);

        // Generic fallback used by both names whenever no special case applies.
        let generic = || SpanName {
            category: self.category.clone(),
            title: self.name.clone(),
        };

        // Each arm constructs the full `SpanNames` so the relationship between
        // `nice_name` and `group_name` is visible at a glance. The `Some(n)`
        // rows handle the "this span carries a `name` arg" case; the `None`
        // arm falls back to the generic shape for both names — including for
        // function/resolve spans, which (in practice) always carry a name arg,
        // so the fallback is mostly defensive.
        match (kind, arg_name) {
            (Kind::Function, Some(n)) => {
                let pretty = SpanName {
                    category: self.name.clone(),
                    title: n.clone(),
                };
                SpanNames {
                    nice_name: pretty.clone(),
                    group_name: pretty,
                }
            }
            (Kind::Resolve, Some(n)) => SpanNames {
                nice_name: SpanName {
                    category: self.name.clone(),
                    title: format!("*{n}").into(),
                },
                group_name: SpanName {
                    category: self.category.clone(),
                    title: format!("{} *{n}", self.name).into(),
                },
            },
            (Kind::Other, Some(n)) => SpanNames {
                nice_name: SpanName {
                    category: self.category.clone(),
                    title: format!("{} {n}", self.name).into(),
                },
                group_name: generic(),
            },
            (_, None) => SpanNames {
                nice_name: generic(),
                group_name: generic(),
            },
        }
    }
}

/// Stores `duration` as `NonZeroU64` so the variant has a niche; combined with
/// `Child`'s `NonZeroUsize` index, this lets the compiler pack `SpanEvent`
/// without a separate discriminant byte (saving 8 bytes per event vs. an
/// `end: Timestamp` layout). Callers must filter zero-duration self-time
/// events before constructing — see [`SpanEvent::self_time`].
pub struct SpanEventSelfTime {
    pub start: Timestamp,
    pub duration: NonZeroU64,
    pub corrected_self_time: OnceLock<Timestamp>,
}

impl SpanEventSelfTime {
    pub fn end(&self) -> Timestamp {
        Timestamp::from_value(*self.start + self.duration.get())
    }
}

pub enum SpanEvent {
    SelfTime(SpanEventSelfTime),
    Child { start: Timestamp, index: SpanIndex },
}

// 32 bytes = 8 (start) + 8 (duration) + 16 (OnceLock<Timestamp>) for the
// SelfTime variant; the Child variant fits in 16 and uses the niche, so no
// extra discriminant byte is needed.
const _: () = assert!(std::mem::size_of::<SpanEvent>() == 32);

impl SpanEvent {
    /// Constructs a `SelfTime` event from start and end timestamps. Returns `None`
    /// if `end <= start` (zero or negative duration).
    pub fn self_time(start: Timestamp, end: Timestamp) -> Option<Self> {
        let duration = NonZeroU64::new(*end.saturating_sub(start))?;
        Some(SpanEvent::SelfTime(SpanEventSelfTime {
            start,
            duration,
            corrected_self_time: OnceLock::new(),
        }))
    }

    pub fn start(&self) -> Timestamp {
        match self {
            SpanEvent::SelfTime(self_time) => self_time.start,
            SpanEvent::Child { start, .. } => *start,
        }
    }
}

impl PartialEq for SpanEvent {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == std::cmp::Ordering::Equal
    }
}

impl Eq for SpanEvent {}

impl PartialOrd for SpanEvent {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for SpanEvent {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.start()
            .cmp(&other.start())
            .then_with(|| match (self, other) {
                (SpanEvent::SelfTime(_), SpanEvent::Child { .. }) => std::cmp::Ordering::Less,
                (SpanEvent::Child { .. }, SpanEvent::SelfTime(_)) => std::cmp::Ordering::Greater,
                (SpanEvent::SelfTime(a), SpanEvent::SelfTime(b)) => a.duration.cmp(&b.duration),
                (
                    SpanEvent::Child { start: _, index: a },
                    SpanEvent::Child { start: _, index: b },
                ) => a.cmp(b),
            })
    }
}

#[derive(Clone)]
pub enum SpanGraphEvent {
    // TODO(sokra) use events instead of children for visualizing span graphs
    #[allow(dead_code)]
    SelfTime {
        duration: Timestamp,
    },
    Child {
        child: Arc<SpanGraph>,
    },
}

pub struct SpanGraph {
    // These values won't change after creation:
    pub root_spans: Vec<SpanIndex>,
    pub recursive_spans: Vec<SpanIndex>,

    // These values are computed when accessed:
    pub max_depth: OnceLock<u32>,
    pub events: OnceLock<Vec<SpanGraphEvent>>,
    pub self_time: OnceLock<Timestamp>,
    pub self_allocations: OnceLock<u64>,
    pub self_deallocations: OnceLock<u64>,
    pub self_persistent_allocations: OnceLock<u64>,
    pub self_allocation_count: OnceLock<u64>,
    pub total_time: OnceLock<Timestamp>,
    pub total_allocations: OnceLock<u64>,
    pub total_deallocations: OnceLock<u64>,
    pub total_persistent_allocations: OnceLock<u64>,
    pub total_allocation_count: OnceLock<u64>,
    pub total_span_count: OnceLock<u64>,
    pub corrected_self_time: OnceLock<Timestamp>,
    pub corrected_total_time: OnceLock<Timestamp>,
    pub bottom_up: OnceLock<Vec<Arc<SpanBottomUp>>>,
}

pub struct SpanBottomUp {
    // These values won't change after creation:
    pub self_spans: Vec<SpanIndex>,
    pub children: Vec<Arc<SpanBottomUp>>,
    pub example_span: SpanIndex,

    // These values are computed when accessed:
    pub max_depth: OnceLock<u32>,
    pub events: OnceLock<Vec<SpanGraphEvent>>,
    pub self_time: OnceLock<Timestamp>,
    pub corrected_self_time: OnceLock<Timestamp>,
    pub self_allocations: OnceLock<u64>,
    pub self_deallocations: OnceLock<u64>,
    pub self_persistent_allocations: OnceLock<u64>,
    pub self_allocation_count: OnceLock<u64>,
}

impl SpanBottomUp {
    pub fn new(
        self_spans: Vec<SpanIndex>,
        example_span: SpanIndex,
        children: Vec<Arc<SpanBottomUp>>,
    ) -> Self {
        Self {
            self_spans,
            children,
            example_span,
            max_depth: OnceLock::new(),
            events: OnceLock::new(),
            self_time: OnceLock::new(),
            corrected_self_time: OnceLock::new(),
            self_allocations: OnceLock::new(),
            self_deallocations: OnceLock::new(),
            self_persistent_allocations: OnceLock::new(),
            self_allocation_count: OnceLock::new(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn span_event_self_time_filters_zero_duration() {
        let t = Timestamp::from_micros(100);
        assert!(SpanEvent::self_time(t, t).is_none());
        // end < start should also return None (saturating_sub clamps to 0).
        assert!(SpanEvent::self_time(t, Timestamp::from_micros(50)).is_none());
    }

    #[test]
    fn span_event_self_time_constructs_positive_duration() {
        let start = Timestamp::from_micros(100);
        let end = Timestamp::from_micros(150);
        let event = SpanEvent::self_time(start, end).unwrap();
        match event {
            SpanEvent::SelfTime(self_time) => {
                assert_eq!(self_time.start, start);
                assert_eq!(self_time.duration.get(), *end - *start);
                assert_eq!(self_time.end(), end);
            }
            SpanEvent::Child { .. } => panic!("expected SelfTime"),
        }
    }

    #[test]
    fn span_event_size_is_packed() {
        // Backstop for the const assert; if this fails the const assert above
        // would also fail, but having a test gives a clearer error message.
        assert_eq!(std::mem::size_of::<SpanEvent>(), 32);
    }
}
Quest for Codev2.0.0
/
SIGN IN