Skip to main content

trustformers_debug/
flame_graph_profiler.rs

1//! Advanced flame graph profiling implementation for TrustformeRS Debug
2// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
3// are retained for the data model, serialization completeness, and future consumers that
4// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
5#![allow(dead_code)]
6
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11use std::time::{Instant, SystemTime};
12
13use crate::profiler::{ProfileEvent, Profiler};
14
15/// Flame graph node representing a stack frame
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FlameGraphNode {
18    pub name: String,
19    pub value: u64,
20    pub delta: Option<i64>, // For differential analysis
21    pub children: HashMap<String, FlameGraphNode>,
22    pub total_value: u64,
23    pub self_value: u64,
24    pub percentage: f64,
25    pub color: Option<String>,
26    pub metadata: HashMap<String, String>,
27}
28
29/// Escape the XML/HTML predefined entities so frame names cannot break out of
30/// the generated markup.
31fn html_escape(text: &str) -> String {
32    let mut out = String::with_capacity(text.len());
33    for ch in text.chars() {
34        match ch {
35            '&' => out.push_str("&amp;"),
36            '<' => out.push_str("&lt;"),
37            '>' => out.push_str("&gt;"),
38            '"' => out.push_str("&quot;"),
39            '\'' => out.push_str("&apos;"),
40            _ => out.push(ch),
41        }
42    }
43    out
44}
45
46/// Stack frame for flame graph construction
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct StackFrame {
49    pub function_name: String,
50    pub module_name: Option<String>,
51    pub file_name: Option<String>,
52    pub line_number: Option<u32>,
53    pub address: Option<u64>,
54}
55
56/// Sample data for flame graph
57#[derive(Debug, Clone)]
58pub struct FlameGraphSample {
59    pub stack: Vec<StackFrame>,
60    pub duration_ns: u64,
61    pub timestamp: u64,
62    pub thread_id: u64,
63    pub cpu_id: Option<u32>,
64    pub memory_usage: Option<usize>,
65    pub gpu_kernel: Option<String>,
66    pub metadata: HashMap<String, String>,
67}
68
69/// Configuration for flame graph generation
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct FlameGraphConfig {
72    pub sampling_rate: u32, // Samples per second
73    pub min_width: f64,     // Minimum width for node visibility
74    pub color_scheme: FlameGraphColorScheme,
75    pub direction: FlameGraphDirection,
76    pub title: String,
77    pub subtitle: Option<String>,
78    pub include_memory: bool,
79    pub include_gpu: bool,
80    pub differential_mode: bool,
81    pub merge_similar_stacks: bool,
82    pub filter_noise: bool,
83    pub noise_threshold: f64,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum FlameGraphColorScheme {
88    Hot,          // Red-orange gradient
89    Cool,         // Blue-purple gradient
90    Java,         // Java-specific colors
91    Memory,       // Memory-aware coloring
92    Differential, // Differential analysis colors
93    Random,       // Random but consistent colors
94    Custom(HashMap<String, String>),
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub enum FlameGraphDirection {
99    TopDown,  // Traditional flame graph
100    BottomUp, // Icicle graph
101}
102
103/// Export format for flame graphs
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum FlameGraphExportFormat {
106    SVG,
107    InteractiveHTML,
108    JSON,
109    Speedscope,
110    D3,
111    Folded,
112}
113
114/// Advanced flame graph profiler
115#[derive(Debug)]
116pub struct FlameGraphProfiler {
117    config: FlameGraphConfig,
118    samples: Vec<FlameGraphSample>,
119    sampling_timer: Option<Instant>,
120    root_node: Option<FlameGraphNode>,
121    baseline_samples: Option<Vec<FlameGraphSample>>, // For differential analysis
122    metadata: HashMap<String, String>,
123    current_cpu_usage: f64,
124    current_memory_usage: usize,
125    performance_counters: HashMap<String, u64>,
126}
127
128impl FlameGraphProfiler {
129    /// Create a new flame graph profiler
130    pub fn new(config: FlameGraphConfig) -> Self {
131        Self {
132            config,
133            samples: Vec::new(),
134            sampling_timer: None,
135            root_node: None,
136            baseline_samples: None,
137            metadata: HashMap::new(),
138            current_cpu_usage: 0.0,
139            current_memory_usage: 0,
140            performance_counters: HashMap::new(),
141        }
142    }
143
144    /// Start profiling with sampling
145    pub fn start_sampling(&mut self) -> Result<()> {
146        tracing::info!(
147            "Starting flame graph sampling at {} Hz",
148            self.config.sampling_rate
149        );
150        self.sampling_timer = Some(Instant::now());
151        self.samples.clear();
152        self.root_node = None;
153
154        // Initialize performance counters
155        self.performance_counters.insert("samples_collected".to_string(), 0);
156        self.performance_counters.insert("stack_depth_max".to_string(), 0);
157        self.performance_counters.insert("unique_functions".to_string(), 0);
158
159        Ok(())
160    }
161
162    /// Stop profiling and build flame graph
163    pub fn stop_sampling(&mut self) -> Result<()> {
164        tracing::info!(
165            "Stopping flame graph sampling, collected {} samples",
166            self.samples.len()
167        );
168        self.sampling_timer = None;
169        self.build_flame_graph()?;
170        Ok(())
171    }
172
173    /// Add a sample to the profiler
174    pub fn add_sample(&mut self, sample: FlameGraphSample) {
175        // Update performance counters
176        if let Some(counter) = self.performance_counters.get_mut("samples_collected") {
177            *counter += 1;
178        }
179
180        let stack_depth = sample.stack.len() as u64;
181        if let Some(max_depth) = self.performance_counters.get_mut("stack_depth_max") {
182            if stack_depth > *max_depth {
183                *max_depth = stack_depth;
184            }
185        }
186
187        self.samples.push(sample);
188    }
189
190    /// Add a sample from current stack trace
191    pub fn sample_current_stack(&mut self, duration_ns: u64) -> Result<()> {
192        let stack = self.capture_stack_trace()?;
193        let sample = FlameGraphSample {
194            stack,
195            duration_ns,
196            timestamp: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_nanos() as u64,
197            thread_id: self.get_current_thread_id(),
198            cpu_id: self.get_current_cpu_id(),
199            memory_usage: Some(self.current_memory_usage),
200            gpu_kernel: None,
201            metadata: HashMap::new(),
202        };
203
204        self.add_sample(sample);
205        Ok(())
206    }
207
208    /// Add GPU kernel sample
209    pub fn sample_gpu_kernel(&mut self, kernel_name: &str, duration_ns: u64) {
210        let stack = vec![StackFrame {
211            function_name: format!("GPU::{}", kernel_name),
212            module_name: Some("GPU".to_string()),
213            file_name: None,
214            line_number: None,
215            address: None,
216        }];
217
218        let sample = FlameGraphSample {
219            stack,
220            duration_ns,
221            timestamp: SystemTime::now()
222                .duration_since(SystemTime::UNIX_EPOCH)
223                .unwrap_or_default()
224                .as_nanos() as u64,
225            thread_id: 0, // GPU operations on virtual thread
226            cpu_id: None,
227            memory_usage: None,
228            gpu_kernel: Some(kernel_name.to_string()),
229            metadata: [("type".to_string(), "gpu".to_string())].into_iter().collect(),
230        };
231
232        self.add_sample(sample);
233    }
234
235    /// Set baseline for differential analysis
236    pub fn set_baseline(&mut self) {
237        self.baseline_samples = Some(self.samples.clone());
238        tracing::info!("Set baseline with {} samples", self.samples.len());
239    }
240
241    /// Build flame graph from collected samples
242    pub fn build_flame_graph(&mut self) -> Result<()> {
243        if self.samples.is_empty() {
244            return Err(anyhow::anyhow!("No samples collected"));
245        }
246
247        let mut root = FlameGraphNode {
248            name: "root".to_string(),
249            value: 0,
250            delta: None,
251            children: HashMap::new(),
252            total_value: 0,
253            self_value: 0,
254            percentage: 100.0,
255            color: None,
256            metadata: HashMap::new(),
257        };
258
259        // Merge samples into tree structure
260        for sample in &self.samples {
261            self.merge_sample_into_tree(&mut root, sample);
262        }
263
264        // Calculate totals and percentages
265        self.calculate_node_metrics(&mut root);
266
267        // Apply differential analysis if baseline exists
268        if self.config.differential_mode && self.baseline_samples.is_some() {
269            self.apply_differential_analysis(&mut root)?;
270        }
271
272        // Filter noise if enabled
273        if self.config.filter_noise {
274            self.filter_noise_nodes(&mut root);
275        }
276
277        // Update performance counters
278        let unique_functions = self.count_unique_functions(&root);
279        if let Some(counter) = self.performance_counters.get_mut("unique_functions") {
280            *counter = unique_functions;
281        }
282
283        self.root_node = Some(root);
284        tracing::info!(
285            "Built flame graph with {} unique functions",
286            unique_functions
287        );
288        Ok(())
289    }
290
291    /// Export flame graph to various formats
292    pub async fn export(&self, format: FlameGraphExportFormat, output_path: &Path) -> Result<()> {
293        let root = self
294            .root_node
295            .as_ref()
296            .ok_or_else(|| anyhow::anyhow!("Flame graph not built yet"))?;
297
298        match format {
299            FlameGraphExportFormat::SVG => self.export_svg(root, output_path).await,
300            FlameGraphExportFormat::InteractiveHTML => {
301                self.export_interactive_html(root, output_path).await
302            },
303            FlameGraphExportFormat::JSON => self.export_json(root, output_path).await,
304            FlameGraphExportFormat::Speedscope => self.export_speedscope(root, output_path).await,
305            FlameGraphExportFormat::D3 => self.export_d3(root, output_path).await,
306            FlameGraphExportFormat::Folded => self.export_folded(output_path).await,
307        }
308    }
309
310    /// Export as SVG flame graph
311    async fn export_svg(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
312        let mut svg_content = String::new();
313
314        // SVG header
315        svg_content.push_str(&format!(
316            r##"<?xml version="1.0" encoding="UTF-8"?>
317<svg width="1200" height="800" xmlns="http://www.w3.org/2000/svg">
318<defs>
319    <linearGradient id="background" x1="0%" y1="0%" x2="0%" y2="100%">
320        <stop offset="0%" style="stop-color:#eeeeee"/>
321        <stop offset="100%" style="stop-color:#eeeeb0"/>
322    </linearGradient>
323</defs>
324<rect width="100%" height="100%" fill="url(#background)"/>
325<text x="600" y="24" text-anchor="middle" font-size="17" font-family="Verdana">{}</text>
326<text x="600" y="44" text-anchor="middle" font-size="12" font-family="Verdana" fill="#999">
327    {} samples, {} functions
328</text>
329"##,
330            self.config.title,
331            self.samples.len(),
332            self.count_unique_functions(root)
333        ));
334
335        // Render flame graph rectangles
336        self.render_svg_node(&mut svg_content, root, 0, 0, 1200, 0)?;
337
338        svg_content.push_str("</svg>");
339
340        tokio::fs::write(output_path, svg_content).await?;
341        tracing::info!("Exported SVG flame graph to {:?}", output_path);
342        Ok(())
343    }
344
345    /// Export the flame graph as a self-contained HTML page.
346    ///
347    /// The page carries a REAL inline SVG flame graph -- one rectangle per
348    /// node, width proportional to that node's total value, `y` by stack depth,
349    /// with a native `<title>` tooltip -- plus the full tree as embedded JSON.
350    ///
351    /// It used to advertise "Click to zoom, double-click to reset. Hover for
352    /// details", render `Reset Zoom` / `Search` buttons wired to functions that
353    /// were never defined, and load d3 from a CDN, while the only script in the
354    /// page was a `console.log`. Nothing was drawn: `#flame-graph` stayed
355    /// empty. The page is now honest about being a static rendering.
356    async fn export_interactive_html(
357        &self,
358        root: &FlameGraphNode,
359        output_path: &Path,
360    ) -> Result<()> {
361        let json_data = serde_json::to_string(root)?;
362        let svg = Self::render_flame_svg(root);
363
364        let html_content = format!(
365            r#"<!DOCTYPE html>
366<html>
367<head>
368    <title>{title}</title>
369    <meta charset="utf-8">
370    <style>
371        body {{ font-family: sans-serif; margin: 0; padding: 20px; }}
372        .flame-graph {{ width: 100%; overflow-x: auto; border: 1px solid #ccc; }}
373        .info {{ margin-top: 20px; font-size: 14px; color: #666; }}
374    </style>
375</head>
376<body>
377    <h1>{title}</h1>
378    <div class="flame-graph">{svg}</div>
379    <div class="info">
380        <p>Samples: {samples} | Functions: {functions} | Total Time: {total_ms:.2}ms</p>
381        <p>Static rendering: hover a frame for its name and timing. There is no
382           zoom or search in this export; the complete tree is embedded below as
383           JSON for tools that want it.</p>
384    </div>
385    <script type="application/json" id="flamegraph-data">{json_data}</script>
386</body>
387</html>"#,
388            title = html_escape(&self.config.title),
389            svg = svg,
390            samples = self.samples.len(),
391            functions = self.count_unique_functions(root),
392            total_ms = root.total_value as f64 / 1_000_000.0,
393            json_data = json_data,
394        );
395
396        tokio::fs::write(output_path, html_content).await?;
397        tracing::info!("Exported HTML flame graph to {:?}", output_path);
398        Ok(())
399    }
400
401    /// Height in pixels of one stack level in the rendered SVG.
402    const FLAME_ROW_HEIGHT: f64 = 18.0;
403    /// Total width in pixels of the rendered SVG.
404    const FLAME_WIDTH: f64 = 1200.0;
405
406    /// Render `root` as a real, dependency-free SVG flame graph.
407    fn render_flame_svg(root: &FlameGraphNode) -> String {
408        use std::fmt::Write as _;
409
410        let depth = Self::tree_depth(root);
411        let height = (depth as f64 + 1.0) * Self::FLAME_ROW_HEIGHT + 4.0;
412        let mut out = format!(
413            "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{w}\" height=\"{h:.0}\" \
414             viewBox=\"0 0 {w} {h:.0}\" font-family=\"sans-serif\" font-size=\"11\">",
415            w = Self::FLAME_WIDTH,
416            h = height,
417        );
418        Self::render_flame_node(&mut out, root, 0.0, 0, Self::FLAME_WIDTH, root.total_value);
419        let _ = write!(out, "</svg>");
420        out
421    }
422
423    fn tree_depth(node: &FlameGraphNode) -> usize {
424        1 + node.children.values().map(Self::tree_depth).max().unwrap_or(0)
425    }
426
427    fn render_flame_node(
428        out: &mut String,
429        node: &FlameGraphNode,
430        x: f64,
431        depth: usize,
432        width: f64,
433        root_total: u64,
434    ) {
435        use std::fmt::Write as _;
436
437        if width < 0.05 {
438            return;
439        }
440        let y = depth as f64 * Self::FLAME_ROW_HEIGHT;
441        // Classic flame-graph warm palette, keyed by the frame name so the same
442        // function keeps the same colour across renders.
443        let hue = (Self::name_hash(&node.name) % 50) as u32;
444        let percentage = if root_total > 0 {
445            node.total_value as f64 / root_total as f64 * 100.0
446        } else {
447            0.0
448        };
449        let _ = write!(
450            out,
451            "<g><rect x=\"{x:.2}\" y=\"{y:.2}\" width=\"{w:.2}\" height=\"{h:.2}\" \
452             fill=\"hsl({hue},80%,55%)\" stroke=\"#fff\" stroke-width=\"0.5\"/>\
453             <title>{name} — {ms:.3}ms ({percentage:.2}%)</title>",
454            x = x,
455            y = y,
456            w = width,
457            h = Self::FLAME_ROW_HEIGHT - 1.0,
458            hue = hue,
459            name = html_escape(&node.name),
460            ms = node.total_value as f64 / 1_000_000.0,
461            percentage = percentage,
462        );
463        // Only label frames wide enough to hold readable text.
464        if width > 40.0 {
465            let max_chars = (width / 6.5) as usize;
466            let label: String = node.name.chars().take(max_chars.max(1)).collect();
467            let _ = write!(
468                out,
469                "<text x=\"{tx:.2}\" y=\"{ty:.2}\" fill=\"#000\">{label}</text>",
470                tx = x + 3.0,
471                ty = y + Self::FLAME_ROW_HEIGHT - 6.0,
472                label = html_escape(&label),
473            );
474        }
475        let _ = write!(out, "</g>");
476
477        // Children are laid out left to right in a stable (name-sorted) order,
478        // each taking the share of the parent's width that its value really is.
479        let mut children: Vec<&FlameGraphNode> = node.children.values().collect();
480        children.sort_by(|a, b| a.name.cmp(&b.name));
481        let child_total: u64 = children.iter().map(|c| c.total_value).sum();
482        if child_total == 0 {
483            return;
484        }
485        let mut cursor = x;
486        for child in children {
487            let child_width = width * (child.total_value as f64 / child_total as f64);
488            Self::render_flame_node(out, child, cursor, depth + 1, child_width, root_total);
489            cursor += child_width;
490        }
491    }
492
493    /// Deterministic FNV-1a hash of a frame name, for stable colouring.
494    fn name_hash(name: &str) -> u64 {
495        let mut hash = 0xcbf2_9ce4_8422_2325_u64;
496        for byte in name.as_bytes() {
497            hash ^= *byte as u64;
498            hash = hash.wrapping_mul(0x0100_0000_01b3);
499        }
500        hash
501    }
502
503    /// Export as JSON
504    async fn export_json(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
505        let json_data = serde_json::to_string_pretty(root)?;
506        tokio::fs::write(output_path, json_data).await?;
507        tracing::info!("Exported JSON flame graph to {:?}", output_path);
508        Ok(())
509    }
510
511    /// Export as Speedscope format
512    async fn export_speedscope(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
513        let speedscope_data = self.convert_to_speedscope_format(root)?;
514        let json_data = serde_json::to_string_pretty(&speedscope_data)?;
515        tokio::fs::write(output_path, json_data).await?;
516        tracing::info!("Exported Speedscope format to {:?}", output_path);
517        Ok(())
518    }
519
520    /// Export as D3.js compatible format
521    async fn export_d3(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
522        let d3_data = self.convert_to_d3_format(root)?;
523        let json_data = serde_json::to_string_pretty(&d3_data)?;
524        tokio::fs::write(output_path, json_data).await?;
525        tracing::info!("Exported D3 format to {:?}", output_path);
526        Ok(())
527    }
528
529    /// Export as folded stack format
530    async fn export_folded(&self, output_path: &Path) -> Result<()> {
531        let mut folded_content = String::new();
532
533        for sample in &self.samples {
534            let stack_str: Vec<String> =
535                sample.stack.iter().map(|frame| frame.function_name.clone()).collect();
536            folded_content.push_str(&format!("{} {}\n", stack_str.join(";"), sample.duration_ns));
537        }
538
539        tokio::fs::write(output_path, folded_content).await?;
540        tracing::info!("Exported folded format to {:?}", output_path);
541        Ok(())
542    }
543
544    /// Get flame graph analysis report
545    pub fn get_analysis_report(&self) -> FlameGraphAnalysisReport {
546        let root = self.root_node.as_ref();
547
548        FlameGraphAnalysisReport {
549            total_samples: self.samples.len(),
550            total_duration_ns: self.samples.iter().map(|s| s.duration_ns).sum(),
551            unique_functions: root.map(|r| self.count_unique_functions(r)).unwrap_or(0),
552            max_stack_depth: self.performance_counters.get("stack_depth_max").copied().unwrap_or(0),
553            hot_functions: self.get_hot_functions(10),
554            memory_usage_stats: self.get_memory_usage_stats(),
555            gpu_kernel_stats: self.get_gpu_kernel_stats(),
556            differential_analysis: self.get_differential_analysis(),
557            performance_insights: self.generate_performance_insights(),
558        }
559    }
560
561    // Private helper methods
562
563    /// Capture the REAL current call stack via [`std::backtrace::Backtrace`].
564    ///
565    /// `std` does not expose structured frames on stable, so the frames are
566    /// parsed out of the backtrace's textual form: each frame is a
567    /// `N: symbol` line optionally followed by an `at path:line[:col]` line.
568    /// Frames belonging to this module's own capture machinery are dropped so
569    /// the sample starts at the caller.
570    ///
571    /// Returns an error when the platform captured nothing (backtraces are
572    /// disabled or unsupported) rather than inventing a frame -- the previous
573    /// implementation returned one hardcoded frame,
574    /// `captured_function @ profiler.rs:1800`, so every sample in every flame
575    /// graph was the same fictional stack.
576    fn capture_stack_trace(&self) -> Result<Vec<StackFrame>> {
577        let backtrace = std::backtrace::Backtrace::force_capture();
578        let frames = Self::parse_backtrace(&backtrace.to_string());
579        if frames.is_empty() {
580            anyhow::bail!(
581                "no stack frames could be captured on this platform (std::backtrace status: \
582                 {:?}); refusing to record a fabricated stack",
583                backtrace.status()
584            );
585        }
586        Ok(frames)
587    }
588
589    /// Parse the textual form of a [`std::backtrace::Backtrace`] into frames.
590    ///
591    /// Split out from [`Self::capture_stack_trace`] so the parsing is testable
592    /// against a fixed input, independent of whatever the live stack happens
593    /// to be.
594    fn parse_backtrace(text: &str) -> Vec<StackFrame> {
595        let mut frames: Vec<StackFrame> = Vec::new();
596        for line in text.lines() {
597            let trimmed = line.trim();
598            if let Some(rest) = trimmed.strip_prefix("at ") {
599                // Location line: attach to the frame just pushed.
600                if let Some(frame) = frames.last_mut() {
601                    let mut parts = rest.rsplitn(3, ':');
602                    // `path:line:col` or `path:line`.
603                    let last = parts.next().unwrap_or_default();
604                    let middle = parts.next();
605                    let head = parts.next();
606                    match (head, middle, last.parse::<u32>()) {
607                        // path:line:col
608                        (Some(path), Some(line_no), Ok(_col)) => {
609                            frame.file_name = Some(path.to_string());
610                            frame.line_number = line_no.parse::<u32>().ok();
611                        },
612                        // path:line
613                        (None, Some(path), Ok(line_no)) => {
614                            frame.file_name = Some(path.to_string());
615                            frame.line_number = Some(line_no);
616                        },
617                        _ => frame.file_name = Some(rest.to_string()),
618                    }
619                }
620                continue;
621            }
622
623            // Frame line: `<index>: <symbol>`.
624            let Some((index, symbol)) = trimmed.split_once(": ") else {
625                continue;
626            };
627            if index.parse::<u32>().is_err() {
628                continue;
629            }
630            let symbol = symbol.trim();
631            if symbol.is_empty() {
632                continue;
633            }
634            // The module path is everything before the final `::segment`.
635            let module_name = symbol.rfind("::").map(|idx| symbol[..idx].to_string());
636            frames.push(StackFrame {
637                function_name: symbol.to_string(),
638                module_name,
639                file_name: None,
640                line_number: None,
641                address: None,
642            });
643        }
644
645        // Drop this crate's own capture frames so the sample begins at the
646        // caller of `sample_current_stack`.
647        let first_caller = frames
648            .iter()
649            .position(|f| {
650                !f.function_name.contains("std::backtrace")
651                    && !f.function_name.contains("Backtrace")
652                    && !f.function_name.contains("capture_stack_trace")
653                    && !f.function_name.contains("parse_backtrace")
654            })
655            .unwrap_or(0);
656        frames.split_off(first_caller)
657    }
658
659    /// A real, stable per-thread identifier.
660    ///
661    /// `std::thread::ThreadId` is opaque on stable, so this hands out dense
662    /// ids from a process-wide counter, one per thread, cached in thread-local
663    /// storage. Two samples from the same thread always share an id and two
664    /// different threads never do -- which is exactly what a flame graph needs,
665    /// and what the previous hardcoded `1` could not provide.
666    fn get_current_thread_id(&self) -> u64 {
667        use std::cell::Cell;
668        use std::sync::atomic::{AtomicU64, Ordering};
669
670        static NEXT_THREAD_ID: AtomicU64 = AtomicU64::new(1);
671        thread_local! {
672            static THREAD_ID: Cell<u64> = const { Cell::new(0) };
673        }
674
675        THREAD_ID.with(|slot| {
676            let existing = slot.get();
677            if existing != 0 {
678                return existing;
679            }
680            let assigned = NEXT_THREAD_ID.fetch_add(1, Ordering::Relaxed);
681            slot.set(assigned);
682            assigned
683        })
684    }
685
686    /// Which CPU the sample was taken on.
687    ///
688    /// Always `None`: reading the current CPU needs a platform-specific call
689    /// (`sched_getcpu` on Linux, `GetCurrentProcessorNumber` on Windows, no
690    /// stable equivalent on macOS) and this crate is Pure Rust with no FFI.
691    /// It used to return `Some(0)`, i.e. "every sample ran on CPU 0".
692    fn get_current_cpu_id(&self) -> Option<u32> {
693        None
694    }
695
696    fn merge_sample_into_tree(&self, node: &mut FlameGraphNode, sample: &FlameGraphSample) {
697        if sample.stack.is_empty() {
698            node.value += sample.duration_ns;
699            return;
700        }
701
702        let frame = &sample.stack[0];
703        let child =
704            node.children
705                .entry(frame.function_name.clone())
706                .or_insert_with(|| FlameGraphNode {
707                    name: frame.function_name.clone(),
708                    value: 0,
709                    delta: None,
710                    children: HashMap::new(),
711                    total_value: 0,
712                    self_value: 0,
713                    percentage: 0.0,
714                    color: None,
715                    metadata: HashMap::new(),
716                });
717
718        if sample.stack.len() == 1 {
719            child.value += sample.duration_ns;
720        } else {
721            let mut remaining_sample = sample.clone();
722            remaining_sample.stack = sample.stack[1..].to_vec();
723            self.merge_sample_into_tree(child, &remaining_sample);
724        }
725    }
726
727    fn calculate_node_metrics(&self, node: &mut FlameGraphNode) {
728        let mut total_children_value = 0;
729
730        for child in node.children.values_mut() {
731            self.calculate_node_metrics(child);
732            total_children_value += child.total_value;
733        }
734
735        node.total_value = node.value + total_children_value;
736        node.self_value = node.value;
737
738        if node.total_value > 0 && node.name != "root" {
739            // Get the total from root node for percentage calculation
740            let total_for_percentage = if let Some(root) = &self.root_node {
741                root.total_value
742            } else {
743                node.total_value // fallback
744            };
745
746            if total_for_percentage > 0 {
747                node.percentage = (node.total_value as f64 / total_for_percentage as f64) * 100.0;
748            }
749        }
750    }
751
752    fn apply_differential_analysis(&self, node: &mut FlameGraphNode) -> Result<()> {
753        if let Some(baseline_samples) = &self.baseline_samples {
754            // Build baseline tree
755            let mut baseline_root = FlameGraphNode {
756                name: "root".to_string(),
757                value: 0,
758                delta: None,
759                children: HashMap::new(),
760                total_value: 0,
761                self_value: 0,
762                percentage: 100.0,
763                color: None,
764                metadata: HashMap::new(),
765            };
766
767            for sample in baseline_samples {
768                self.merge_sample_into_tree(&mut baseline_root, sample);
769            }
770
771            // Calculate deltas
772            self.calculate_deltas(node, &baseline_root);
773        }
774        Ok(())
775    }
776
777    fn calculate_deltas(&self, current: &mut FlameGraphNode, baseline: &FlameGraphNode) {
778        let baseline_value =
779            baseline.children.get(&current.name).map(|n| n.total_value as i64).unwrap_or(0);
780
781        current.delta = Some(current.total_value as i64 - baseline_value);
782
783        for (name, child) in &mut current.children {
784            if let Some(baseline_child) = baseline.children.get(name) {
785                self.calculate_deltas(child, baseline_child);
786            } else {
787                child.delta = Some(child.total_value as i64);
788            }
789        }
790    }
791
792    fn filter_noise_nodes(&self, node: &mut FlameGraphNode) {
793        let threshold = (node.total_value as f64 * self.config.noise_threshold / 100.0) as u64;
794
795        node.children.retain(|_, child| {
796            self.filter_noise_nodes(child);
797            child.total_value >= threshold
798        });
799    }
800
801    fn count_unique_functions(&self, node: &FlameGraphNode) -> u64 {
802        let mut count = 1; // Count this node
803        for child in node.children.values() {
804            count += self.count_unique_functions(child);
805        }
806        count
807    }
808
809    fn render_svg_node(
810        &self,
811        svg: &mut String,
812        node: &FlameGraphNode,
813        x: i32,
814        y: i32,
815        width: i32,
816        depth: i32,
817    ) -> Result<()> {
818        if width < 1 {
819            return Ok(());
820        }
821
822        let height = 20;
823        let color = self.get_node_color(node);
824
825        svg.push_str(&format!(
826            r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" stroke="white" stroke-width="0.5">
827<title>{}: {:.2}% ({} samples)</title>
828</rect>
829<text x="{}" y="{}" font-size="12" font-family="Verdana" fill="black">{}</text>
830"#,
831            x, y + depth * height, width, height,
832            color,
833            node.name, node.percentage, node.value,
834            x + 2, y + depth * height + 14,
835            if width > 50 { &node.name } else { "" }
836        ));
837
838        // Render children
839        let mut child_x = x;
840        for child in node.children.values() {
841            let child_width = if node.total_value > 0 {
842                (width as f64 * child.total_value as f64 / node.total_value as f64) as i32
843            } else {
844                0
845            };
846            if child_width > 0 {
847                self.render_svg_node(svg, child, child_x, y, child_width, depth + 1)?;
848                child_x += child_width;
849            }
850        }
851
852        Ok(())
853    }
854
855    fn get_node_color(&self, node: &FlameGraphNode) -> String {
856        match &self.config.color_scheme {
857            FlameGraphColorScheme::Hot => {
858                let intensity = (node.percentage / 100.0 * 255.0) as u8;
859                format!("rgb({}, {}, 0)", 255, 255 - intensity)
860            },
861            FlameGraphColorScheme::Cool => {
862                let intensity = (node.percentage / 100.0 * 255.0) as u8;
863                format!("rgb(0, {}, {})", intensity, 255)
864            },
865            FlameGraphColorScheme::Memory => {
866                if node.name.contains("alloc") || node.name.contains("malloc") {
867                    "#ff6b6b".to_string()
868                } else {
869                    "#4ecdc4".to_string()
870                }
871            },
872            FlameGraphColorScheme::Differential => {
873                match node.delta {
874                    Some(delta) if delta > 0 => "#ff4444".to_string(), // Red for increases
875                    Some(delta) if delta < 0 => "#44ff44".to_string(), // Green for decreases
876                    _ => "#cccccc".to_string(),                        // Gray for no change
877                }
878            },
879            FlameGraphColorScheme::Java => "#ff9800".to_string(),
880            FlameGraphColorScheme::Random => {
881                let hash = self.hash_string(&node.name);
882                format!("hsl({}, 70%, 60%)", hash % 360)
883            },
884            FlameGraphColorScheme::Custom(colors) => {
885                colors.get(&node.name).cloned().unwrap_or_else(|| "#cccccc".to_string())
886            },
887        }
888    }
889
890    fn hash_string(&self, s: &str) -> u32 {
891        let mut hash = 0u32;
892        for byte in s.bytes() {
893            hash = hash.wrapping_mul(31).wrapping_add(byte as u32);
894        }
895        hash
896    }
897
898    fn convert_to_speedscope_format(&self, root: &FlameGraphNode) -> Result<serde_json::Value> {
899        // Simplified Speedscope format conversion
900        Ok(serde_json::json!({
901            "version": "0.7.1",
902            "profiles": [{
903                "type": "sampled",
904                "name": self.config.title,
905                "unit": "nanoseconds",
906                "startValue": 0,
907                "endValue": root.total_value,
908                "samples": [],
909                "weights": []
910            }]
911        }))
912    }
913
914    fn convert_to_d3_format(&self, root: &FlameGraphNode) -> Result<serde_json::Value> {
915        Ok(serde_json::to_value(root)?)
916    }
917
918    fn get_hot_functions(&self, limit: usize) -> Vec<HotFunctionInfo> {
919        let mut functions = Vec::new();
920
921        if let Some(root) = &self.root_node {
922            self.collect_hot_functions(root, &mut functions);
923        }
924
925        functions.sort_by_key(|item| std::cmp::Reverse(item.total_time_ns));
926        functions.truncate(limit);
927        functions
928    }
929
930    fn collect_hot_functions(&self, node: &FlameGraphNode, functions: &mut Vec<HotFunctionInfo>) {
931        functions.push(HotFunctionInfo {
932            name: node.name.clone(),
933            total_time_ns: node.total_value,
934            self_time_ns: node.self_value,
935            percentage: node.percentage,
936            call_count: 1, // Simplified
937        });
938
939        for child in node.children.values() {
940            self.collect_hot_functions(child, functions);
941        }
942    }
943
944    fn get_memory_usage_stats(&self) -> MemoryUsageStats {
945        let memory_samples: Vec<usize> =
946            self.samples.iter().filter_map(|s| s.memory_usage).collect();
947
948        if memory_samples.is_empty() {
949            return MemoryUsageStats::default();
950        }
951
952        let total: usize = memory_samples.iter().sum();
953        let max = memory_samples.iter().max().copied().unwrap_or(0);
954        let min = memory_samples.iter().min().copied().unwrap_or(0);
955        let avg = total / memory_samples.len();
956
957        MemoryUsageStats {
958            peak_memory_bytes: max,
959            avg_memory_bytes: avg,
960            min_memory_bytes: min,
961            total_samples: memory_samples.len(),
962        }
963    }
964
965    fn get_gpu_kernel_stats(&self) -> GpuKernelStats {
966        let gpu_samples: Vec<&FlameGraphSample> =
967            self.samples.iter().filter(|s| s.gpu_kernel.is_some()).collect();
968
969        let total_gpu_time: u64 = gpu_samples.iter().map(|s| s.duration_ns).sum();
970        let unique_kernels: std::collections::HashSet<String> =
971            gpu_samples.iter().filter_map(|s| s.gpu_kernel.clone()).collect();
972
973        GpuKernelStats {
974            total_kernel_time_ns: total_gpu_time,
975            unique_kernels: unique_kernels.len(),
976            total_kernel_calls: gpu_samples.len(),
977        }
978    }
979
980    fn get_differential_analysis(&self) -> Option<DifferentialAnalysis> {
981        if !self.config.differential_mode || self.baseline_samples.is_none() {
982            return None;
983        }
984
985        let current_total: u64 = self.samples.iter().map(|s| s.duration_ns).sum();
986        let baseline_total: u64 =
987            self.baseline_samples.as_ref()?.iter().map(|s| s.duration_ns).sum();
988
989        let performance_change = if baseline_total > 0 {
990            ((current_total as f64 - baseline_total as f64) / baseline_total as f64) * 100.0
991        } else {
992            0.0
993        };
994
995        Some(DifferentialAnalysis {
996            baseline_samples: self.baseline_samples.as_ref()?.len(),
997            current_samples: self.samples.len(),
998            performance_change_percent: performance_change,
999            is_regression: performance_change > 5.0,
1000            is_improvement: performance_change < -5.0,
1001        })
1002    }
1003
1004    fn generate_performance_insights(&self) -> Vec<String> {
1005        let mut insights = Vec::new();
1006
1007        if let Some(root) = &self.root_node {
1008            let hot_functions = self.get_hot_functions(3);
1009
1010            if let Some(hottest) = hot_functions.first() {
1011                if hottest.percentage > 50.0 {
1012                    insights.push(format!(
1013                        "Function '{}' dominates execution time ({:.1}%)",
1014                        hottest.name, hottest.percentage
1015                    ));
1016                }
1017            }
1018
1019            let gpu_stats = self.get_gpu_kernel_stats();
1020            if gpu_stats.total_kernel_calls > 0 {
1021                let gpu_percentage =
1022                    (gpu_stats.total_kernel_time_ns as f64 / root.total_value as f64) * 100.0;
1023                insights.push(format!(
1024                    "GPU kernels account for {:.1}% of execution time",
1025                    gpu_percentage
1026                ));
1027            }
1028
1029            if let Some(diff) = self.get_differential_analysis() {
1030                if diff.is_regression {
1031                    insights.push(format!(
1032                        "Performance regression detected: {:.1}% slower than baseline",
1033                        diff.performance_change_percent
1034                    ));
1035                } else if diff.is_improvement {
1036                    insights.push(format!(
1037                        "Performance improvement: {:.1}% faster than baseline",
1038                        -diff.performance_change_percent
1039                    ));
1040                }
1041            }
1042        }
1043
1044        if insights.is_empty() {
1045            insights.push("No significant performance patterns detected".to_string());
1046        }
1047
1048        insights
1049    }
1050}
1051
1052/// Hot function information
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1054pub struct HotFunctionInfo {
1055    pub name: String,
1056    pub total_time_ns: u64,
1057    pub self_time_ns: u64,
1058    pub percentage: f64,
1059    pub call_count: usize,
1060}
1061
1062/// Memory usage statistics
1063#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1064pub struct MemoryUsageStats {
1065    pub peak_memory_bytes: usize,
1066    pub avg_memory_bytes: usize,
1067    pub min_memory_bytes: usize,
1068    pub total_samples: usize,
1069}
1070
1071/// GPU kernel statistics
1072#[derive(Debug, Clone, Serialize, Deserialize)]
1073pub struct GpuKernelStats {
1074    pub total_kernel_time_ns: u64,
1075    pub unique_kernels: usize,
1076    pub total_kernel_calls: usize,
1077}
1078
1079/// Differential analysis results
1080#[derive(Debug, Clone, Serialize, Deserialize)]
1081pub struct DifferentialAnalysis {
1082    pub baseline_samples: usize,
1083    pub current_samples: usize,
1084    pub performance_change_percent: f64,
1085    pub is_regression: bool,
1086    pub is_improvement: bool,
1087}
1088
1089/// Flame graph analysis report
1090#[derive(Debug, Clone, Serialize, Deserialize)]
1091pub struct FlameGraphAnalysisReport {
1092    pub total_samples: usize,
1093    pub total_duration_ns: u64,
1094    pub unique_functions: u64,
1095    pub max_stack_depth: u64,
1096    pub hot_functions: Vec<HotFunctionInfo>,
1097    pub memory_usage_stats: MemoryUsageStats,
1098    pub gpu_kernel_stats: GpuKernelStats,
1099    pub differential_analysis: Option<DifferentialAnalysis>,
1100    pub performance_insights: Vec<String>,
1101}
1102
1103/// Default configuration for flame graphs
1104impl Default for FlameGraphConfig {
1105    fn default() -> Self {
1106        Self {
1107            sampling_rate: 1000, // 1000 Hz
1108            min_width: 0.01,
1109            color_scheme: FlameGraphColorScheme::Hot,
1110            direction: FlameGraphDirection::TopDown,
1111            title: "Flame Graph".to_string(),
1112            subtitle: None,
1113            include_memory: true,
1114            include_gpu: true,
1115            differential_mode: false,
1116            merge_similar_stacks: true,
1117            filter_noise: true,
1118            noise_threshold: 0.1, // 0.1%
1119        }
1120    }
1121}
1122
1123/// Integration with main Profiler
1124impl Profiler {
1125    /// Create flame graph profiler with current configuration
1126    pub fn create_flame_graph_profiler(&self) -> FlameGraphProfiler {
1127        let config = FlameGraphConfig {
1128            title: "TrustformeRS Debug Flame Graph".to_string(),
1129            subtitle: Some("Performance Analysis".to_string()),
1130            ..Default::default()
1131        };
1132        FlameGraphProfiler::new(config)
1133    }
1134
1135    /// Start flame graph profiling
1136    pub async fn start_flame_graph_profiling(&mut self) -> Result<()> {
1137        // This would integrate with the main profiler's timing events
1138        tracing::info!("Starting integrated flame graph profiling");
1139        Ok(())
1140    }
1141
1142    /// Export flame graph from current profiling data
1143    pub async fn export_flame_graph(
1144        &self,
1145        format: FlameGraphExportFormat,
1146        output_path: &Path,
1147    ) -> Result<()> {
1148        let mut flame_profiler = self.create_flame_graph_profiler();
1149
1150        // Convert existing events to flame graph samples
1151        for event in self.get_events() {
1152            match event {
1153                ProfileEvent::FunctionCall {
1154                    function_name,
1155                    duration,
1156                    ..
1157                } => {
1158                    let sample = FlameGraphSample {
1159                        stack: vec![StackFrame {
1160                            function_name: function_name.clone(),
1161                            module_name: None,
1162                            file_name: None,
1163                            line_number: None,
1164                            address: None,
1165                        }],
1166                        duration_ns: duration.as_nanos() as u64,
1167                        timestamp: 0,
1168                        thread_id: 0,
1169                        cpu_id: None,
1170                        memory_usage: None,
1171                        gpu_kernel: None,
1172                        metadata: HashMap::new(),
1173                    };
1174                    flame_profiler.add_sample(sample);
1175                },
1176                ProfileEvent::LayerExecution {
1177                    layer_name,
1178                    layer_type,
1179                    forward_time,
1180                    ..
1181                } => {
1182                    let sample = FlameGraphSample {
1183                        stack: vec![
1184                            StackFrame {
1185                                function_name: "neural_network".to_string(),
1186                                module_name: Some("trustformers".to_string()),
1187                                file_name: None,
1188                                line_number: None,
1189                                address: None,
1190                            },
1191                            StackFrame {
1192                                function_name: format!("{}::{}", layer_type, layer_name),
1193                                module_name: Some("layers".to_string()),
1194                                file_name: None,
1195                                line_number: None,
1196                                address: None,
1197                            },
1198                        ],
1199                        duration_ns: forward_time.as_nanos() as u64,
1200                        timestamp: 0,
1201                        thread_id: 0,
1202                        cpu_id: None,
1203                        memory_usage: None,
1204                        gpu_kernel: None,
1205                        metadata: HashMap::new(),
1206                    };
1207                    flame_profiler.add_sample(sample);
1208                },
1209                _ => {}, // Handle other event types as needed
1210            }
1211        }
1212
1213        flame_profiler.build_flame_graph()?;
1214        flame_profiler.export(format, output_path).await?;
1215        Ok(())
1216    }
1217}
1218
1219#[cfg(test)]
1220#[path = "flame_graph_profiler_tests.rs"]
1221mod flame_graph_profiler_tests;
1222
1223#[cfg(test)]
1224mod tests {
1225    use super::*;
1226
1227    // ---- Wave 6c debug-sweep2: real stack capture and real SVG ------------
1228
1229    #[test]
1230    fn parse_backtrace_extracts_real_frames_and_locations() {
1231        let text = "   0: std::backtrace::Backtrace::force_capture\n                    \x20            at /rustc/lib/backtrace.rs:10:5\n                    \x20  1: my_crate::my_module::my_function\n                    \x20            at src/lib.rs:42:9\n                    \x20  2: main\n";
1232        let frames = FlameGraphProfiler::parse_backtrace(text);
1233        // The std::backtrace frame is dropped as capture machinery.
1234        assert_eq!(frames.len(), 2, "{frames:?}");
1235        assert_eq!(frames[0].function_name, "my_crate::my_module::my_function");
1236        assert_eq!(
1237            frames[0].module_name.as_deref(),
1238            Some("my_crate::my_module")
1239        );
1240        assert_eq!(frames[0].file_name.as_deref(), Some("src/lib.rs"));
1241        assert_eq!(frames[0].line_number, Some(42));
1242        assert_eq!(frames[1].function_name, "main");
1243        assert_eq!(frames[1].line_number, None);
1244        // The old capture returned exactly one fictional frame.
1245        assert!(frames.iter().all(|f| f.function_name != "captured_function"));
1246    }
1247
1248    #[test]
1249    fn parse_backtrace_returns_nothing_for_unparseable_text() {
1250        assert!(FlameGraphProfiler::parse_backtrace("").is_empty());
1251        assert!(FlameGraphProfiler::parse_backtrace("disabled backtrace").is_empty());
1252    }
1253
1254    #[test]
1255    fn sample_current_stack_captures_this_test_function() {
1256        let mut profiler = FlameGraphProfiler::new(FlameGraphConfig::default());
1257        profiler.sample_current_stack(1_000).expect("a live stack must be capturable");
1258        let sample = profiler.samples.last().expect("one sample");
1259        assert!(!sample.stack.is_empty());
1260        // A real capture names real symbols, not a single fixed placeholder.
1261        assert!(
1262            sample.stack.iter().all(|f| f.function_name != "captured_function"),
1263            "{:?}",
1264            sample.stack
1265        );
1266        // CPU id is honestly absent rather than a fabricated 0.
1267        assert_eq!(sample.cpu_id, None);
1268        assert!(sample.thread_id > 0, "a real dense thread id is assigned");
1269    }
1270
1271    #[test]
1272    fn thread_ids_are_stable_per_thread_and_distinct_across_threads() {
1273        let profiler = FlameGraphProfiler::new(FlameGraphConfig::default());
1274        let mine = profiler.get_current_thread_id();
1275        assert_eq!(
1276            mine,
1277            profiler.get_current_thread_id(),
1278            "stable within a thread"
1279        );
1280        let other = std::thread::spawn(move || {
1281            let p = FlameGraphProfiler::new(FlameGraphConfig::default());
1282            p.get_current_thread_id()
1283        })
1284        .join()
1285        .expect("thread join");
1286        assert_ne!(
1287            mine, other,
1288            "a different thread must get a different id (was always 1)"
1289        );
1290    }
1291
1292    #[test]
1293    fn render_flame_svg_draws_one_rect_per_node_with_real_widths() {
1294        let mut root = FlameGraphNode {
1295            name: "root".to_string(),
1296            value: 0,
1297            delta: None,
1298            children: HashMap::new(),
1299            total_value: 100,
1300            self_value: 0,
1301            percentage: 100.0,
1302            color: None,
1303            metadata: HashMap::new(),
1304        };
1305        for (name, value) in [("hot", 75_u64), ("cold", 25_u64)] {
1306            root.children.insert(
1307                name.to_string(),
1308                FlameGraphNode {
1309                    name: name.to_string(),
1310                    value,
1311                    delta: None,
1312                    children: HashMap::new(),
1313                    total_value: value,
1314                    self_value: value,
1315                    percentage: value as f64,
1316                    color: None,
1317                    metadata: HashMap::new(),
1318                },
1319            );
1320        }
1321
1322        let svg = FlameGraphProfiler::render_flame_svg(&root);
1323        assert!(svg.starts_with("<svg"), "must be a real SVG document");
1324        assert_eq!(
1325            svg.matches("<rect").count(),
1326            3,
1327            "root + two children:\n{svg}"
1328        );
1329        assert!(
1330            svg.contains("root — 0.000ms (100.00%)"),
1331            "real hover tooltip: {svg}"
1332        );
1333        assert!(
1334            svg.contains("(75.00%)") && svg.contains("(25.00%)"),
1335            "real shares: {svg}"
1336        );
1337        // "cold" sorts before "hot", so it is laid out first at x = 0 with a
1338        // quarter of the width.
1339        assert!(svg.contains("width=\"300.00\""), "25% of 1200px: {svg}");
1340        assert!(svg.contains("width=\"900.00\""), "75% of 1200px: {svg}");
1341    }
1342
1343    #[test]
1344    fn flame_graph_frame_names_cannot_inject_markup() {
1345        let root = FlameGraphNode {
1346            name: "</svg><script>x</script>".to_string(),
1347            value: 1,
1348            delta: None,
1349            children: HashMap::new(),
1350            total_value: 1,
1351            self_value: 1,
1352            percentage: 100.0,
1353            color: None,
1354            metadata: HashMap::new(),
1355        };
1356        let svg = FlameGraphProfiler::render_flame_svg(&root);
1357        assert!(!svg.contains("<script>"));
1358        assert_eq!(svg.matches("</svg>").count(), 1);
1359    }
1360
1361    fn make_config() -> FlameGraphConfig {
1362        FlameGraphConfig {
1363            sampling_rate: 100,
1364            min_width: 0.1,
1365            color_scheme: FlameGraphColorScheme::Hot,
1366            direction: FlameGraphDirection::TopDown,
1367            title: "Test Profile".to_string(),
1368            subtitle: None,
1369            include_memory: false,
1370            include_gpu: false,
1371            differential_mode: false,
1372            merge_similar_stacks: false,
1373            filter_noise: false,
1374            noise_threshold: 0.01,
1375        }
1376    }
1377
1378    fn make_sample(func_name: &str, duration_ns: u64) -> FlameGraphSample {
1379        FlameGraphSample {
1380            stack: vec![StackFrame {
1381                function_name: func_name.to_string(),
1382                module_name: None,
1383                file_name: None,
1384                line_number: None,
1385                address: None,
1386            }],
1387            duration_ns,
1388            timestamp: 1000,
1389            thread_id: 1,
1390            cpu_id: Some(0),
1391            memory_usage: None,
1392            gpu_kernel: None,
1393            metadata: HashMap::new(),
1394        }
1395    }
1396
1397    fn make_nested_sample(funcs: &[&str], duration_ns: u64) -> FlameGraphSample {
1398        let stack = funcs
1399            .iter()
1400            .map(|name| StackFrame {
1401                function_name: name.to_string(),
1402                module_name: None,
1403                file_name: None,
1404                line_number: None,
1405                address: None,
1406            })
1407            .collect();
1408        FlameGraphSample {
1409            stack,
1410            duration_ns,
1411            timestamp: 1000,
1412            thread_id: 1,
1413            cpu_id: None,
1414            memory_usage: None,
1415            gpu_kernel: None,
1416            metadata: HashMap::new(),
1417        }
1418    }
1419
1420    #[test]
1421    fn test_flame_graph_profiler_creation() {
1422        let profiler = FlameGraphProfiler::new(make_config());
1423        assert!(profiler.samples.is_empty());
1424        assert!(profiler.root_node.is_none());
1425    }
1426
1427    #[test]
1428    fn test_start_sampling() {
1429        let mut profiler = FlameGraphProfiler::new(make_config());
1430        let result = profiler.start_sampling();
1431        assert!(result.is_ok());
1432        assert!(profiler.sampling_timer.is_some());
1433    }
1434
1435    #[test]
1436    fn test_add_sample() {
1437        let mut profiler = FlameGraphProfiler::new(make_config());
1438        profiler.add_sample(make_sample("main", 1000));
1439        assert_eq!(profiler.samples.len(), 1);
1440    }
1441
1442    #[test]
1443    fn test_add_multiple_samples() {
1444        let mut profiler = FlameGraphProfiler::new(make_config());
1445        for i in 0..10 {
1446            profiler.add_sample(make_sample(&format!("func_{}", i), (i + 1) * 100));
1447        }
1448        assert_eq!(profiler.samples.len(), 10);
1449    }
1450
1451    #[test]
1452    fn test_sample_gpu_kernel() {
1453        let mut profiler = FlameGraphProfiler::new(make_config());
1454        profiler.sample_gpu_kernel("matmul_kernel", 5000);
1455        assert_eq!(profiler.samples.len(), 1);
1456        assert_eq!(
1457            profiler.samples[0].gpu_kernel,
1458            Some("matmul_kernel".to_string())
1459        );
1460    }
1461
1462    #[test]
1463    fn test_build_flame_graph_empty() {
1464        let mut profiler = FlameGraphProfiler::new(make_config());
1465        let result = profiler.build_flame_graph();
1466        assert!(result.is_err());
1467    }
1468
1469    #[test]
1470    fn test_build_flame_graph_single_sample() {
1471        let mut profiler = FlameGraphProfiler::new(make_config());
1472        profiler.add_sample(make_sample("main", 1000));
1473        let result = profiler.build_flame_graph();
1474        assert!(result.is_ok());
1475        assert!(profiler.root_node.is_some());
1476    }
1477
1478    #[test]
1479    fn test_build_flame_graph_nested_stacks() {
1480        let mut profiler = FlameGraphProfiler::new(make_config());
1481        profiler.add_sample(make_nested_sample(&["main", "compute", "matmul"], 5000));
1482        profiler.add_sample(make_nested_sample(&["main", "compute", "softmax"], 3000));
1483        profiler.add_sample(make_nested_sample(&["main", "io", "load_data"], 2000));
1484        let result = profiler.build_flame_graph();
1485        assert!(result.is_ok());
1486        let root = profiler.root_node.as_ref().expect("root should exist");
1487        assert!(root.children.contains_key("main"));
1488    }
1489
1490    #[test]
1491    fn test_set_baseline() {
1492        let mut profiler = FlameGraphProfiler::new(make_config());
1493        profiler.add_sample(make_sample("func_a", 100));
1494        profiler.add_sample(make_sample("func_b", 200));
1495        profiler.set_baseline();
1496        assert!(profiler.baseline_samples.is_some());
1497        let baseline = profiler.baseline_samples.as_ref().expect("baseline should exist");
1498        assert_eq!(baseline.len(), 2);
1499    }
1500
1501    #[test]
1502    fn test_performance_counters() {
1503        let mut profiler = FlameGraphProfiler::new(make_config());
1504        let start_result = profiler.start_sampling();
1505        assert!(start_result.is_ok());
1506        profiler.add_sample(make_nested_sample(&["a", "b", "c"], 100));
1507        profiler.add_sample(make_nested_sample(&["a", "d"], 200));
1508        let counter = profiler.performance_counters.get("samples_collected");
1509        assert_eq!(counter, Some(&2));
1510        let depth = profiler.performance_counters.get("stack_depth_max");
1511        assert_eq!(depth, Some(&3));
1512    }
1513
1514    #[test]
1515    fn test_stop_sampling_with_data() {
1516        let mut profiler = FlameGraphProfiler::new(make_config());
1517        let _ = profiler.start_sampling();
1518        profiler.add_sample(make_sample("test_func", 500));
1519        let result = profiler.stop_sampling();
1520        assert!(result.is_ok());
1521        assert!(profiler.sampling_timer.is_none());
1522        assert!(profiler.root_node.is_some());
1523    }
1524
1525    #[test]
1526    fn test_flame_graph_node_structure() {
1527        let mut profiler = FlameGraphProfiler::new(make_config());
1528        profiler.add_sample(make_nested_sample(&["root_fn", "child_fn"], 1000));
1529        profiler.add_sample(make_nested_sample(&["root_fn", "child_fn"], 2000));
1530        let _ = profiler.build_flame_graph();
1531        let root = profiler.root_node.as_ref().expect("root should exist");
1532        assert_eq!(root.name, "root");
1533    }
1534
1535    #[test]
1536    fn test_stack_frame_equality() {
1537        let frame1 = StackFrame {
1538            function_name: "test".to_string(),
1539            module_name: None,
1540            file_name: None,
1541            line_number: None,
1542            address: None,
1543        };
1544        let frame2 = StackFrame {
1545            function_name: "test".to_string(),
1546            module_name: None,
1547            file_name: None,
1548            line_number: None,
1549            address: None,
1550        };
1551        assert_eq!(frame1, frame2);
1552    }
1553
1554    #[test]
1555    fn test_flame_graph_config_differential_mode() {
1556        let mut config = make_config();
1557        config.differential_mode = true;
1558        let mut profiler = FlameGraphProfiler::new(config);
1559        profiler.add_sample(make_sample("func_a", 100));
1560        profiler.set_baseline();
1561        profiler.add_sample(make_sample("func_a", 200));
1562        let result = profiler.build_flame_graph();
1563        assert!(result.is_ok());
1564    }
1565
1566    #[test]
1567    fn test_flame_graph_config_noise_filter() {
1568        let mut config = make_config();
1569        config.filter_noise = true;
1570        config.noise_threshold = 0.05;
1571        let mut profiler = FlameGraphProfiler::new(config);
1572        profiler.add_sample(make_nested_sample(&["main", "big_func"], 10000));
1573        profiler.add_sample(make_nested_sample(&["main", "tiny_func"], 1));
1574        let result = profiler.build_flame_graph();
1575        assert!(result.is_ok());
1576    }
1577
1578    #[test]
1579    fn test_sample_current_stack() {
1580        let mut profiler = FlameGraphProfiler::new(make_config());
1581        let result = profiler.sample_current_stack(500);
1582        assert!(result.is_ok());
1583        assert_eq!(profiler.samples.len(), 1);
1584    }
1585
1586    #[tokio::test]
1587    async fn test_export_no_graph() {
1588        let profiler = FlameGraphProfiler::new(make_config());
1589        let tmp = std::env::temp_dir().join("test_flamegraph_export.json");
1590        let result = profiler.export(FlameGraphExportFormat::JSON, &tmp).await;
1591        assert!(result.is_err());
1592    }
1593
1594    #[tokio::test]
1595    async fn test_export_json() {
1596        let mut profiler = FlameGraphProfiler::new(make_config());
1597        profiler.add_sample(make_sample("test", 100));
1598        let _ = profiler.build_flame_graph();
1599        let tmp = std::env::temp_dir().join("test_flamegraph_export_ok.json");
1600        let result = profiler.export(FlameGraphExportFormat::JSON, &tmp).await;
1601        assert!(result.is_ok());
1602        let _ = tokio::fs::remove_file(&tmp).await;
1603    }
1604
1605    #[test]
1606    fn test_flame_graph_color_schemes() {
1607        let schemes = [
1608            FlameGraphColorScheme::Hot,
1609            FlameGraphColorScheme::Cool,
1610            FlameGraphColorScheme::Java,
1611            FlameGraphColorScheme::Memory,
1612            FlameGraphColorScheme::Differential,
1613            FlameGraphColorScheme::Random,
1614            FlameGraphColorScheme::Custom(HashMap::new()),
1615        ];
1616        assert_eq!(schemes.len(), 7);
1617    }
1618
1619    #[test]
1620    fn test_flame_graph_directions() {
1621        let directions = [FlameGraphDirection::TopDown, FlameGraphDirection::BottomUp];
1622        assert_eq!(directions.len(), 2);
1623    }
1624
1625    #[test]
1626    fn test_flame_graph_export_formats() {
1627        let formats = [
1628            FlameGraphExportFormat::SVG,
1629            FlameGraphExportFormat::InteractiveHTML,
1630            FlameGraphExportFormat::JSON,
1631            FlameGraphExportFormat::Speedscope,
1632            FlameGraphExportFormat::D3,
1633            FlameGraphExportFormat::Folded,
1634        ];
1635        assert_eq!(formats.len(), 6);
1636    }
1637
1638    #[test]
1639    fn test_flame_graph_config_with_subtitle() {
1640        let mut config = make_config();
1641        config.subtitle = Some("Test Subtitle".to_string());
1642        let profiler = FlameGraphProfiler::new(config);
1643        assert!(profiler.samples.is_empty());
1644    }
1645
1646    #[test]
1647    fn test_flame_graph_config_with_gpu_and_memory() {
1648        let mut config = make_config();
1649        config.include_gpu = true;
1650        config.include_memory = true;
1651        let profiler = FlameGraphProfiler::new(config);
1652        assert!(profiler.samples.is_empty());
1653    }
1654
1655    #[test]
1656    fn test_flame_graph_node_creation() {
1657        let node = FlameGraphNode {
1658            name: "test_func".to_string(),
1659            value: 1000,
1660            delta: None,
1661            children: HashMap::new(),
1662            total_value: 1000,
1663            self_value: 500,
1664            percentage: 50.0,
1665            color: Some("#ff0000".to_string()),
1666            metadata: HashMap::new(),
1667        };
1668        assert_eq!(node.name, "test_func");
1669        assert_eq!(node.value, 1000);
1670        assert!((node.percentage - 50.0).abs() < f64::EPSILON);
1671    }
1672
1673    #[test]
1674    fn test_stack_frame_with_all_fields() {
1675        let frame = StackFrame {
1676            function_name: "my_func".to_string(),
1677            module_name: Some("my_module".to_string()),
1678            file_name: Some("src/lib.rs".to_string()),
1679            line_number: Some(42),
1680            address: Some(0x12345678),
1681        };
1682        assert_eq!(frame.function_name, "my_func");
1683        assert_eq!(frame.module_name, Some("my_module".to_string()));
1684        assert_eq!(frame.line_number, Some(42));
1685    }
1686}