Skip to main content

probador/
tracing.rs

1//! Deep Tracing Module (PROBAR-SPEC-006 Section J)
2//!
3//! Implements syscall tracing, WASM event capture, flamegraph generation,
4//! and source correlation for performance analysis.
5//!
6//! Based on research:
7//! - [C9] Treadmill methodology for trace attribution
8
9#![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// =============================================================================
31// J.2 Trace Categories
32// =============================================================================
33
34/// Trace category for filtering
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
36pub enum TraceCategory {
37    /// System call tracing
38    Syscall,
39    /// WASM-specific events
40    Wasm,
41    /// Network events
42    Network,
43    /// Memory operations
44    Memory,
45    /// GPU/rendering events
46    Gpu,
47}
48
49impl TraceCategory {
50    /// Get display name
51    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    /// Get all categories
62    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// =============================================================================
74// J.4 Trace Configuration
75// =============================================================================
76
77/// Deep trace configuration
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TraceConfig {
80    /// Categories to trace
81    pub categories: Vec<TraceCategory>,
82    /// Sample rate (0.0 - 1.0)
83    pub sample_rate: f64,
84    /// Maximum events to capture
85    pub max_events: usize,
86    /// Source map path for Rust code
87    pub source_map: Option<PathBuf>,
88    /// WASM source map path
89    pub wasm_source_map: Option<PathBuf>,
90    /// Output path for trace file
91    pub output_path: Option<PathBuf>,
92}
93
94impl TraceConfig {
95    /// Create default config
96    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    /// Enable only specific categories
108    pub fn with_categories(mut self, cats: Vec<TraceCategory>) -> Self {
109        self.categories = cats;
110        self
111    }
112
113    /// Set sample rate
114    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    /// Set source map
120    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// =============================================================================
133// J.3 Trace Events
134// =============================================================================
135
136/// A span in the trace timeline
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TraceSpan {
139    /// Span name
140    pub name: String,
141    /// Category
142    pub category: TraceCategory,
143    /// Start time in microseconds
144    pub start_us: u64,
145    /// Duration in microseconds
146    pub duration_us: u64,
147    /// Thread ID
148    pub thread_id: u64,
149    /// Additional metadata
150    pub metadata: HashMap<String, String>,
151}
152
153impl TraceSpan {
154    /// Create a new span
155    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    /// End time
167    pub fn end_us(&self) -> u64 {
168        self.start_us + self.duration_us
169    }
170
171    /// Duration in milliseconds
172    pub fn duration_ms(&self) -> f64 {
173        self.duration_us as f64 / 1000.0
174    }
175
176    /// Add metadata
177    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/// Syscall statistics
184#[derive(Debug, Clone, Serialize, Deserialize, Default)]
185pub struct SyscallStats {
186    /// Syscall name
187    pub name: String,
188    /// Call count
189    pub count: u64,
190    /// Total time in microseconds
191    pub total_us: u64,
192    /// Average time in microseconds
193    pub avg_us: u64,
194    /// Maximum time in microseconds
195    pub max_us: u64,
196    /// Percentage of total time
197    pub percent: f64,
198}
199
200impl SyscallStats {
201    /// Create new stats
202    pub fn new(name: &str) -> Self {
203        Self {
204            name: name.to_string(),
205            ..Default::default()
206        }
207    }
208
209    /// Record a syscall
210    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/// WASM-specific event
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct WasmEvent {
221    /// Event type
222    pub event_type: WasmEventType,
223    /// Duration in microseconds
224    pub duration_us: u64,
225    /// Memory impact in bytes
226    pub memory_impact: i64,
227    /// Source location (if available)
228    pub source_location: Option<SourceLocation>,
229}
230
231/// WASM event types
232#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
233pub enum WasmEventType {
234    /// WASM module compilation
235    Compile,
236    /// Module instantiation
237    Instantiate,
238    /// Function call
239    Call,
240    /// Memory grow operation
241    MemoryGrow,
242    /// Table operation
243    TableOp,
244}
245
246impl WasmEventType {
247    /// Get display name
248    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/// Source code location
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct SourceLocation {
262    /// File path
263    pub file: PathBuf,
264    /// Line number
265    pub line: u32,
266    /// Column number
267    pub column: Option<u32>,
268    /// Function name
269    pub function: Option<String>,
270}
271
272impl SourceLocation {
273    /// Create new location
274    pub fn new(file: PathBuf, line: u32) -> Self {
275        Self {
276            file,
277            line,
278            column: None,
279            function: None,
280        }
281    }
282
283    /// Format as string
284    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// =============================================================================
294// J.3 Source Hotspots
295// =============================================================================
296
297/// Source code hotspot (performance bottleneck)
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct SourceHotspot {
300    /// File path
301    pub file: PathBuf,
302    /// Line number
303    pub line: u32,
304    /// Function name
305    pub function: String,
306    /// Total time in microseconds
307    pub total_us: u64,
308    /// Call count
309    pub call_count: u64,
310    /// Optimization suggestion
311    pub suggestion: Option<OptimizationSuggestion>,
312}
313
314impl SourceHotspot {
315    /// Create new hotspot
316    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    /// Total time in milliseconds
328    pub fn total_ms(&self) -> f64 {
329        self.total_us as f64 / 1000.0
330    }
331}
332
333/// Optimization suggestion for hotspot
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub enum OptimizationSuggestion {
336    /// Use SIMD instructions
337    UseSIMD {
338        /// Expected speedup factor
339        expected_speedup: f64,
340    },
341    /// Use object pool
342    UsePool {
343        /// Current allocation count
344        current_allocs: u64,
345    },
346    /// Batch operations
347    BatchOperations {
348        /// Current call count
349        current_calls: u64,
350    },
351    /// Use async I/O
352    AsyncIO {
353        /// Blocking time in microseconds
354        blocking_us: u64,
355    },
356}
357
358impl OptimizationSuggestion {
359    /// Get display hint
360    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// =============================================================================
371// J.3 Trace Analysis
372// =============================================================================
373
374/// Complete trace analysis
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct TraceAnalysis {
377    /// Request identifier
378    pub request_id: String,
379    /// Total duration in microseconds
380    pub total_us: u64,
381    /// Timeline of spans
382    pub timeline: Vec<TraceSpan>,
383    /// Syscall breakdown
384    pub syscall_breakdown: HashMap<String, SyscallStats>,
385    /// WASM events
386    pub wasm_events: Vec<WasmEvent>,
387    /// Source hotspots
388    pub source_hotspots: Vec<SourceHotspot>,
389    /// Critical path components
390    pub critical_path: Vec<String>,
391}
392
393impl TraceAnalysis {
394    /// Create new analysis
395    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    /// Add a span
408    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    /// Record a syscall
414    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    /// Add a WASM event
422    pub fn add_wasm_event(&mut self, event: WasmEvent) {
423        self.wasm_events.push(event);
424    }
425
426    /// Add a hotspot
427    pub fn add_hotspot(&mut self, hotspot: SourceHotspot) {
428        self.source_hotspots.push(hotspot);
429    }
430
431    /// Calculate critical path
432    pub fn calculate_critical_path(&mut self) {
433        // Sort spans by duration descending
434        let mut spans: Vec<_> = self.timeline.iter().collect();
435        spans.sort_by(|a, b| b.duration_us.cmp(&a.duration_us));
436
437        // Take top contributors
438        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    /// Calculate syscall percentages
452    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    /// Total duration in milliseconds
462    pub fn total_ms(&self) -> f64 {
463        self.total_us as f64 / 1000.0
464    }
465}
466
467// =============================================================================
468// J.2 Flamegraph Data
469// =============================================================================
470
471/// Flamegraph node
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct FlamegraphNode {
474    /// Function/span name
475    pub name: String,
476    /// Self time (not including children)
477    pub self_time_us: u64,
478    /// Total time (including children)
479    pub total_time_us: u64,
480    /// Children nodes
481    pub children: Vec<FlamegraphNode>,
482}
483
484impl FlamegraphNode {
485    /// Create new node
486    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    /// Add time
496    pub fn add_time(&mut self, us: u64) {
497        self.self_time_us += us;
498        self.total_time_us += us;
499    }
500
501    /// Add child
502    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/// Flamegraph data structure
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct Flamegraph {
511    /// Root nodes
512    pub roots: Vec<FlamegraphNode>,
513    /// Total time in microseconds
514    pub total_us: u64,
515}
516
517impl Flamegraph {
518    /// Create new flamegraph
519    pub fn new() -> Self {
520        Self {
521            roots: Vec::new(),
522            total_us: 0,
523        }
524    }
525
526    /// Add a root node
527    pub fn add_root(&mut self, node: FlamegraphNode) {
528        self.total_us += node.total_time_us;
529        self.roots.push(node);
530    }
531
532    /// Render as folded stack format (for external tools)
533    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// =============================================================================
566// Rendering
567// =============================================================================
568
569/// Render trace analysis as TUI
570#[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    // Timeline
582    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    // Syscall breakdown
605    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    // WASM events
637    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    // Source hotspots
674    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
710/// Render as JSON
711pub fn render_trace_json(analysis: &TraceAnalysis) -> String {
712    serde_json::to_string_pretty(analysis).unwrap_or_else(|_| "{}".to_string())
713}
714
715/// Truncate string
716fn 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// =============================================================================
725// Tests
726// =============================================================================
727
728#[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        // Cover all TraceCategory::name() branches
850        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        // Cover all WasmEventType::name() branches
883        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        // Cover all OptimizationSuggestion::hint() branches
908        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        // Should not panic with empty syscalls
969        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        // Add spans
1007        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        // Add syscalls
1016        analysis.record_syscall("read", 500);
1017        analysis.record_syscall("write", 300);
1018        analysis.calculate_syscall_percentages();
1019
1020        // Add WASM events
1021        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        // Add negative memory impact WASM event
1030        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        // Add hotspots
1039        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        // Calculate critical path
1048        analysis.calculate_critical_path();
1049
1050        let report = render_trace_report(&analysis);
1051
1052        // Verify all sections present
1053        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}