1#![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#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FlameGraphNode {
18 pub name: String,
19 pub value: u64,
20 pub delta: Option<i64>, 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
29fn 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("&"),
36 '<' => out.push_str("<"),
37 '>' => out.push_str(">"),
38 '"' => out.push_str("""),
39 '\'' => out.push_str("'"),
40 _ => out.push(ch),
41 }
42 }
43 out
44}
45
46#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct FlameGraphConfig {
72 pub sampling_rate: u32, pub min_width: f64, 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, Cool, Java, Memory, Differential, Random, Custom(HashMap<String, String>),
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub enum FlameGraphDirection {
99 TopDown, BottomUp, }
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum FlameGraphExportFormat {
106 SVG,
107 InteractiveHTML,
108 JSON,
109 Speedscope,
110 D3,
111 Folded,
112}
113
114#[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>>, 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 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 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 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 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 pub fn add_sample(&mut self, sample: FlameGraphSample) {
175 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 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 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, 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 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 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 for sample in &self.samples {
261 self.merge_sample_into_tree(&mut root, sample);
262 }
263
264 self.calculate_node_metrics(&mut root);
266
267 if self.config.differential_mode && self.baseline_samples.is_some() {
269 self.apply_differential_analysis(&mut root)?;
270 }
271
272 if self.config.filter_noise {
274 self.filter_noise_nodes(&mut root);
275 }
276
277 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 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 async fn export_svg(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
312 let mut svg_content = String::new();
313
314 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 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 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 const FLAME_ROW_HEIGHT: f64 = 18.0;
403 const FLAME_WIDTH: f64 = 1200.0;
405
406 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 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 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 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 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 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 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 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 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 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 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 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 if let Some(frame) = frames.last_mut() {
601 let mut parts = rest.rsplitn(3, ':');
602 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 (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 (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 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 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 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 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 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 let total_for_percentage = if let Some(root) = &self.root_node {
741 root.total_value
742 } else {
743 node.total_value };
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 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 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(¤t.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; 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 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(), Some(delta) if delta < 0 => "#44ff44".to_string(), _ => "#cccccc".to_string(), }
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 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, });
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#[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#[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#[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#[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#[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
1103impl Default for FlameGraphConfig {
1105 fn default() -> Self {
1106 Self {
1107 sampling_rate: 1000, 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, }
1120 }
1121}
1122
1123impl Profiler {
1125 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 pub async fn start_flame_graph_profiling(&mut self) -> Result<()> {
1137 tracing::info!("Starting integrated flame graph profiling");
1139 Ok(())
1140 }
1141
1142 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 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 _ => {}, }
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 #[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 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 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 assert!(
1262 sample.stack.iter().all(|f| f.function_name != "captured_function"),
1263 "{:?}",
1264 sample.stack
1265 );
1266 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 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}