1#![allow(clippy::must_use_candidate)]
10#![allow(clippy::missing_panics_doc)]
11#![allow(clippy::missing_errors_doc)]
12#![allow(clippy::module_name_repetitions)]
13#![allow(clippy::missing_const_for_fn)]
14#![allow(clippy::cast_possible_truncation)]
15#![allow(clippy::cast_precision_loss)]
16#![allow(clippy::cast_sign_loss)]
17#![allow(clippy::format_push_string)]
18#![allow(clippy::uninlined_format_args)]
19#![allow(clippy::return_self_not_must_use)]
20#![allow(clippy::option_if_let_else)]
21#![allow(clippy::use_self)]
22#![allow(clippy::only_used_in_recursion)]
23#![allow(clippy::map_unwrap_or)]
24#![allow(clippy::redundant_closure_for_method_calls)]
25
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28use std::path::PathBuf;
29
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
36pub enum TraceCategory {
37 Syscall,
39 Wasm,
41 Network,
43 Memory,
45 Gpu,
47}
48
49impl TraceCategory {
50 pub fn name(&self) -> &'static str {
52 match self {
53 Self::Syscall => "Syscall",
54 Self::Wasm => "WASM",
55 Self::Network => "Network",
56 Self::Memory => "Memory",
57 Self::Gpu => "GPU",
58 }
59 }
60
61 pub fn all() -> Vec<Self> {
63 vec![
64 Self::Syscall,
65 Self::Wasm,
66 Self::Network,
67 Self::Memory,
68 Self::Gpu,
69 ]
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TraceConfig {
80 pub categories: Vec<TraceCategory>,
82 pub sample_rate: f64,
84 pub max_events: usize,
86 pub source_map: Option<PathBuf>,
88 pub wasm_source_map: Option<PathBuf>,
90 pub output_path: Option<PathBuf>,
92}
93
94impl TraceConfig {
95 pub fn new() -> Self {
97 Self {
98 categories: TraceCategory::all(),
99 sample_rate: 1.0,
100 max_events: 100_000,
101 source_map: None,
102 wasm_source_map: None,
103 output_path: None,
104 }
105 }
106
107 pub fn with_categories(mut self, cats: Vec<TraceCategory>) -> Self {
109 self.categories = cats;
110 self
111 }
112
113 pub fn with_sample_rate(mut self, rate: f64) -> Self {
115 self.sample_rate = rate.clamp(0.0, 1.0);
116 self
117 }
118
119 pub fn with_source_map(mut self, path: PathBuf) -> Self {
121 self.source_map = Some(path);
122 self
123 }
124}
125
126impl Default for TraceConfig {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TraceSpan {
139 pub name: String,
141 pub category: TraceCategory,
143 pub start_us: u64,
145 pub duration_us: u64,
147 pub thread_id: u64,
149 pub metadata: HashMap<String, String>,
151}
152
153impl TraceSpan {
154 pub fn new(name: &str, category: TraceCategory, start_us: u64, duration_us: u64) -> Self {
156 Self {
157 name: name.to_string(),
158 category,
159 start_us,
160 duration_us,
161 thread_id: 0,
162 metadata: HashMap::new(),
163 }
164 }
165
166 pub fn end_us(&self) -> u64 {
168 self.start_us + self.duration_us
169 }
170
171 pub fn duration_ms(&self) -> f64 {
173 self.duration_us as f64 / 1000.0
174 }
175
176 pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
178 self.metadata.insert(key.to_string(), value.to_string());
179 self
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, Default)]
185pub struct SyscallStats {
186 pub name: String,
188 pub count: u64,
190 pub total_us: u64,
192 pub avg_us: u64,
194 pub max_us: u64,
196 pub percent: f64,
198}
199
200impl SyscallStats {
201 pub fn new(name: &str) -> Self {
203 Self {
204 name: name.to_string(),
205 ..Default::default()
206 }
207 }
208
209 pub fn record(&mut self, duration_us: u64) {
211 self.count += 1;
212 self.total_us += duration_us;
213 self.max_us = self.max_us.max(duration_us);
214 self.avg_us = self.total_us / self.count;
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct WasmEvent {
221 pub event_type: WasmEventType,
223 pub duration_us: u64,
225 pub memory_impact: i64,
227 pub source_location: Option<SourceLocation>,
229}
230
231#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
233pub enum WasmEventType {
234 Compile,
236 Instantiate,
238 Call,
240 MemoryGrow,
242 TableOp,
244}
245
246impl WasmEventType {
247 pub fn name(&self) -> &'static str {
249 match self {
250 Self::Compile => "wasm_compile",
251 Self::Instantiate => "wasm_instantiate",
252 Self::Call => "wasm_call",
253 Self::MemoryGrow => "wasm_memory_grow",
254 Self::TableOp => "wasm_table_op",
255 }
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct SourceLocation {
262 pub file: PathBuf,
264 pub line: u32,
266 pub column: Option<u32>,
268 pub function: Option<String>,
270}
271
272impl SourceLocation {
273 pub fn new(file: PathBuf, line: u32) -> Self {
275 Self {
276 file,
277 line,
278 column: None,
279 function: None,
280 }
281 }
282
283 pub fn display(&self) -> String {
285 if let Some(ref func) = self.function {
286 format!("{}:{} ({})", self.file.display(), self.line, func)
287 } else {
288 format!("{}:{}", self.file.display(), self.line)
289 }
290 }
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct SourceHotspot {
300 pub file: PathBuf,
302 pub line: u32,
304 pub function: String,
306 pub total_us: u64,
308 pub call_count: u64,
310 pub suggestion: Option<OptimizationSuggestion>,
312}
313
314impl SourceHotspot {
315 pub fn new(file: PathBuf, line: u32, function: &str) -> Self {
317 Self {
318 file,
319 line,
320 function: function.to_string(),
321 total_us: 0,
322 call_count: 0,
323 suggestion: None,
324 }
325 }
326
327 pub fn total_ms(&self) -> f64 {
329 self.total_us as f64 / 1000.0
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub enum OptimizationSuggestion {
336 UseSIMD {
338 expected_speedup: f64,
340 },
341 UsePool {
343 current_allocs: u64,
345 },
346 BatchOperations {
348 current_calls: u64,
350 },
351 AsyncIO {
353 blocking_us: u64,
355 },
356}
357
358impl OptimizationSuggestion {
359 pub fn hint(&self) -> &'static str {
361 match self {
362 Self::UseSIMD { .. } => "⚠ SIMD",
363 Self::UsePool { .. } => "⚠ Pool",
364 Self::BatchOperations { .. } => "⚠ Batch",
365 Self::AsyncIO { .. } => "⚠ Async",
366 }
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct TraceAnalysis {
377 pub request_id: String,
379 pub total_us: u64,
381 pub timeline: Vec<TraceSpan>,
383 pub syscall_breakdown: HashMap<String, SyscallStats>,
385 pub wasm_events: Vec<WasmEvent>,
387 pub source_hotspots: Vec<SourceHotspot>,
389 pub critical_path: Vec<String>,
391}
392
393impl TraceAnalysis {
394 pub fn new(request_id: &str) -> Self {
396 Self {
397 request_id: request_id.to_string(),
398 total_us: 0,
399 timeline: Vec::new(),
400 syscall_breakdown: HashMap::new(),
401 wasm_events: Vec::new(),
402 source_hotspots: Vec::new(),
403 critical_path: Vec::new(),
404 }
405 }
406
407 pub fn add_span(&mut self, span: TraceSpan) {
409 self.total_us = self.total_us.max(span.end_us());
410 self.timeline.push(span);
411 }
412
413 pub fn record_syscall(&mut self, name: &str, duration_us: u64) {
415 self.syscall_breakdown
416 .entry(name.to_string())
417 .or_insert_with(|| SyscallStats::new(name))
418 .record(duration_us);
419 }
420
421 pub fn add_wasm_event(&mut self, event: WasmEvent) {
423 self.wasm_events.push(event);
424 }
425
426 pub fn add_hotspot(&mut self, hotspot: SourceHotspot) {
428 self.source_hotspots.push(hotspot);
429 }
430
431 pub fn calculate_critical_path(&mut self) {
433 let mut spans: Vec<_> = self.timeline.iter().collect();
435 spans.sort_by(|a, b| b.duration_us.cmp(&a.duration_us));
436
437 self.critical_path = spans
439 .iter()
440 .take(5)
441 .map(|s| {
442 format!(
443 "{} ({:.1}%)",
444 s.name,
445 s.duration_us as f64 / self.total_us as f64 * 100.0
446 )
447 })
448 .collect();
449 }
450
451 pub fn calculate_syscall_percentages(&mut self) {
453 let total: u64 = self.syscall_breakdown.values().map(|s| s.total_us).sum();
454 if total > 0 {
455 for stats in self.syscall_breakdown.values_mut() {
456 stats.percent = stats.total_us as f64 / total as f64 * 100.0;
457 }
458 }
459 }
460
461 pub fn total_ms(&self) -> f64 {
463 self.total_us as f64 / 1000.0
464 }
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct FlamegraphNode {
474 pub name: String,
476 pub self_time_us: u64,
478 pub total_time_us: u64,
480 pub children: Vec<FlamegraphNode>,
482}
483
484impl FlamegraphNode {
485 pub fn new(name: &str) -> Self {
487 Self {
488 name: name.to_string(),
489 self_time_us: 0,
490 total_time_us: 0,
491 children: Vec::new(),
492 }
493 }
494
495 pub fn add_time(&mut self, us: u64) {
497 self.self_time_us += us;
498 self.total_time_us += us;
499 }
500
501 pub fn add_child(&mut self, child: FlamegraphNode) {
503 self.total_time_us += child.total_time_us;
504 self.children.push(child);
505 }
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct Flamegraph {
511 pub roots: Vec<FlamegraphNode>,
513 pub total_us: u64,
515}
516
517impl Flamegraph {
518 pub fn new() -> Self {
520 Self {
521 roots: Vec::new(),
522 total_us: 0,
523 }
524 }
525
526 pub fn add_root(&mut self, node: FlamegraphNode) {
528 self.total_us += node.total_time_us;
529 self.roots.push(node);
530 }
531
532 pub fn to_folded(&self) -> String {
534 let mut out = String::new();
535 for root in &self.roots {
536 self.fold_node(&mut out, root, "");
537 }
538 out
539 }
540
541 #[allow(clippy::self_only_used_in_recursion)]
542 fn fold_node(&self, out: &mut String, node: &FlamegraphNode, prefix: &str) {
543 let path = if prefix.is_empty() {
544 node.name.clone()
545 } else {
546 format!("{};{}", prefix, node.name)
547 };
548
549 if node.self_time_us > 0 {
550 out.push_str(&format!("{} {}\n", path, node.self_time_us));
551 }
552
553 for child in &node.children {
554 self.fold_node(out, child, &path);
555 }
556 }
557}
558
559impl Default for Flamegraph {
560 fn default() -> Self {
561 Self::new()
562 }
563}
564
565#[allow(clippy::too_many_lines)]
571pub fn render_trace_report(analysis: &TraceAnalysis) -> String {
572 let mut out = String::new();
573
574 out.push_str(&format!(
575 "DEEP TRACE ANALYSIS: {} (total: {:.1}ms)\n",
576 analysis.request_id,
577 analysis.total_ms()
578 ));
579 out.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
580
581 out.push_str("TIMELINE\n");
583 out.push_str("┌──────────────────────────────────────────────────────────────────────────┐\n");
584 for span in &analysis.timeline {
585 let bar_len = (span.duration_us as f64 / analysis.total_us as f64 * 30.0) as usize;
586 let bar: String = "█".repeat(bar_len.max(1));
587 out.push_str(&format!(
588 "│ [{:<8}] {:30} {:.1}ms\n",
589 span.category.name(),
590 bar,
591 span.duration_ms()
592 ));
593 }
594 if !analysis.critical_path.is_empty() {
595 out.push_str(&format!(
596 "│ Critical Path: {}\n",
597 analysis.critical_path.join(" → ")
598 ));
599 }
600 out.push_str(
601 "└──────────────────────────────────────────────────────────────────────────┘\n\n",
602 );
603
604 if !analysis.syscall_breakdown.is_empty() {
606 out.push_str("SYSCALL BREAKDOWN\n");
607 out.push_str(
608 "┌─────────────────┬───────┬────────────┬──────────┬──────────┬────────────┐\n",
609 );
610 out.push_str(
611 "│ Syscall │ Count │ Total Time │ Avg Time │ Max Time │ % of Total │\n",
612 );
613 out.push_str(
614 "├─────────────────┼───────┼────────────┼──────────┼──────────┼────────────┤\n",
615 );
616
617 let mut stats: Vec<_> = analysis.syscall_breakdown.values().collect();
618 stats.sort_by(|a, b| b.total_us.cmp(&a.total_us));
619
620 for stat in stats.iter().take(10) {
621 out.push_str(&format!(
622 "│ {:<15} │ {:>5} │ {:>8.1}ms │ {:>6}μs │ {:>6}μs │ {:>9.1}% │\n",
623 truncate(&stat.name, 15),
624 stat.count,
625 stat.total_us as f64 / 1000.0,
626 stat.avg_us,
627 stat.max_us,
628 stat.percent
629 ));
630 }
631 out.push_str(
632 "└─────────────────┴───────┴────────────┴──────────┴──────────┴────────────┘\n\n",
633 );
634 }
635
636 if !analysis.wasm_events.is_empty() {
638 out.push_str("WASM-SPECIFIC EVENTS\n");
639 out.push_str(
640 "┌────────────────────┬──────────┬───────────────┬─────────────────────────┐\n",
641 );
642 out.push_str(
643 "│ Event │ Duration │ Memory Impact │ Source Location │\n",
644 );
645 out.push_str(
646 "├────────────────────┼──────────┼───────────────┼─────────────────────────┤\n",
647 );
648
649 for event in &analysis.wasm_events {
650 let memory_str = if event.memory_impact >= 0 {
651 format!("+{}KB", event.memory_impact / 1024)
652 } else {
653 format!("{}KB", event.memory_impact / 1024)
654 };
655 let source = event
656 .source_location
657 .as_ref()
658 .map(|s| s.display())
659 .unwrap_or_else(|| "(internal)".to_string());
660 out.push_str(&format!(
661 "│ {:<18} │ {:>6}ms │ {:>13} │ {:<23} │\n",
662 event.event_type.name(),
663 event.duration_us / 1000,
664 memory_str,
665 truncate(&source, 23)
666 ));
667 }
668 out.push_str(
669 "└────────────────────┴──────────┴───────────────┴─────────────────────────┘\n\n",
670 );
671 }
672
673 if !analysis.source_hotspots.is_empty() {
675 out.push_str("SOURCE CORRELATION (top hotspots)\n");
676 out.push_str(
677 "┌─────────────────────────┬─────────────────────┬───────┬───────┬───────────┐\n",
678 );
679 out.push_str(
680 "│ File:Line │ Function │ Time │ Calls │ Suggestion│\n",
681 );
682 out.push_str(
683 "├─────────────────────────┼─────────────────────┼───────┼───────┼───────────┤\n",
684 );
685
686 for hotspot in analysis.source_hotspots.iter().take(5) {
687 let file_line = format!("{}:{}", hotspot.file.display(), hotspot.line);
688 let suggestion = hotspot
689 .suggestion
690 .as_ref()
691 .map(|s| s.hint())
692 .unwrap_or("✓ OK");
693 out.push_str(&format!(
694 "│ {:<23} │ {:<19} │ {:>4}ms│ {:>5} │ {:<9} │\n",
695 truncate(&file_line, 23),
696 truncate(&hotspot.function, 19),
697 hotspot.total_ms() as u64,
698 hotspot.call_count,
699 suggestion
700 ));
701 }
702 out.push_str(
703 "└─────────────────────────┴─────────────────────┴───────┴───────┴───────────┘\n",
704 );
705 }
706
707 out
708}
709
710pub fn render_trace_json(analysis: &TraceAnalysis) -> String {
712 serde_json::to_string_pretty(analysis).unwrap_or_else(|_| "{}".to_string())
713}
714
715fn truncate(s: &str, max: usize) -> String {
717 if s.len() <= max {
718 s.to_string()
719 } else {
720 format!("{}…", &s[..max - 1])
721 }
722}
723
724#[cfg(test)]
729#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn test_trace_category() {
735 assert_eq!(TraceCategory::Syscall.name(), "Syscall");
736 assert_eq!(TraceCategory::all().len(), 5);
737 }
738
739 #[test]
740 fn test_trace_config() {
741 let config = TraceConfig::new()
742 .with_sample_rate(0.5)
743 .with_categories(vec![TraceCategory::Wasm]);
744
745 assert_eq!(config.sample_rate, 0.5);
746 assert_eq!(config.categories.len(), 1);
747 }
748
749 #[test]
750 fn test_trace_span() {
751 let span =
752 TraceSpan::new("test", TraceCategory::Network, 1000, 500).with_metadata("key", "value");
753
754 assert_eq!(span.end_us(), 1500);
755 assert_eq!(span.duration_ms(), 0.5);
756 assert_eq!(span.metadata.get("key"), Some(&"value".to_string()));
757 }
758
759 #[test]
760 fn test_syscall_stats() {
761 let mut stats = SyscallStats::new("read");
762 stats.record(100);
763 stats.record(200);
764
765 assert_eq!(stats.count, 2);
766 assert_eq!(stats.total_us, 300);
767 assert_eq!(stats.avg_us, 150);
768 assert_eq!(stats.max_us, 200);
769 }
770
771 #[test]
772 fn test_wasm_event_type() {
773 assert_eq!(WasmEventType::Compile.name(), "wasm_compile");
774 assert_eq!(WasmEventType::MemoryGrow.name(), "wasm_memory_grow");
775 }
776
777 #[test]
778 fn test_source_location() {
779 let mut loc = SourceLocation::new(PathBuf::from("src/main.rs"), 42);
780 loc.function = Some("main".to_string());
781
782 assert!(loc.display().contains("src/main.rs:42"));
783 assert!(loc.display().contains("main"));
784 }
785
786 #[test]
787 fn test_source_hotspot() {
788 let hotspot = SourceHotspot::new(PathBuf::from("src/lib.rs"), 100, "process");
789 assert_eq!(hotspot.function, "process");
790 }
791
792 #[test]
793 fn test_optimization_suggestion() {
794 let suggestion = OptimizationSuggestion::UseSIMD {
795 expected_speedup: 4.0,
796 };
797 assert_eq!(suggestion.hint(), "⚠ SIMD");
798 }
799
800 #[test]
801 fn test_trace_analysis() {
802 let mut analysis = TraceAnalysis::new("req-123");
803 analysis.add_span(TraceSpan::new("network", TraceCategory::Network, 0, 1000));
804 analysis.record_syscall("read", 500);
805 analysis.calculate_critical_path();
806
807 assert_eq!(analysis.total_us, 1000);
808 assert_eq!(analysis.timeline.len(), 1);
809 assert!(analysis.syscall_breakdown.contains_key("read"));
810 }
811
812 #[test]
813 fn test_flamegraph() {
814 let mut fg = Flamegraph::new();
815 let mut root = FlamegraphNode::new("main");
816 root.add_time(1000);
817
818 let mut child = FlamegraphNode::new("process");
819 child.add_time(500);
820 root.add_child(child);
821
822 fg.add_root(root);
823
824 assert_eq!(fg.total_us, 1500);
825 let folded = fg.to_folded();
826 assert!(folded.contains("main"));
827 }
828
829 #[test]
830 fn test_render_trace_report() {
831 let mut analysis = TraceAnalysis::new("test-request");
832 analysis.add_span(TraceSpan::new("dns", TraceCategory::Network, 0, 5000));
833 analysis.record_syscall("read", 1000);
834
835 let report = render_trace_report(&analysis);
836 assert!(report.contains("test-request"));
837 assert!(report.contains("TIMELINE"));
838 }
839
840 #[test]
841 fn test_render_trace_json() {
842 let analysis = TraceAnalysis::new("json-test");
843 let json = render_trace_json(&analysis);
844 assert!(json.contains("json-test"));
845 }
846
847 #[test]
848 fn test_trace_category_all_names() {
849 assert_eq!(TraceCategory::Syscall.name(), "Syscall");
851 assert_eq!(TraceCategory::Wasm.name(), "WASM");
852 assert_eq!(TraceCategory::Network.name(), "Network");
853 assert_eq!(TraceCategory::Memory.name(), "Memory");
854 assert_eq!(TraceCategory::Gpu.name(), "GPU");
855 }
856
857 #[test]
858 fn test_trace_config_default() {
859 let config = TraceConfig::default();
860 assert_eq!(config.sample_rate, 1.0);
861 assert_eq!(config.max_events, 100_000);
862 assert_eq!(config.categories.len(), 5);
863 }
864
865 #[test]
866 fn test_trace_config_with_source_map() {
867 let config = TraceConfig::new().with_source_map(PathBuf::from("/src/source.map"));
868 assert_eq!(config.source_map, Some(PathBuf::from("/src/source.map")));
869 }
870
871 #[test]
872 fn test_trace_config_sample_rate_clamp() {
873 let config_high = TraceConfig::new().with_sample_rate(1.5);
874 assert_eq!(config_high.sample_rate, 1.0);
875
876 let config_low = TraceConfig::new().with_sample_rate(-0.5);
877 assert_eq!(config_low.sample_rate, 0.0);
878 }
879
880 #[test]
881 fn test_wasm_event_type_all_names() {
882 assert_eq!(WasmEventType::Compile.name(), "wasm_compile");
884 assert_eq!(WasmEventType::Instantiate.name(), "wasm_instantiate");
885 assert_eq!(WasmEventType::Call.name(), "wasm_call");
886 assert_eq!(WasmEventType::MemoryGrow.name(), "wasm_memory_grow");
887 assert_eq!(WasmEventType::TableOp.name(), "wasm_table_op");
888 }
889
890 #[test]
891 fn test_source_location_without_function() {
892 let loc = SourceLocation::new(PathBuf::from("src/lib.rs"), 10);
893 let display = loc.display();
894 assert!(display.contains("src/lib.rs:10"));
895 assert!(!display.contains('('));
896 }
897
898 #[test]
899 fn test_source_hotspot_total_ms() {
900 let mut hotspot = SourceHotspot::new(PathBuf::from("src/hot.rs"), 50, "hot_function");
901 hotspot.total_us = 5000;
902 assert_eq!(hotspot.total_ms(), 5.0);
903 }
904
905 #[test]
906 fn test_optimization_suggestion_all_hints() {
907 let simd = OptimizationSuggestion::UseSIMD {
909 expected_speedup: 4.0,
910 };
911 assert_eq!(simd.hint(), "⚠ SIMD");
912
913 let pool = OptimizationSuggestion::UsePool {
914 current_allocs: 100,
915 };
916 assert_eq!(pool.hint(), "⚠ Pool");
917
918 let batch = OptimizationSuggestion::BatchOperations { current_calls: 50 };
919 assert_eq!(batch.hint(), "⚠ Batch");
920
921 let async_io = OptimizationSuggestion::AsyncIO { blocking_us: 1000 };
922 assert_eq!(async_io.hint(), "⚠ Async");
923 }
924
925 #[test]
926 fn test_trace_analysis_wasm_events() {
927 let mut analysis = TraceAnalysis::new("wasm-test");
928
929 let event = WasmEvent {
930 event_type: WasmEventType::Compile,
931 duration_us: 1000,
932 memory_impact: 1024,
933 source_location: Some(SourceLocation::new(PathBuf::from("src/wasm.rs"), 1)),
934 };
935 analysis.add_wasm_event(event);
936
937 assert_eq!(analysis.wasm_events.len(), 1);
938 }
939
940 #[test]
941 fn test_trace_analysis_hotspots() {
942 let mut analysis = TraceAnalysis::new("hotspot-test");
943
944 let hotspot = SourceHotspot::new(PathBuf::from("src/slow.rs"), 100, "slow_function");
945 analysis.add_hotspot(hotspot);
946
947 assert_eq!(analysis.source_hotspots.len(), 1);
948 }
949
950 #[test]
951 fn test_trace_analysis_syscall_percentages() {
952 let mut analysis = TraceAnalysis::new("syscall-test");
953 analysis.record_syscall("read", 700);
954 analysis.record_syscall("write", 300);
955 analysis.calculate_syscall_percentages();
956
957 let read_stats = analysis.syscall_breakdown.get("read").unwrap();
958 let write_stats = analysis.syscall_breakdown.get("write").unwrap();
959
960 assert_eq!(read_stats.percent, 70.0);
961 assert_eq!(write_stats.percent, 30.0);
962 }
963
964 #[test]
965 fn test_trace_analysis_empty_syscall_percentages() {
966 let mut analysis = TraceAnalysis::new("empty-syscall");
967 analysis.calculate_syscall_percentages();
968 assert!(analysis.syscall_breakdown.is_empty());
970 }
971
972 #[test]
973 fn test_flamegraph_default() {
974 let fg = Flamegraph::default();
975 assert!(fg.roots.is_empty());
976 assert_eq!(fg.total_us, 0);
977 }
978
979 #[test]
980 fn test_flamegraph_to_folded_with_children() {
981 let mut fg = Flamegraph::new();
982
983 let mut root = FlamegraphNode::new("root");
984 root.add_time(100);
985
986 let mut child1 = FlamegraphNode::new("child1");
987 child1.add_time(50);
988
989 let mut grandchild = FlamegraphNode::new("grandchild");
990 grandchild.add_time(25);
991 child1.add_child(grandchild);
992
993 root.add_child(child1);
994 fg.add_root(root);
995
996 let folded = fg.to_folded();
997 assert!(folded.contains("root 100"));
998 assert!(folded.contains("root;child1 50"));
999 assert!(folded.contains("root;child1;grandchild 25"));
1000 }
1001
1002 #[test]
1003 fn test_render_trace_report_full() {
1004 let mut analysis = TraceAnalysis::new("full-test");
1005
1006 analysis.add_span(TraceSpan::new(
1008 "dns_lookup",
1009 TraceCategory::Network,
1010 0,
1011 2000,
1012 ));
1013 analysis.add_span(TraceSpan::new("compile", TraceCategory::Wasm, 2000, 5000));
1014
1015 analysis.record_syscall("read", 500);
1017 analysis.record_syscall("write", 300);
1018 analysis.calculate_syscall_percentages();
1019
1020 let wasm_event = WasmEvent {
1022 event_type: WasmEventType::Instantiate,
1023 duration_us: 3000,
1024 memory_impact: 2048,
1025 source_location: None,
1026 };
1027 analysis.add_wasm_event(wasm_event);
1028
1029 let wasm_event_negative = WasmEvent {
1031 event_type: WasmEventType::MemoryGrow,
1032 duration_us: 1000,
1033 memory_impact: -1024,
1034 source_location: Some(SourceLocation::new(PathBuf::from("src/mem.rs"), 42)),
1035 };
1036 analysis.add_wasm_event(wasm_event_negative);
1037
1038 let mut hotspot = SourceHotspot::new(PathBuf::from("src/hot.rs"), 100, "hot_func");
1040 hotspot.total_us = 4000;
1041 hotspot.call_count = 1000;
1042 hotspot.suggestion = Some(OptimizationSuggestion::UseSIMD {
1043 expected_speedup: 2.0,
1044 });
1045 analysis.add_hotspot(hotspot);
1046
1047 analysis.calculate_critical_path();
1049
1050 let report = render_trace_report(&analysis);
1051
1052 assert!(report.contains("DEEP TRACE ANALYSIS"));
1054 assert!(report.contains("TIMELINE"));
1055 assert!(report.contains("SYSCALL BREAKDOWN"));
1056 assert!(report.contains("WASM-SPECIFIC EVENTS"));
1057 assert!(report.contains("SOURCE CORRELATION"));
1058 assert!(report.contains("Critical Path"));
1059 }
1060
1061 #[test]
1062 fn test_truncate_helper() {
1063 assert_eq!(truncate("short", 10), "short");
1064 assert_eq!(truncate("verylongstring", 5), "very…");
1065 }
1066}