Skip to main content

turbopack_trace_server/
lib.rs

1#![feature(box_patterns)]
2#![feature(bufreader_peek)]
3
4use std::{
5    hash::BuildHasherDefault,
6    path::PathBuf,
7    sync::Arc,
8    thread,
9    time::{Duration, Instant},
10};
11
12use rustc_hash::FxHasher;
13
14use self::{
15    reader::TraceReader, server::serve, span_graph_ref::SpanGraphEventRef, span_ref::SpanRef,
16    store_container::StoreContainer,
17};
18
19mod bottom_up;
20mod chunked_vec;
21mod lazy_sorted_vec;
22mod reader;
23mod self_time_tree;
24mod server;
25mod span;
26mod span_bottom_up_ref;
27mod span_graph_ref;
28mod span_ref;
29mod store;
30pub mod store_container;
31mod string_tuple_ref;
32mod timestamp;
33mod u64_empty_string;
34mod u64_string;
35mod viewer;
36
37#[allow(
38    dead_code,
39    reason = "It's actually used, not sure why it is marked as dead code"
40)]
41type FxIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<FxHasher>>;
42
43/// Starts the trace server on a background thread and returns the store
44/// immediately. The WebSocket server runs non-blocking.
45pub fn start_turbopack_trace_server(path: PathBuf, port: Option<u16>) -> Arc<StoreContainer> {
46    let store = Arc::new(StoreContainer::new());
47
48    let store_for_reader = store.clone();
49    let store_for_server = store.clone();
50
51    TraceReader::spawn(store_for_reader, path);
52
53    thread::spawn(move || {
54        serve(store_for_server, port.unwrap_or(5747));
55    });
56
57    store
58}
59
60const PAGE_SIZE: usize = 20;
61
62/// How spans should be sorted.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum SortMode {
65    /// No sorting — spans appear in execution/natural order.
66    #[default]
67    ExecutionOrder,
68    /// Sort by value (corrected duration descending).
69    Value,
70    /// Sort alphabetically by name, then by category.
71    Name,
72}
73
74/// Options for querying spans from the trace store.
75pub struct QueryOptions {
76    /// Optional parent span ID (as produced by `SpanInfo::id`).
77    /// `None` means root level.
78    pub parent: Option<String>,
79    /// When true, aggregate child spans with the same name.
80    pub aggregated: bool,
81    /// How to sort the results.
82    pub sort: SortMode,
83    /// Optional substring search query.
84    pub search: Option<String>,
85    /// 1-based page number.
86    pub page: usize,
87}
88
89/// Information about a single span (or aggregated group of spans).
90pub struct SpanInfo {
91    /// Span ID string.
92    ///
93    /// The format encodes both the type and the navigation path:
94    /// - A **raw span** leaf is its decimal index: `"123"`.
95    /// - An **aggregated span** leaf is `"a"` + the first-span index: `"a123"`.
96    /// - When the span is a child of another span, the parent's ID is prepended with a dash
97    ///   separator, e.g. `"a5-a34"` or `"1-a5-a34-20"`.
98    ///
99    /// Pass the full ID as the `parent` option of the next `query_spans` call
100    /// to enumerate the children of that span.
101    pub id: String,
102    /// Display name: `"category title"` or just `"title"`.
103    pub name: String,
104    /// Raw CPU total time in internal ticks (100 ticks = 1 µs).
105    /// For aggregated spans, this is the **first (example) span's** value, not the group total.
106    /// See `total_cpu_duration` for the group total.
107    pub cpu_duration: u64,
108    /// Concurrency-corrected total time in internal ticks.
109    /// For aggregated spans, this is the **first (example) span's** value, not the group total.
110    /// See `total_corrected_duration` for the group total.
111    pub corrected_duration: u64,
112    /// Start of span relative to parent start, in internal ticks.
113    pub start_relative_to_parent: i64,
114    /// End of span relative to parent start, in internal ticks.
115    pub end_relative_to_parent: i64,
116    /// Key-value attributes from the span.
117    pub args: Vec<(String, String)>,
118    /// True if this entry represents an aggregated group of spans.
119    pub is_aggregated: bool,
120    /// Number of spans in the group (only set for aggregated spans).
121    pub count: Option<u64>,
122    /// Sum of cpu_duration across all spans in the group.
123    pub total_cpu_duration: Option<u64>,
124    /// Average cpu_duration across all spans in the group.
125    pub avg_cpu_duration: Option<u64>,
126    /// Sum of corrected_duration across all spans in the group.
127    pub total_corrected_duration: Option<u64>,
128    /// Average corrected_duration across all spans in the group.
129    pub avg_corrected_duration: Option<u64>,
130    /// Raw span ID for aggregated groups (the index of the first span).
131    pub first_span_id: Option<String>,
132}
133
134/// Result of a `query_spans` call.
135pub struct QueryResult {
136    pub spans: Vec<SpanInfo>,
137    pub page: usize,
138    pub total_pages: usize,
139    pub total_count: usize,
140}
141
142/// Paginate a vec of items. Returns `(page_items, clamped_page, total_pages, total_count)`.
143fn paginate<T>(items: Vec<T>, page: usize) -> (Vec<T>, usize, usize, usize) {
144    let total_count = items.len();
145    let total_pages = total_count.div_ceil(PAGE_SIZE).max(1);
146    let page = page.clamp(1, total_pages);
147    let start = (page - 1) * PAGE_SIZE;
148    let page_items = items.into_iter().skip(start).take(PAGE_SIZE).collect();
149    (page_items, page, total_pages, total_count)
150}
151
152fn format_span_name(cat: &str, title: &str) -> String {
153    if cat.is_empty() {
154        title.to_string()
155    } else {
156        format!("{cat} {title}")
157    }
158}
159
160/// Build a span ID by appending a leaf segment to the optional parent path.
161fn build_span_id(parent: Option<&str>, leaf: &str) -> String {
162    match parent {
163        Some(p) => format!("{p}-{leaf}"),
164        None => leaf.to_string(),
165    }
166}
167
168/// Query spans from the store.
169///
170/// Waits up to 10 seconds for at least some data to be loaded before
171/// returning, so callers don't need to poll separately.
172pub fn query_spans(store: &Arc<StoreContainer>, options: QueryOptions) -> QueryResult {
173    // Wait briefly for initial data if the store is empty.
174    let deadline = Instant::now() + Duration::from_secs(10);
175    loop {
176        {
177            let guard = store.read();
178            // root span always exists (index 0); real spans start at index 1
179            if guard.spans.len() > 1 {
180                break;
181            }
182        }
183        if Instant::now() >= deadline {
184            break;
185        }
186        thread::sleep(Duration::from_millis(50));
187    }
188
189    let store_guard = store.read();
190    let store_ref = &*store_guard;
191
192    // Resolve the parent span.
193    let parent_span: Option<SpanRef<'_>> = if let Some(ref parent_id) = options.parent {
194        resolve_span_by_id(store_ref, parent_id)
195    } else {
196        None
197    };
198
199    let parent_start = parent_span.as_ref().map(|s| *s.start()).unwrap_or_default();
200
201    if options.aggregated {
202        // Collect aggregated children (SpanGraphRef) from either the resolved
203        // parent span or the root.
204        let graph_children: Vec<_> = if let Some(ref parent) = parent_span {
205            // The parent might be an aggregated node: look up which graph node
206            // the parent ID refers to, then iterate its children's graph events.
207            // For simplicity, resolve via the parent span's graph().
208            parent
209                .graph()
210                .filter_map(|event| match event {
211                    SpanGraphEventRef::Child { graph } => Some(graph),
212                    SpanGraphEventRef::SelfTime { .. } => None,
213                })
214                .collect()
215        } else {
216            // Root level: use root span's graph.
217            store_ref
218                .root_span()
219                .graph()
220                .filter_map(|event| match event {
221                    SpanGraphEventRef::Child { graph } => Some(graph),
222                    SpanGraphEventRef::SelfTime { .. } => None,
223                })
224                .collect()
225        };
226
227        // Apply search filter.
228        let mut filtered: Vec<_> = if let Some(ref query) = options.search {
229            graph_children
230                .into_iter()
231                .filter(|g| {
232                    let (cat, title) = g.nice_name();
233                    cat.contains(query.as_str()) || title.contains(query.as_str())
234                })
235                .collect()
236        } else {
237            graph_children
238        };
239
240        // Sort if requested.
241        match options.sort {
242            SortMode::Value => {
243                filtered.sort_by(|a, b| {
244                    b.corrected_total_time()
245                        .cmp(&a.corrected_total_time())
246                        .then_with(|| b.total_time().cmp(&a.total_time()))
247                });
248            }
249            SortMode::Name => {
250                filtered.sort_by(|a, b| {
251                    let (a_cat, a_title) = a.nice_name();
252                    let (b_cat, b_title) = b.nice_name();
253                    a_title.cmp(b_title).then_with(|| a_cat.cmp(b_cat))
254                });
255            }
256            SortMode::ExecutionOrder => {}
257        }
258
259        let (page_items, page, total_pages, total_count) = paginate(filtered, options.page);
260
261        let spans = page_items
262            .into_iter()
263            .map(|graph| {
264                let first = graph.first_span();
265                let (cat, title) = graph.nice_name();
266                let name = format_span_name(cat, title);
267                let count = graph.count() as u64;
268                let total_cpu = *graph.total_time();
269                let total_corrected = *graph.corrected_total_time();
270                let avg_cpu = total_cpu.checked_div(count).unwrap_or(0);
271                let avg_corrected = total_corrected.checked_div(count).unwrap_or(0);
272
273                // Build the full path ID for this aggregated span.
274                // The leaf segment is "a{first_span_index}"; prepend the parent
275                // path (if any) with a dash so callers can pass the full string
276                // back as `parent` to drill into children.
277                let first_index = first.index;
278                let graph_id = build_span_id(options.parent.as_deref(), &format!("a{first_index}"));
279
280                // start/end of the first/example span relative to parent.
281                let span_start = *first.start();
282                let span_end = *first.end();
283                let rel_start = (span_start as i64) - (parent_start as i64);
284                let rel_end = (span_end as i64) - (parent_start as i64);
285
286                SpanInfo {
287                    id: graph_id,
288                    name,
289                    cpu_duration: *first.total_time(),
290                    corrected_duration: *first.corrected_total_time(),
291                    start_relative_to_parent: rel_start,
292                    end_relative_to_parent: rel_end,
293                    args: first
294                        .args()
295                        .map(|(k, v)| (k.to_string(), v.to_string()))
296                        .collect(),
297                    is_aggregated: count > 1,
298                    count: Some(count),
299                    total_cpu_duration: Some(total_cpu),
300                    avg_cpu_duration: Some(avg_cpu),
301                    total_corrected_duration: Some(total_corrected),
302                    avg_corrected_duration: Some(avg_corrected),
303                    first_span_id: Some(first_index.to_string()),
304                }
305            })
306            .collect();
307
308        QueryResult {
309            spans,
310            page,
311            total_pages,
312            total_count,
313        }
314    } else {
315        // Raw spans mode.
316        let raw_children: Vec<SpanRef<'_>> = if let Some(ref parent) = parent_span {
317            parent.children().collect()
318        } else {
319            store_ref.root_spans().collect()
320        };
321
322        // Apply search filter using the span's search index.
323        let mut filtered: Vec<_> = if let Some(ref query) = options.search {
324            if let Some(ref parent) = parent_span {
325                parent.search(query).collect()
326            } else {
327                store_ref.root_span().search(query).collect()
328            }
329        } else {
330            raw_children
331        };
332
333        // Sort if requested.
334        match options.sort {
335            SortMode::Value => {
336                filtered.sort_by(|a, b| {
337                    b.corrected_total_time()
338                        .cmp(&a.corrected_total_time())
339                        .then_with(|| b.total_time().cmp(&a.total_time()))
340                });
341            }
342            SortMode::Name => {
343                filtered.sort_by(|a, b| {
344                    let (a_cat, a_title) = a.nice_name();
345                    let (b_cat, b_title) = b.nice_name();
346                    a_title.cmp(b_title).then_with(|| a_cat.cmp(b_cat))
347                });
348            }
349            SortMode::ExecutionOrder => {}
350        }
351
352        let (page_items, page, total_pages, total_count) = paginate(filtered, options.page);
353
354        let spans = page_items
355            .into_iter()
356            .map(|span| {
357                let (cat, title) = span.nice_name();
358                let name = format_span_name(cat, title);
359                let span_start = *span.start();
360                let span_end = *span.end();
361                let rel_start = (span_start as i64) - (parent_start as i64);
362                let rel_end = (span_end as i64) - (parent_start as i64);
363
364                SpanInfo {
365                    id: build_span_id(options.parent.as_deref(), &span.index.to_string()),
366                    name,
367                    cpu_duration: *span.total_time(),
368                    corrected_duration: *span.corrected_total_time(),
369                    start_relative_to_parent: rel_start,
370                    end_relative_to_parent: rel_end,
371                    args: span
372                        .args()
373                        .map(|(k, v)| (k.to_string(), v.to_string()))
374                        .collect(),
375                    is_aggregated: false,
376                    count: None,
377                    total_cpu_duration: None,
378                    avg_cpu_duration: None,
379                    total_corrected_duration: None,
380                    avg_corrected_duration: None,
381                    first_span_id: None,
382                }
383            })
384            .collect();
385
386        QueryResult {
387            spans,
388            page,
389            total_pages,
390            total_count,
391        }
392    }
393}
394
395/// Resolve a span by its MCP ID string.
396///
397/// IDs use the format `[a]<index>[-[a]<index>...]`:
398/// - A plain decimal segment (e.g. `"123"`) refers to a raw span at that store index.
399/// - A segment prefixed with `"a"` (e.g. `"a123"`) refers to the first span of an aggregated group
400///   at that store index.
401/// - Segments are separated by `-` to form a navigation path, e.g. `"a5-a34-20"`. Only the **last**
402///   segment is needed to look up the span whose children we want to enumerate; the earlier
403///   segments provide navigation context for the caller.
404fn resolve_span_by_id<'a>(store: &'a store::Store, id: &str) -> Option<SpanRef<'a>> {
405    // Take only the last path segment (everything after the final `-`).
406    let last = id.split('-').next_back().unwrap_or(id);
407    // Strip the optional "a" prefix that marks aggregated spans.
408    let index_str = last.strip_prefix('a').unwrap_or(last);
409    let index: usize = index_str.parse().ok()?;
410    store.spans.get(index).map(|s| SpanRef {
411        span: s,
412        store,
413        index,
414    })
415}