1#![feature(deref_patterns)]
2#![feature(bufreader_peek)]
3
4use std::{
5 cmp::Reverse,
6 collections::VecDeque,
7 hash::BuildHasherDefault,
8 path::PathBuf,
9 sync::Arc,
10 thread,
11 time::{Duration, Instant},
12};
13
14use rustc_hash::FxHasher;
15
16use self::{
17 reader::TraceReader,
18 server::serve,
19 span_graph_ref::{SpanGraphEventRef, SpanGraphRef},
20 span_ref::SpanRef,
21 store_container::StoreContainer,
22 timestamp::Timestamp,
23};
24
25mod bottom_up;
26mod chunked_vec;
27mod lazy_sorted_vec;
28#[cfg(test)]
29mod query_tests;
30mod reader;
31mod self_time_tree;
32mod server;
33mod span;
34mod span_bottom_up_ref;
35mod span_graph_ref;
36mod span_ref;
37mod store;
38pub mod store_container;
39mod string_tuple_ref;
40mod timestamp;
41mod u64_empty_string;
42mod u64_string;
43mod viewer;
44
45#[allow(
46 dead_code,
47 reason = "It's actually used, not sure why it is marked as dead code"
48)]
49type FxIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<FxHasher>>;
50
51pub fn start_turbopack_trace_server(path: PathBuf, port: Option<u16>) -> Arc<StoreContainer> {
54 let store = Arc::new(StoreContainer::new());
55
56 let store_for_reader = store.clone();
57 let store_for_server = store.clone();
58
59 TraceReader::spawn(store_for_reader, path);
60
61 thread::spawn(move || {
62 serve(store_for_server, port.unwrap_or(5747));
63 });
64
65 store
66}
67
68const DEFAULT_PAGE_SIZE: usize = 20;
70
71const MAX_PAGE_SIZE: usize = 500;
73
74const DEFAULT_SEARCH_MAX_DEPTH: u32 = 32;
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum SortMode {
81 #[default]
83 ExecutionOrder,
84 Value,
86 Name,
88 Allocations,
90 PersistentAllocations,
92}
93
94pub struct QueryOptions {
96 pub parent: Option<String>,
99 pub aggregated: bool,
101 pub sort: SortMode,
103 pub search: Option<String>,
113 pub max_depth: u32,
116 pub depth: u32,
119 pub page: usize,
121 pub page_size: Option<usize>,
124}
125
126pub struct SpanInfo {
128 pub id: String,
139 pub name: String,
141 pub cpu_duration: u64,
145 pub corrected_duration: u64,
149 pub start_relative_to_parent: i64,
151 pub end_relative_to_parent: i64,
153 pub args: Vec<(String, String)>,
155 pub is_aggregated: bool,
157 pub count: Option<u64>,
159 pub total_cpu_duration: Option<u64>,
161 pub avg_cpu_duration: Option<u64>,
163 pub total_corrected_duration: Option<u64>,
165 pub avg_corrected_duration: Option<u64>,
167 pub first_span_id: Option<String>,
172 pub heaviest_span_id: Option<String>,
175 pub allocations: u64,
181 pub deallocations: u64,
184 pub persistent_allocations: u64,
196 pub allocation_count: u64,
199 pub self_allocations: u64,
202 pub self_deallocations: u64,
212 pub self_persistent_allocations: u64,
215 pub self_allocation_count: u64,
218 pub memory_samples: Vec<(i64, u64, u8, u64, u64)>,
240 pub memory_summary: Option<MemorySummary>,
242 pub children: Vec<SpanInfo>,
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub struct MemorySummary {
253 pub count: usize,
255 pub start: u64,
257 pub end: u64,
259 pub min: u64,
261 pub peak: u64,
263 pub max_pressure: u8,
265 pub max_footprint: u64,
268}
269
270impl MemorySummary {
271 fn from_samples(samples: &[(i64, u64, u8, u64, u64)]) -> Option<Self> {
273 let (_, first_bytes, first_pressure, first_footprint, _) = *samples.first()?;
274 let mut summary = MemorySummary {
275 count: samples.len(),
276 start: first_bytes,
277 end: samples.last().expect("non-empty").1,
278 min: first_bytes,
279 peak: first_bytes,
280 max_pressure: first_pressure,
281 max_footprint: first_footprint,
282 };
283 for &(_, bytes, pressure, footprint, _) in &samples[1..] {
284 summary.min = summary.min.min(bytes);
285 summary.peak = summary.peak.max(bytes);
286 summary.max_pressure = summary.max_pressure.max(pressure);
287 summary.max_footprint = summary.max_footprint.max(footprint);
288 }
289 Some(summary)
290 }
291}
292
293pub struct QueryResult {
295 pub spans: Vec<SpanInfo>,
296 pub page: usize,
297 pub total_pages: usize,
298 pub total_count: usize,
299}
300fn paginate<T>(items: Vec<T>, page: usize, page_size: usize) -> (Vec<T>, usize, usize, usize) {
302 let page_size = page_size.clamp(1, MAX_PAGE_SIZE);
303 let total_count = items.len();
304 let total_pages = total_count.div_ceil(page_size).max(1);
305 let page = page.clamp(1, total_pages);
306 let start = (page - 1) * page_size;
307 let page_items = items.into_iter().skip(start).take(page_size).collect();
308 (page_items, page, total_pages, total_count)
309}
310
311fn format_span_name(cat: &str, title: &str) -> String {
312 if cat.is_empty() {
313 title.to_string()
314 } else {
315 format!("{cat} {title}")
316 }
317}
318
319fn build_span_id(parent: Option<&str>, leaf: &str) -> String {
321 match parent {
322 Some(p) => format!("{p}-{leaf}"),
323 None => leaf.to_string(),
324 }
325}
326
327struct Located<T> {
329 item: T,
330 id: String,
332}
333
334fn name_matches(cat: &str, title: &str, query: &str) -> bool {
341 query
342 .split(',')
343 .map(str::trim)
344 .filter(|term| !term.is_empty())
345 .all(|term| contains_in_joined(cat, title, term))
346}
347
348fn contains_in_joined(cat: &str, title: &str, term: &str) -> bool {
359 if cat.contains(term) || title.contains(term) {
360 return true;
361 }
362 if cat.is_empty() {
367 return false;
368 }
369 term.match_indices(' ')
370 .any(|(i, _)| cat.ends_with(&term[..i]) && title.starts_with(&term[i + 1..]))
371}
372
373fn graph_children<'a>(graph: &SpanGraphRef<'a>) -> Vec<SpanGraphRef<'a>> {
375 graph
376 .events()
377 .filter_map(|event| match event {
378 SpanGraphEventRef::Child { graph } => Some(graph),
379 SpanGraphEventRef::SelfTime { .. } => None,
380 })
381 .collect()
382}
383
384fn span_graph_children<'a>(span: &SpanRef<'a>) -> Vec<SpanGraphRef<'a>> {
386 span.graph()
387 .filter_map(|event| match event {
388 SpanGraphEventRef::Child { graph } => Some(graph),
389 SpanGraphEventRef::SelfTime { .. } => None,
390 })
391 .collect()
392}
393
394fn search_graph_recursive<'a>(
403 roots: Vec<Located<SpanGraphRef<'a>>>,
404 query: &str,
405 max_depth: u32,
406) -> Vec<Located<SpanGraphRef<'a>>> {
407 let mut matches = Vec::new();
408 let mut queue: VecDeque<(Located<SpanGraphRef<'a>>, u32)> =
409 roots.into_iter().map(|located| (located, 1)).collect();
410
411 while let Some((located, depth)) = queue.pop_front() {
412 let (cat, title) = located.item.nice_name();
413 let is_match = name_matches(cat, title, query);
414
415 if depth < max_depth {
416 for child in graph_children(&located.item) {
417 let leaf = format!("a{}", child.first_span().index);
418 let id = build_span_id(Some(&located.id), &leaf);
419 queue.push_back((Located { item: child, id }, depth + 1));
420 }
421 }
422
423 if is_match {
424 matches.push(located);
425 }
426 }
427
428 matches
429}
430
431fn search_spans_recursive<'a>(
437 roots: Vec<SpanRef<'a>>,
438 query: &str,
439 max_depth: u32,
440) -> Vec<SpanRef<'a>> {
441 let mut matches = Vec::new();
442 let mut queue: VecDeque<(SpanRef<'a>, u32)> = roots.into_iter().map(|span| (span, 1)).collect();
443
444 while let Some((span, depth)) = queue.pop_front() {
445 let (cat, title) = span.nice_name();
446 if name_matches(cat, title, query) {
447 matches.push(span);
448 }
449 if depth < max_depth {
450 for child in span.children() {
451 queue.push_back((child, depth + 1));
452 }
453 }
454 }
455
456 matches
457}
458
459fn sort_graph(items: &mut [Located<SpanGraphRef<'_>>], sort: SortMode) {
461 match sort {
462 SortMode::Value => items.sort_by(|a, b| {
463 b.item
464 .corrected_total_time()
465 .cmp(&a.item.corrected_total_time())
466 .then_with(|| b.item.total_time().cmp(&a.item.total_time()))
467 }),
468 SortMode::Name => items.sort_by(|a, b| {
469 let (a_cat, a_title) = a.item.nice_name();
470 let (b_cat, b_title) = b.item.nice_name();
471 a_title.cmp(b_title).then_with(|| a_cat.cmp(b_cat))
472 }),
473 SortMode::Allocations => {
474 items.sort_by_key(|s| Reverse(s.item.total_allocations()));
475 }
476 SortMode::PersistentAllocations => {
477 items.sort_by_key(|s| Reverse(s.item.total_persistent_allocations()));
478 }
479 SortMode::ExecutionOrder => {}
480 }
481}
482
483fn sort_spans(items: &mut [Located<SpanRef<'_>>], sort: SortMode) {
485 match sort {
486 SortMode::Value => items.sort_by(|a, b| {
487 b.item
488 .corrected_total_time()
489 .cmp(&a.item.corrected_total_time())
490 .then_with(|| b.item.total_time().cmp(&a.item.total_time()))
491 }),
492 SortMode::Name => items.sort_by(|a, b| {
493 let (a_cat, a_title) = a.item.nice_name();
494 let (b_cat, b_title) = b.item.nice_name();
495 a_title.cmp(b_title).then_with(|| a_cat.cmp(b_cat))
496 }),
497 SortMode::Allocations => {
498 items.sort_by_key(|s| Reverse(s.item.total_allocations()));
499 }
500 SortMode::PersistentAllocations => {
501 items.sort_by_key(|s| Reverse(s.item.total_persistent_allocations()));
502 }
503 SortMode::ExecutionOrder => {}
504 }
505}
506
507fn memory_samples_for(store: &store::Store, span: &SpanRef<'_>) -> Vec<(i64, u64, u8, u64, u64)> {
509 let span_start = *span.start() as i64;
510 store
511 .memory_samples_for_range_with_ts(span.start(), span.end())
512 .into_iter()
513 .map(|(ts, mem, pressure, footprint, workers)| {
514 ((*ts as i64) - span_start, mem, pressure, footprint, workers)
515 })
516 .collect()
517}
518
519fn build_graph_span_info(
522 store: &store::Store,
523 located: Located<SpanGraphRef<'_>>,
524 parent_start: Timestamp,
525 sort: SortMode,
526 depth: u32,
527) -> SpanInfo {
528 let Located { item: graph, id } = located;
529 let first = graph.first_span();
530 let (cat, title) = graph.nice_name();
531 let count = graph.count() as u64;
532 let total_cpu = *graph.total_time();
533 let total_corrected = *graph.corrected_total_time();
534
535 let heaviest = graph
536 .root_spans()
537 .max_by_key(|span| span.total_persistent_allocations())
538 .map(|span| span.index.to_string());
539
540 let memory_samples = memory_samples_for(store, &first);
541 let memory_summary = MemorySummary::from_samples(&memory_samples);
542
543 let children = if depth > 1 {
544 let mut located_children: Vec<_> = graph_children(&graph)
545 .into_iter()
546 .map(|child| {
547 let leaf = format!("a{}", child.first_span().index);
548 Located {
549 id: build_span_id(Some(&id), &leaf),
550 item: child,
551 }
552 })
553 .collect();
554 sort_graph(&mut located_children, sort);
555 located_children
556 .into_iter()
557 .map(|child| build_graph_span_info(store, child, first.start(), sort, depth - 1))
558 .collect()
559 } else {
560 Vec::new()
561 };
562
563 SpanInfo {
564 id,
565 name: format_span_name(cat, title),
566 cpu_duration: *first.total_time(),
567 corrected_duration: *first.corrected_total_time(),
568 start_relative_to_parent: (*first.start() as i64) - (*parent_start as i64),
569 end_relative_to_parent: (*first.end() as i64) - (*parent_start as i64),
570 args: first
571 .args()
572 .map(|(k, v)| (k.to_string(), v.to_string()))
573 .collect(),
574 is_aggregated: count > 1,
575 count: Some(count),
576 total_cpu_duration: Some(total_cpu),
577 avg_cpu_duration: Some(total_cpu.checked_div(count).unwrap_or(0)),
578 total_corrected_duration: Some(total_corrected),
579 avg_corrected_duration: Some(total_corrected.checked_div(count).unwrap_or(0)),
580 first_span_id: Some(first.index.to_string()),
581 heaviest_span_id: heaviest,
582 allocations: graph.total_allocations(),
583 deallocations: graph.total_deallocations(),
584 persistent_allocations: graph.total_persistent_allocations(),
585 allocation_count: graph.total_allocation_count(),
586 self_allocations: graph.self_allocations(),
587 self_deallocations: graph.self_deallocations(),
588 self_persistent_allocations: graph.self_persistent_allocations(),
589 self_allocation_count: graph.self_allocation_count(),
590 memory_samples,
591 memory_summary,
592 children,
593 }
594}
595
596fn build_raw_span_info(
598 store: &store::Store,
599 located: Located<SpanRef<'_>>,
600 parent_start: Timestamp,
601 sort: SortMode,
602 depth: u32,
603) -> SpanInfo {
604 let Located { item: span, id } = located;
605 let (cat, title) = span.nice_name();
606
607 let memory_samples = memory_samples_for(store, &span);
608 let memory_summary = MemorySummary::from_samples(&memory_samples);
609
610 let children = if depth > 1 {
611 let mut located_children: Vec<_> = span
612 .children()
613 .map(|child| Located {
614 id: build_span_id(Some(&id), &child.index.to_string()),
615 item: child,
616 })
617 .collect();
618 sort_spans(&mut located_children, sort);
619 located_children
620 .into_iter()
621 .map(|child| build_raw_span_info(store, child, span.start(), sort, depth - 1))
622 .collect()
623 } else {
624 Vec::new()
625 };
626
627 SpanInfo {
628 id,
629 name: format_span_name(cat, title),
630 cpu_duration: *span.total_time(),
631 corrected_duration: *span.corrected_total_time(),
632 start_relative_to_parent: (*span.start() as i64) - (*parent_start as i64),
633 end_relative_to_parent: (*span.end() as i64) - (*parent_start as i64),
634 args: span
635 .args()
636 .map(|(k, v)| (k.to_string(), v.to_string()))
637 .collect(),
638 is_aggregated: false,
639 count: None,
640 total_cpu_duration: None,
641 avg_cpu_duration: None,
642 total_corrected_duration: None,
643 avg_corrected_duration: None,
644 first_span_id: None,
645 heaviest_span_id: None,
646 allocations: span.total_allocations(),
647 deallocations: span.total_deallocations(),
648 persistent_allocations: span.total_persistent_allocations(),
649 allocation_count: span.total_allocation_count(),
650 self_allocations: span.self_allocations(),
651 self_deallocations: span.self_deallocations(),
652 self_persistent_allocations: span.self_persistent_allocations(),
653 self_allocation_count: span.self_allocation_count(),
654 memory_samples,
655 memory_summary,
656 children,
657 }
658}
659
660pub fn query_spans(store: &Arc<StoreContainer>, options: QueryOptions) -> QueryResult {
665 let deadline = Instant::now() + Duration::from_secs(10);
667 loop {
668 {
669 let guard = store.read();
670 if guard.spans.len() > 1 {
672 break;
673 }
674 }
675 if Instant::now() >= deadline {
676 break;
677 }
678 thread::sleep(Duration::from_millis(50));
679 }
680
681 let store_guard = store.read();
682 let store_ref = &*store_guard;
683
684 let parent_span: Option<SpanRef<'_>> = if let Some(ref parent_id) = options.parent {
685 resolve_span_by_id(store_ref, parent_id)
686 } else {
687 None
688 };
689
690 let parent_start = parent_span.as_ref().map(|s| s.start()).unwrap_or_default();
691 let max_depth = options.max_depth.clamp(1, DEFAULT_SEARCH_MAX_DEPTH);
692 let depth = options.depth.clamp(1, DEFAULT_SEARCH_MAX_DEPTH);
693 let page_size = options
694 .page_size
695 .unwrap_or(DEFAULT_PAGE_SIZE)
696 .clamp(1, MAX_PAGE_SIZE);
697
698 if options.aggregated {
699 let direct: Vec<Located<SpanGraphRef<'_>>> = match parent_span {
700 Some(ref parent) => span_graph_children(parent),
701 None => span_graph_children(&store_ref.root_span()),
702 }
703 .into_iter()
704 .map(|graph| {
705 let leaf = format!("a{}", graph.first_span().index);
706 Located {
707 id: build_span_id(options.parent.as_deref(), &leaf),
708 item: graph,
709 }
710 })
711 .collect();
712
713 let mut filtered = match options.search {
715 Some(ref query) => search_graph_recursive(direct, query, max_depth),
716 None => direct,
717 };
718
719 sort_graph(&mut filtered, options.sort);
720
721 let (page_items, page, total_pages, total_count) =
722 paginate(filtered, options.page, page_size);
723
724 let spans = page_items
725 .into_iter()
726 .map(|located| {
727 build_graph_span_info(store_ref, located, parent_start, options.sort, depth)
728 })
729 .collect();
730
731 QueryResult {
732 spans,
733 page,
734 total_pages,
735 total_count,
736 }
737 } else {
738 let filtered: Vec<Located<SpanRef<'_>>> = if let Some(ref query) = options.search {
744 let needs_scan = query.split(',').any(|term| term.trim().contains(' '));
745 let matches: Vec<SpanRef<'_>> = if needs_scan {
746 let roots: Vec<SpanRef<'_>> = match parent_span {
747 Some(ref parent) => parent.children().collect(),
748 None => store_ref.root_spans().collect(),
749 };
750 search_spans_recursive(roots, query, max_depth)
751 } else {
752 match parent_span {
753 Some(ref parent) => parent.search(query).collect(),
754 None => store_ref.root_span().search(query).collect(),
755 }
756 };
757 matches
758 .into_iter()
759 .filter_map(|span| {
760 raw_span_path(&span, parent_span.as_ref(), max_depth).map(|id| Located {
761 item: span,
762 id: build_span_id(options.parent.as_deref(), &id),
763 })
764 })
765 .collect()
766 } else {
767 match parent_span {
768 Some(ref parent) => parent.children().collect::<Vec<_>>(),
769 None => store_ref.root_spans().collect::<Vec<_>>(),
770 }
771 .into_iter()
772 .map(|span| Located {
773 id: build_span_id(options.parent.as_deref(), &span.index.to_string()),
774 item: span,
775 })
776 .collect()
777 };
778
779 let mut filtered = filtered;
780 sort_spans(&mut filtered, options.sort);
781
782 let (page_items, page, total_pages, total_count) =
783 paginate(filtered, options.page, page_size);
784
785 let spans = page_items
786 .into_iter()
787 .map(|located| {
788 build_raw_span_info(store_ref, located, parent_start, options.sort, depth)
789 })
790 .collect();
791
792 QueryResult {
793 spans,
794 page,
795 total_pages,
796 total_count,
797 }
798 }
799}
800
801fn raw_span_path(
808 span: &SpanRef<'_>,
809 ancestor: Option<&SpanRef<'_>>,
810 max_depth: u32,
811) -> Option<String> {
812 let stop_at = ancestor.map(|a| a.index);
813 let mut segments = Vec::new();
814 let mut current = *span;
815
816 loop {
817 if Some(current.index) == stop_at {
818 break;
819 }
820 if segments.len() as u32 >= max_depth {
821 return None;
822 }
823 segments.push(current.index.to_string());
824 match current.parent() {
825 Some(parent) => current = parent,
826 None => {
828 if stop_at.is_some() {
829 return None;
830 }
831 break;
832 }
833 }
834 }
835
836 segments.reverse();
837 Some(segments.join("-"))
838}
839
840fn resolve_span_by_id<'a>(store: &'a store::Store, id: &str) -> Option<SpanRef<'a>> {
850 let last = id.split('-').next_back().unwrap_or(id);
852 let index_str = last.strip_prefix('a').unwrap_or(last);
854 let index: usize = index_str.parse().ok()?;
855 store.spans.get(index).map(|s| SpanRef {
856 span: s,
857 store,
858 index,
859 })
860}
861
862#[cfg(test)]
863mod tests {
864 use rustc_hash::FxHashSet;
865 use turbo_rcstr::RcStr;
866
867 use super::*;
868 use crate::{
869 span::{SpanArgs, SpanIndex},
870 timestamp::Timestamp,
871 };
872
873 fn nested_store() -> (Arc<StoreContainer>, Vec<SpanIndex>) {
883 let container = Arc::new(StoreContainer::new());
884 let mut indices = Vec::new();
885 {
886 let mut store = container.write();
887 let mut outdated = FxHashSet::default();
888
889 let outer = store.add_span(
890 None,
891 Timestamp::from_micros(0),
892 RcStr::default(),
893 RcStr::from("outer"),
894 SpanArgs::new(),
895 &mut outdated,
896 );
897 let middle = store.add_span(
898 Some(outer),
899 Timestamp::from_micros(1),
900 RcStr::default(),
901 RcStr::from("middle"),
902 SpanArgs::new(),
903 &mut outdated,
904 );
905 let needle = store.add_span(
908 Some(middle),
909 Timestamp::from_micros(2),
910 RcStr::from("cat"),
911 RcStr::from("needle"),
912 SpanArgs::new(),
913 &mut outdated,
914 );
915 let other = store.add_span(
916 Some(outer),
917 Timestamp::from_micros(3),
918 RcStr::default(),
919 RcStr::from("other"),
920 SpanArgs::new(),
921 &mut outdated,
922 );
923
924 store.add_allocation(needle, 1_000, 10, &mut outdated);
927 store.add_allocation(other, 50_000, 100, &mut outdated);
928
929 for span in [outer, middle, needle, other] {
930 store.complete_span(span);
931 }
932 store.invalidate_outdated_spans(&outdated);
933 indices.extend([outer, middle, needle, other]);
934 }
935 (container, indices)
936 }
937
938 fn query(store: &Arc<StoreContainer>, options: QueryOptions) -> QueryResult {
939 query_spans(store, options)
940 }
941
942 fn options() -> QueryOptions {
943 QueryOptions {
944 parent: None,
945 aggregated: true,
946 sort: SortMode::ExecutionOrder,
947 search: None,
948 max_depth: DEFAULT_SEARCH_MAX_DEPTH,
949 depth: 1,
950 page: 1,
951 page_size: None,
952 }
953 }
954
955 #[test]
956 fn aggregated_search_finds_deep_descendants() {
957 let (store, _) = nested_store();
958
959 let result = query(
961 &store,
962 QueryOptions {
963 search: Some("needle".to_string()),
964 ..options()
965 },
966 );
967
968 assert_eq!(result.total_count, 1, "expected to find the nested span");
969 assert_eq!(result.spans[0].name, "cat needle");
970 }
971
972 #[test]
973 fn aggregated_search_returns_full_navigable_path() {
974 let (store, _) = nested_store();
975 let result = query(
976 &store,
977 QueryOptions {
978 search: Some("needle".to_string()),
979 ..options()
980 },
981 );
982
983 let id = &result.spans[0].id;
984 assert_eq!(
985 id.split('-').count(),
986 3,
987 "expected a 3-segment path, got {id}"
988 );
989
990 let children = query(
992 &store,
993 QueryOptions {
994 parent: Some(id.clone()),
995 ..options()
996 },
997 );
998 assert_eq!(children.total_count, 0, "needle has no children");
999 }
1000
1001 #[test]
1002 fn raw_search_finds_deep_descendants_with_paths() {
1003 let (store, _) = nested_store();
1004 let result = query(
1005 &store,
1006 QueryOptions {
1007 aggregated: false,
1008 search: Some("needle".to_string()),
1009 ..options()
1010 },
1011 );
1012
1013 assert_eq!(result.total_count, 1);
1014 assert_eq!(result.spans[0].name, "cat needle");
1015 assert_eq!(result.spans[0].id.split('-').count(), 3);
1017 }
1018
1019 #[test]
1020 fn raw_search_matches_the_display_name() {
1021 let (store, _) = nested_store();
1022 let result = query(
1026 &store,
1027 QueryOptions {
1028 aggregated: false,
1029 search: Some("cat needle".to_string()),
1030 ..options()
1031 },
1032 );
1033 assert_eq!(result.total_count, 1);
1034 assert_eq!(result.spans[0].name, "cat needle");
1035 }
1036
1037 #[test]
1038 fn raw_search_handles_multi_word_terms_that_the_index_matches_partially() {
1039 let (store, _) = nested_store();
1040 let both = query(
1044 &store,
1045 QueryOptions {
1046 aggregated: false,
1047 search: Some("cat needle".to_string()),
1048 ..options()
1049 },
1050 );
1051 assert_eq!(both.total_count, 1);
1052 assert_eq!(both.spans[0].name, "cat needle");
1053
1054 let neither = query(
1057 &store,
1058 QueryOptions {
1059 aggregated: false,
1060 search: Some("cat nomatch".to_string()),
1061 ..options()
1062 },
1063 );
1064 assert_eq!(neither.total_count, 0);
1065 }
1066
1067 #[test]
1068 fn search_respects_max_depth() {
1069 let (store, _) = nested_store();
1070 let shallow = query(
1071 &store,
1072 QueryOptions {
1073 search: Some("needle".to_string()),
1074 max_depth: 2,
1075 ..options()
1076 },
1077 );
1078 assert_eq!(
1079 shallow.total_count, 0,
1080 "needle sits at depth 3 and must be excluded at max_depth 2"
1081 );
1082 }
1083
1084 #[test]
1085 fn depth_returns_nested_children_in_one_call() {
1086 let (store, _) = nested_store();
1087 let result = query(
1088 &store,
1089 QueryOptions {
1090 depth: 3,
1091 ..options()
1092 },
1093 );
1094
1095 let outer = &result.spans[0];
1096 assert_eq!(outer.name, "outer");
1097 assert_eq!(outer.children.len(), 2, "middle and other");
1098
1099 let middle = outer
1100 .children
1101 .iter()
1102 .find(|c| c.name == "middle")
1103 .expect("middle present");
1104 assert_eq!(middle.children.len(), 1);
1105 assert_eq!(middle.children[0].name, "cat needle");
1106
1107 assert!(middle.children[0].id.starts_with(&outer.id));
1109 }
1110
1111 #[test]
1112 fn depth_one_leaves_children_empty() {
1113 let (store, _) = nested_store();
1114 let result = query(&store, options());
1115 assert!(result.spans[0].children.is_empty());
1116 }
1117
1118 #[test]
1119 fn raw_allocation_sorts_are_descending() {
1120 let (store, _) = nested_store();
1121
1122 for sort in [SortMode::Allocations, SortMode::PersistentAllocations] {
1125 let result = query(
1126 &store,
1127 QueryOptions {
1128 aggregated: false,
1129 search: Some("e".to_string()),
1131 sort,
1132 ..options()
1133 },
1134 );
1135 let values: Vec<u64> = result
1136 .spans
1137 .iter()
1138 .map(|s| match sort {
1139 SortMode::PersistentAllocations => s.persistent_allocations,
1140 _ => s.allocations,
1141 })
1142 .collect();
1143 let mut sorted = values.clone();
1144 sorted.sort_by(|a, b| b.cmp(a));
1145 assert_eq!(values, sorted, "{sort:?} must be descending");
1146 }
1147 }
1148
1149 #[test]
1150 fn page_size_is_honored_and_capped() {
1151 let (store, _) = nested_store();
1152
1153 let paged = query(
1154 &store,
1155 QueryOptions {
1156 parent: Some("1".to_string()),
1157 aggregated: false,
1158 page_size: Some(1),
1159 ..options()
1160 },
1161 );
1162 assert_eq!(paged.spans.len(), 1);
1163 assert_eq!(paged.total_count, 2, "outer has two children");
1164 assert_eq!(paged.total_pages, 2);
1165
1166 let huge = query(
1168 &store,
1169 QueryOptions {
1170 page_size: Some(usize::MAX),
1171 ..options()
1172 },
1173 );
1174 assert_eq!(huge.page, 1);
1175 }
1176
1177 #[test]
1178 fn heaviest_span_id_points_at_the_biggest_member() {
1179 let (store, indices) = nested_store();
1180 let result = query(
1181 &store,
1182 QueryOptions {
1183 parent: Some("1".to_string()),
1184 ..options()
1185 },
1186 );
1187
1188 let other = result
1189 .spans
1190 .iter()
1191 .find(|s| s.name == "other")
1192 .expect("other present");
1193 assert_eq!(
1195 other.heaviest_span_id.as_deref(),
1196 Some(indices[3].get().to_string().as_str())
1197 );
1198 }
1199
1200 #[test]
1201 fn heaviest_span_id_differs_from_first_in_a_multi_member_group() {
1202 let container = Arc::new(StoreContainer::new());
1207 let mut members = Vec::new();
1208 {
1209 let mut store = container.write();
1210 let mut outdated = FxHashSet::default();
1211 let parent = store.add_span(
1212 None,
1213 Timestamp::from_micros(0),
1214 RcStr::default(),
1215 RcStr::from("parent"),
1216 SpanArgs::new(),
1217 &mut outdated,
1218 );
1219 for (i, bytes) in [1_000u64, 90_000, 40_000].into_iter().enumerate() {
1222 let child = store.add_span(
1223 Some(parent),
1224 Timestamp::from_micros(i as u64 + 1),
1225 RcStr::default(),
1226 RcStr::from("member"),
1227 SpanArgs::new(),
1228 &mut outdated,
1229 );
1230 store.add_allocation(child, bytes, 10, &mut outdated);
1231 store.complete_span(child);
1232 members.push(child);
1233 }
1234 store.complete_span(parent);
1235 store.invalidate_outdated_spans(&outdated);
1236 }
1237
1238 let result = query(
1239 &container,
1240 QueryOptions {
1241 parent: Some("1".to_string()),
1242 ..options()
1243 },
1244 );
1245 let group = result
1246 .spans
1247 .iter()
1248 .find(|s| s.name == "member")
1249 .expect("group present");
1250 assert_eq!(group.count, Some(3), "the three spans must aggregate");
1251
1252 let first = members[0].get().to_string();
1253 let heaviest = members[1].get().to_string();
1254 assert_eq!(group.first_span_id.as_deref(), Some(first.as_str()));
1255 assert_eq!(group.heaviest_span_id.as_deref(), Some(heaviest.as_str()));
1256 assert_ne!(
1257 group.first_span_id, group.heaviest_span_id,
1258 "otherwise this test cannot tell the two apart"
1259 );
1260 }
1261
1262 #[test]
1263 fn memory_summary_is_absent_without_samples() {
1264 let (store, _) = nested_store();
1265 let result = query(&store, options());
1266 assert!(result.spans[0].memory_summary.is_none());
1267 }
1268
1269 #[test]
1270 fn memory_summary_reports_peak_not_last() {
1271 let samples = [
1272 (0i64, 100u64, 0u8, 5000u64, 2u64),
1273 (1, 900, 3, 4000, 1),
1274 (2, 200, 1, 7000, 3),
1275 ];
1276 let summary = MemorySummary::from_samples(&samples).expect("samples present");
1277 assert_eq!(summary.count, 3);
1278 assert_eq!(summary.start, 100);
1279 assert_eq!(summary.end, 200);
1280 assert_eq!(summary.min, 100);
1281 assert_eq!(summary.peak, 900);
1282 assert_eq!(summary.max_pressure, 3);
1283 assert_eq!(summary.max_footprint, 7000);
1285 assert!(MemorySummary::from_samples(&[]).is_none());
1286 }
1287
1288 #[test]
1289 fn search_terms_are_anded() {
1290 assert!(name_matches("cat", "needle in haystack", "needle,haystack"));
1291 assert!(!name_matches("cat", "needle only", "needle,haystack"));
1292 assert!(name_matches("turbopack", "build", "turbopack"));
1294 }
1295
1296 #[test]
1297 fn search_matches_the_display_name_callers_see() {
1298 assert!(name_matches("turbopack", "build", "turbopack build"));
1301 assert!(!name_matches("turbopack", "build", "turbopack missing"));
1303 assert!(!name_matches("turbopack", "build", "nope"));
1304 }
1305
1306 #[test]
1307 fn search_term_is_a_substring_of_the_display_name() {
1308 assert!(name_matches("turbopack", "build", "turbopack build"));
1310 assert!(!name_matches("turbopack", "build", "build turbopack"));
1311 assert!(name_matches(
1313 "a",
1314 "analyze ecmascript module",
1315 "analyze ecmascript"
1316 ));
1317 assert!(!name_matches(
1318 "a",
1319 "analyze ecmascript module",
1320 "analyze module"
1321 ));
1322 assert!(name_matches(
1324 "turbopack_ecmascript",
1325 "analyze module",
1326 "ecmascript analyze"
1327 ));
1328 assert!(!name_matches(
1329 "turbopack_ecmascript",
1330 "analyze module",
1331 "analyze ecmascript"
1332 ));
1333 assert!(!name_matches(
1335 "turbopack_ecmascript",
1336 "analyze module",
1337 "turbopack module"
1338 ));
1339 }
1340
1341 #[test]
1342 fn joined_match_is_equivalent_to_the_formatted_name() {
1343 for (cat, title) in [("foo", ""), ("", "bar"), ("foo", "bar"), ("", "")] {
1349 for term in ["foo", "bar", "foo ", " bar", "foo bar", "o b", "", " "] {
1350 let expected = format_span_name(cat, title).contains(term);
1351 assert_eq!(
1352 contains_in_joined(cat, title, term),
1353 expected,
1354 "cat={cat:?} title={title:?} term={term:?}"
1355 );
1356 }
1357 }
1358 }
1359
1360 #[test]
1361 fn search_handles_non_ascii_without_panicking() {
1362 assert!(name_matches("café", "über naïve", "café über"));
1365 assert!(name_matches("café", "über naïve", "é ü"));
1366 assert!(name_matches("café", "über naïve", "über naïve"));
1367 assert!(!name_matches("café", "über naïve", "naïve café"));
1368 assert!(!name_matches("日本語", "テスト", "テスト 日本語"));
1369 assert!(name_matches("日本語", "テスト", "日本語 テスト"));
1370 }
1371
1372 #[test]
1373 fn search_ignores_empty_terms_and_padding() {
1374 assert!(name_matches("turbopack", "build", " build "));
1377 assert!(name_matches("turbopack", "build", "build,,"));
1378 assert!(!name_matches("turbopack", "build", "build, nope"));
1379 }
1380}