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    /// TurboMalloc memory-usage samples recorded while this span (or its
133    /// example span, for aggregated groups) was live.
134    ///
135    /// Each tuple is `(ts_offset_from_span_start_in_ticks, bytes, pressure)`,
136    /// where `pressure` is the memory-pressure byte recorded with the sample
137    /// (0 = no pressure, higher = more pressure). `100 ticks = 1 µs`. The
138    /// offset is always `>= 0` and `<= span_duration`.
139    ///
140    /// The store caps the series at `MAX_MEMORY_SAMPLES`; when more samples
141    /// exist in the range, consecutive groups are merged by picking the
142    /// group's max-memory sample (timestamp, value, and pressure kept
143    /// together).
144    pub memory_samples: Vec<(i64, u64, u8)>,
145}
146
147/// Result of a `query_spans` call.
148pub struct QueryResult {
149    pub spans: Vec<SpanInfo>,
150    pub page: usize,
151    pub total_pages: usize,
152    pub total_count: usize,
153}
154
155/// Paginate a vec of items. Returns `(page_items, clamped_page, total_pages, total_count)`.
156fn paginate<T>(items: Vec<T>, page: usize) -> (Vec<T>, usize, usize, usize) {
157    let total_count = items.len();
158    let total_pages = total_count.div_ceil(PAGE_SIZE).max(1);
159    let page = page.clamp(1, total_pages);
160    let start = (page - 1) * PAGE_SIZE;
161    let page_items = items.into_iter().skip(start).take(PAGE_SIZE).collect();
162    (page_items, page, total_pages, total_count)
163}
164
165fn format_span_name(cat: &str, title: &str) -> String {
166    if cat.is_empty() {
167        title.to_string()
168    } else {
169        format!("{cat} {title}")
170    }
171}
172
173/// Build a span ID by appending a leaf segment to the optional parent path.
174fn build_span_id(parent: Option<&str>, leaf: &str) -> String {
175    match parent {
176        Some(p) => format!("{p}-{leaf}"),
177        None => leaf.to_string(),
178    }
179}
180
181/// Query spans from the store.
182///
183/// Waits up to 10 seconds for at least some data to be loaded before
184/// returning, so callers don't need to poll separately.
185pub fn query_spans(store: &Arc<StoreContainer>, options: QueryOptions) -> QueryResult {
186    // Wait briefly for initial data if the store is empty.
187    let deadline = Instant::now() + Duration::from_secs(10);
188    loop {
189        {
190            let guard = store.read();
191            // root span always exists (index 0); real spans start at index 1
192            if guard.spans.len() > 1 {
193                break;
194            }
195        }
196        if Instant::now() >= deadline {
197            break;
198        }
199        thread::sleep(Duration::from_millis(50));
200    }
201
202    let store_guard = store.read();
203    let store_ref = &*store_guard;
204
205    // Resolve the parent span.
206    let parent_span: Option<SpanRef<'_>> = if let Some(ref parent_id) = options.parent {
207        resolve_span_by_id(store_ref, parent_id)
208    } else {
209        None
210    };
211
212    let parent_start = parent_span.as_ref().map(|s| *s.start()).unwrap_or_default();
213
214    if options.aggregated {
215        // Collect aggregated children (SpanGraphRef) from either the resolved
216        // parent span or the root.
217        let graph_children: Vec<_> = if let Some(ref parent) = parent_span {
218            // The parent might be an aggregated node: look up which graph node
219            // the parent ID refers to, then iterate its children's graph events.
220            // For simplicity, resolve via the parent span's graph().
221            parent
222                .graph()
223                .filter_map(|event| match event {
224                    SpanGraphEventRef::Child { graph } => Some(graph),
225                    SpanGraphEventRef::SelfTime { .. } => None,
226                })
227                .collect()
228        } else {
229            // Root level: use root span's graph.
230            store_ref
231                .root_span()
232                .graph()
233                .filter_map(|event| match event {
234                    SpanGraphEventRef::Child { graph } => Some(graph),
235                    SpanGraphEventRef::SelfTime { .. } => None,
236                })
237                .collect()
238        };
239
240        // Apply search filter.
241        let mut filtered: Vec<_> = if let Some(ref query) = options.search {
242            graph_children
243                .into_iter()
244                .filter(|g| {
245                    let (cat, title) = g.nice_name();
246                    cat.contains(query.as_str()) || title.contains(query.as_str())
247                })
248                .collect()
249        } else {
250            graph_children
251        };
252
253        // Sort if requested.
254        match options.sort {
255            SortMode::Value => {
256                filtered.sort_by(|a, b| {
257                    b.corrected_total_time()
258                        .cmp(&a.corrected_total_time())
259                        .then_with(|| b.total_time().cmp(&a.total_time()))
260                });
261            }
262            SortMode::Name => {
263                filtered.sort_by(|a, b| {
264                    let (a_cat, a_title) = a.nice_name();
265                    let (b_cat, b_title) = b.nice_name();
266                    a_title.cmp(b_title).then_with(|| a_cat.cmp(b_cat))
267                });
268            }
269            SortMode::ExecutionOrder => {}
270        }
271
272        let (page_items, page, total_pages, total_count) = paginate(filtered, options.page);
273
274        let spans = page_items
275            .into_iter()
276            .map(|graph| {
277                let first = graph.first_span();
278                let (cat, title) = graph.nice_name();
279                let name = format_span_name(cat, title);
280                let count = graph.count() as u64;
281                let total_cpu = *graph.total_time();
282                let total_corrected = *graph.corrected_total_time();
283                let avg_cpu = total_cpu.checked_div(count).unwrap_or(0);
284                let avg_corrected = total_corrected.checked_div(count).unwrap_or(0);
285
286                // Build the full path ID for this aggregated span.
287                // The leaf segment is "a{first_span_index}"; prepend the parent
288                // path (if any) with a dash so callers can pass the full string
289                // back as `parent` to drill into children.
290                let first_index = first.index;
291                let graph_id = build_span_id(options.parent.as_deref(), &format!("a{first_index}"));
292
293                // start/end of the first/example span relative to parent.
294                let span_start = *first.start();
295                let span_end = *first.end();
296                let rel_start = (span_start as i64) - (parent_start as i64);
297                let rel_end = (span_end as i64) - (parent_start as i64);
298
299                let first_start_ticks = *first.start();
300                let memory_samples: Vec<(i64, u64, u8)> = store_ref
301                    .memory_samples_for_range_with_ts(first.start(), first.end())
302                    .into_iter()
303                    .map(|(ts, mem, pressure)| {
304                        ((*ts as i64) - (first_start_ticks as i64), mem, pressure)
305                    })
306                    .collect();
307
308                SpanInfo {
309                    id: graph_id,
310                    name,
311                    cpu_duration: *first.total_time(),
312                    corrected_duration: *first.corrected_total_time(),
313                    start_relative_to_parent: rel_start,
314                    end_relative_to_parent: rel_end,
315                    args: first
316                        .args()
317                        .map(|(k, v)| (k.to_string(), v.to_string()))
318                        .collect(),
319                    is_aggregated: count > 1,
320                    count: Some(count),
321                    total_cpu_duration: Some(total_cpu),
322                    avg_cpu_duration: Some(avg_cpu),
323                    total_corrected_duration: Some(total_corrected),
324                    avg_corrected_duration: Some(avg_corrected),
325                    first_span_id: Some(first_index.to_string()),
326                    memory_samples,
327                }
328            })
329            .collect();
330
331        QueryResult {
332            spans,
333            page,
334            total_pages,
335            total_count,
336        }
337    } else {
338        // Raw spans mode.
339        let raw_children: Vec<SpanRef<'_>> = if let Some(ref parent) = parent_span {
340            parent.children().collect()
341        } else {
342            store_ref.root_spans().collect()
343        };
344
345        // Apply search filter using the span's search index.
346        let mut filtered: Vec<_> = if let Some(ref query) = options.search {
347            if let Some(ref parent) = parent_span {
348                parent.search(query).collect()
349            } else {
350                store_ref.root_span().search(query).collect()
351            }
352        } else {
353            raw_children
354        };
355
356        // Sort if requested.
357        match options.sort {
358            SortMode::Value => {
359                filtered.sort_by(|a, b| {
360                    b.corrected_total_time()
361                        .cmp(&a.corrected_total_time())
362                        .then_with(|| b.total_time().cmp(&a.total_time()))
363                });
364            }
365            SortMode::Name => {
366                filtered.sort_by(|a, b| {
367                    let (a_cat, a_title) = a.nice_name();
368                    let (b_cat, b_title) = b.nice_name();
369                    a_title.cmp(b_title).then_with(|| a_cat.cmp(b_cat))
370                });
371            }
372            SortMode::ExecutionOrder => {}
373        }
374
375        let (page_items, page, total_pages, total_count) = paginate(filtered, options.page);
376
377        let spans = page_items
378            .into_iter()
379            .map(|span| {
380                let (cat, title) = span.nice_name();
381                let name = format_span_name(cat, title);
382                let span_start = *span.start();
383                let span_end = *span.end();
384                let rel_start = (span_start as i64) - (parent_start as i64);
385                let rel_end = (span_end as i64) - (parent_start as i64);
386
387                let raw_span_start = span_start;
388                let memory_samples: Vec<(i64, u64, u8)> = store_ref
389                    .memory_samples_for_range_with_ts(span.start(), span.end())
390                    .into_iter()
391                    .map(|(ts, mem, pressure)| {
392                        ((*ts as i64) - (raw_span_start as i64), mem, pressure)
393                    })
394                    .collect();
395
396                SpanInfo {
397                    id: build_span_id(options.parent.as_deref(), &span.index.to_string()),
398                    name,
399                    cpu_duration: *span.total_time(),
400                    corrected_duration: *span.corrected_total_time(),
401                    start_relative_to_parent: rel_start,
402                    end_relative_to_parent: rel_end,
403                    args: span
404                        .args()
405                        .map(|(k, v)| (k.to_string(), v.to_string()))
406                        .collect(),
407                    is_aggregated: false,
408                    count: None,
409                    total_cpu_duration: None,
410                    avg_cpu_duration: None,
411                    total_corrected_duration: None,
412                    avg_corrected_duration: None,
413                    first_span_id: None,
414                    memory_samples,
415                }
416            })
417            .collect();
418
419        QueryResult {
420            spans,
421            page,
422            total_pages,
423            total_count,
424        }
425    }
426}
427
428/// Resolve a span by its MCP ID string.
429///
430/// IDs use the format `[a]<index>[-[a]<index>...]`:
431/// - A plain decimal segment (e.g. `"123"`) refers to a raw span at that store index.
432/// - A segment prefixed with `"a"` (e.g. `"a123"`) refers to the first span of an aggregated group
433///   at that store index.
434/// - Segments are separated by `-` to form a navigation path, e.g. `"a5-a34-20"`. Only the **last**
435///   segment is needed to look up the span whose children we want to enumerate; the earlier
436///   segments provide navigation context for the caller.
437fn resolve_span_by_id<'a>(store: &'a store::Store, id: &str) -> Option<SpanRef<'a>> {
438    // Take only the last path segment (everything after the final `-`).
439    let last = id.split('-').next_back().unwrap_or(id);
440    // Strip the optional "a" prefix that marks aggregated spans.
441    let index_str = last.strip_prefix('a').unwrap_or(last);
442    let index: usize = index_str.parse().ok()?;
443    store.spans.get(index).map(|s| SpanRef {
444        span: s,
445        store,
446        index,
447    })
448}