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
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct StackFrame {
32 pub function_name: String,
33 pub module_name: Option<String>,
34 pub file_name: Option<String>,
35 pub line_number: Option<u32>,
36 pub address: Option<u64>,
37}
38
39#[derive(Debug, Clone)]
41pub struct FlameGraphSample {
42 pub stack: Vec<StackFrame>,
43 pub duration_ns: u64,
44 pub timestamp: u64,
45 pub thread_id: u64,
46 pub cpu_id: Option<u32>,
47 pub memory_usage: Option<usize>,
48 pub gpu_kernel: Option<String>,
49 pub metadata: HashMap<String, String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct FlameGraphConfig {
55 pub sampling_rate: u32, pub min_width: f64, pub color_scheme: FlameGraphColorScheme,
58 pub direction: FlameGraphDirection,
59 pub title: String,
60 pub subtitle: Option<String>,
61 pub include_memory: bool,
62 pub include_gpu: bool,
63 pub differential_mode: bool,
64 pub merge_similar_stacks: bool,
65 pub filter_noise: bool,
66 pub noise_threshold: f64,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub enum FlameGraphColorScheme {
71 Hot, Cool, Java, Memory, Differential, Random, Custom(HashMap<String, String>),
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum FlameGraphDirection {
82 TopDown, BottomUp, }
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub enum FlameGraphExportFormat {
89 SVG,
90 InteractiveHTML,
91 JSON,
92 Speedscope,
93 D3,
94 Folded,
95}
96
97#[derive(Debug)]
99pub struct FlameGraphProfiler {
100 config: FlameGraphConfig,
101 samples: Vec<FlameGraphSample>,
102 sampling_timer: Option<Instant>,
103 root_node: Option<FlameGraphNode>,
104 baseline_samples: Option<Vec<FlameGraphSample>>, metadata: HashMap<String, String>,
106 current_cpu_usage: f64,
107 current_memory_usage: usize,
108 performance_counters: HashMap<String, u64>,
109}
110
111impl FlameGraphProfiler {
112 pub fn new(config: FlameGraphConfig) -> Self {
114 Self {
115 config,
116 samples: Vec::new(),
117 sampling_timer: None,
118 root_node: None,
119 baseline_samples: None,
120 metadata: HashMap::new(),
121 current_cpu_usage: 0.0,
122 current_memory_usage: 0,
123 performance_counters: HashMap::new(),
124 }
125 }
126
127 pub fn start_sampling(&mut self) -> Result<()> {
129 tracing::info!(
130 "Starting flame graph sampling at {} Hz",
131 self.config.sampling_rate
132 );
133 self.sampling_timer = Some(Instant::now());
134 self.samples.clear();
135 self.root_node = None;
136
137 self.performance_counters.insert("samples_collected".to_string(), 0);
139 self.performance_counters.insert("stack_depth_max".to_string(), 0);
140 self.performance_counters.insert("unique_functions".to_string(), 0);
141
142 Ok(())
143 }
144
145 pub fn stop_sampling(&mut self) -> Result<()> {
147 tracing::info!(
148 "Stopping flame graph sampling, collected {} samples",
149 self.samples.len()
150 );
151 self.sampling_timer = None;
152 self.build_flame_graph()?;
153 Ok(())
154 }
155
156 pub fn add_sample(&mut self, sample: FlameGraphSample) {
158 if let Some(counter) = self.performance_counters.get_mut("samples_collected") {
160 *counter += 1;
161 }
162
163 let stack_depth = sample.stack.len() as u64;
164 if let Some(max_depth) = self.performance_counters.get_mut("stack_depth_max") {
165 if stack_depth > *max_depth {
166 *max_depth = stack_depth;
167 }
168 }
169
170 self.samples.push(sample);
171 }
172
173 pub fn sample_current_stack(&mut self, duration_ns: u64) -> Result<()> {
175 let stack = self.capture_stack_trace()?;
176 let sample = FlameGraphSample {
177 stack,
178 duration_ns,
179 timestamp: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_nanos() as u64,
180 thread_id: self.get_current_thread_id(),
181 cpu_id: self.get_current_cpu_id(),
182 memory_usage: Some(self.current_memory_usage),
183 gpu_kernel: None,
184 metadata: HashMap::new(),
185 };
186
187 self.add_sample(sample);
188 Ok(())
189 }
190
191 pub fn sample_gpu_kernel(&mut self, kernel_name: &str, duration_ns: u64) {
193 let stack = vec![StackFrame {
194 function_name: format!("GPU::{}", kernel_name),
195 module_name: Some("GPU".to_string()),
196 file_name: None,
197 line_number: None,
198 address: None,
199 }];
200
201 let sample = FlameGraphSample {
202 stack,
203 duration_ns,
204 timestamp: SystemTime::now()
205 .duration_since(SystemTime::UNIX_EPOCH)
206 .unwrap_or_default()
207 .as_nanos() as u64,
208 thread_id: 0, cpu_id: None,
210 memory_usage: None,
211 gpu_kernel: Some(kernel_name.to_string()),
212 metadata: [("type".to_string(), "gpu".to_string())].into_iter().collect(),
213 };
214
215 self.add_sample(sample);
216 }
217
218 pub fn set_baseline(&mut self) {
220 self.baseline_samples = Some(self.samples.clone());
221 tracing::info!("Set baseline with {} samples", self.samples.len());
222 }
223
224 pub fn build_flame_graph(&mut self) -> Result<()> {
226 if self.samples.is_empty() {
227 return Err(anyhow::anyhow!("No samples collected"));
228 }
229
230 let mut root = FlameGraphNode {
231 name: "root".to_string(),
232 value: 0,
233 delta: None,
234 children: HashMap::new(),
235 total_value: 0,
236 self_value: 0,
237 percentage: 100.0,
238 color: None,
239 metadata: HashMap::new(),
240 };
241
242 for sample in &self.samples {
244 self.merge_sample_into_tree(&mut root, sample);
245 }
246
247 self.calculate_node_metrics(&mut root);
249
250 if self.config.differential_mode && self.baseline_samples.is_some() {
252 self.apply_differential_analysis(&mut root)?;
253 }
254
255 if self.config.filter_noise {
257 self.filter_noise_nodes(&mut root);
258 }
259
260 let unique_functions = self.count_unique_functions(&root);
262 if let Some(counter) = self.performance_counters.get_mut("unique_functions") {
263 *counter = unique_functions;
264 }
265
266 self.root_node = Some(root);
267 tracing::info!(
268 "Built flame graph with {} unique functions",
269 unique_functions
270 );
271 Ok(())
272 }
273
274 pub async fn export(&self, format: FlameGraphExportFormat, output_path: &Path) -> Result<()> {
276 let root = self
277 .root_node
278 .as_ref()
279 .ok_or_else(|| anyhow::anyhow!("Flame graph not built yet"))?;
280
281 match format {
282 FlameGraphExportFormat::SVG => self.export_svg(root, output_path).await,
283 FlameGraphExportFormat::InteractiveHTML => {
284 self.export_interactive_html(root, output_path).await
285 },
286 FlameGraphExportFormat::JSON => self.export_json(root, output_path).await,
287 FlameGraphExportFormat::Speedscope => self.export_speedscope(root, output_path).await,
288 FlameGraphExportFormat::D3 => self.export_d3(root, output_path).await,
289 FlameGraphExportFormat::Folded => self.export_folded(output_path).await,
290 }
291 }
292
293 async fn export_svg(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
295 let mut svg_content = String::new();
296
297 svg_content.push_str(&format!(
299 r##"<?xml version="1.0" encoding="UTF-8"?>
300<svg width="1200" height="800" xmlns="http://www.w3.org/2000/svg">
301<defs>
302 <linearGradient id="background" x1="0%" y1="0%" x2="0%" y2="100%">
303 <stop offset="0%" style="stop-color:#eeeeee"/>
304 <stop offset="100%" style="stop-color:#eeeeb0"/>
305 </linearGradient>
306</defs>
307<rect width="100%" height="100%" fill="url(#background)"/>
308<text x="600" y="24" text-anchor="middle" font-size="17" font-family="Verdana">{}</text>
309<text x="600" y="44" text-anchor="middle" font-size="12" font-family="Verdana" fill="#999">
310 {} samples, {} functions
311</text>
312"##,
313 self.config.title,
314 self.samples.len(),
315 self.count_unique_functions(root)
316 ));
317
318 self.render_svg_node(&mut svg_content, root, 0, 0, 1200, 0)?;
320
321 svg_content.push_str("</svg>");
322
323 tokio::fs::write(output_path, svg_content).await?;
324 tracing::info!("Exported SVG flame graph to {:?}", output_path);
325 Ok(())
326 }
327
328 async fn export_interactive_html(
330 &self,
331 root: &FlameGraphNode,
332 output_path: &Path,
333 ) -> Result<()> {
334 let json_data = serde_json::to_string(root)?;
335
336 let html_content = format!(
337 r#"<!DOCTYPE html>
338<html>
339<head>
340 <title>{}</title>
341 <meta charset="utf-8">
342 <style>
343 body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; }}
344 .flame-graph {{ width: 100%; height: 600px; border: 1px solid #ccc; }}
345 .tooltip {{ position: absolute; background: rgba(0,0,0,0.8); color: white;
346 padding: 10px; border-radius: 4px; pointer-events: none; z-index: 1000; }}
347 .controls {{ margin-bottom: 20px; }}
348 .info {{ margin-top: 20px; font-size: 14px; color: #666; }}
349 </style>
350 <script src="https://d3js.org/d3.v7.min.js"></script>
351</head>
352<body>
353 <h1>{}</h1>
354 <div class="controls">
355 <button onclick="resetZoom()">Reset Zoom</button>
356 <button onclick="searchFunction()">Search</button>
357 <input type="text" id="searchInput" placeholder="Function name...">
358 </div>
359 <div id="flame-graph" class="flame-graph"></div>
360 <div class="info">
361 <p>Samples: {} | Functions: {} | Total Time: {:.2}ms</p>
362 <p>Click to zoom, double-click to reset. Hover for details.</p>
363 </div>
364 <div id="tooltip" class="tooltip" style="display: none;"></div>
365
366 <script>
367 const data = {};
368 // Interactive flame graph implementation would go here
369 // This is a simplified version - full implementation would include D3.js visualization
370 console.log('Flame graph data loaded:', data);
371 </script>
372</body>
373</html>"#,
374 self.config.title,
375 self.config.title,
376 self.samples.len(),
377 self.count_unique_functions(root),
378 root.total_value as f64 / 1_000_000.0, json_data
380 );
381
382 tokio::fs::write(output_path, html_content).await?;
383 tracing::info!("Exported interactive HTML flame graph to {:?}", output_path);
384 Ok(())
385 }
386
387 async fn export_json(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
389 let json_data = serde_json::to_string_pretty(root)?;
390 tokio::fs::write(output_path, json_data).await?;
391 tracing::info!("Exported JSON flame graph to {:?}", output_path);
392 Ok(())
393 }
394
395 async fn export_speedscope(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
397 let speedscope_data = self.convert_to_speedscope_format(root)?;
398 let json_data = serde_json::to_string_pretty(&speedscope_data)?;
399 tokio::fs::write(output_path, json_data).await?;
400 tracing::info!("Exported Speedscope format to {:?}", output_path);
401 Ok(())
402 }
403
404 async fn export_d3(&self, root: &FlameGraphNode, output_path: &Path) -> Result<()> {
406 let d3_data = self.convert_to_d3_format(root)?;
407 let json_data = serde_json::to_string_pretty(&d3_data)?;
408 tokio::fs::write(output_path, json_data).await?;
409 tracing::info!("Exported D3 format to {:?}", output_path);
410 Ok(())
411 }
412
413 async fn export_folded(&self, output_path: &Path) -> Result<()> {
415 let mut folded_content = String::new();
416
417 for sample in &self.samples {
418 let stack_str: Vec<String> =
419 sample.stack.iter().map(|frame| frame.function_name.clone()).collect();
420 folded_content.push_str(&format!("{} {}\n", stack_str.join(";"), sample.duration_ns));
421 }
422
423 tokio::fs::write(output_path, folded_content).await?;
424 tracing::info!("Exported folded format to {:?}", output_path);
425 Ok(())
426 }
427
428 pub fn get_analysis_report(&self) -> FlameGraphAnalysisReport {
430 let root = self.root_node.as_ref();
431
432 FlameGraphAnalysisReport {
433 total_samples: self.samples.len(),
434 total_duration_ns: self.samples.iter().map(|s| s.duration_ns).sum(),
435 unique_functions: root.map(|r| self.count_unique_functions(r)).unwrap_or(0),
436 max_stack_depth: self.performance_counters.get("stack_depth_max").copied().unwrap_or(0),
437 hot_functions: self.get_hot_functions(10),
438 memory_usage_stats: self.get_memory_usage_stats(),
439 gpu_kernel_stats: self.get_gpu_kernel_stats(),
440 differential_analysis: self.get_differential_analysis(),
441 performance_insights: self.generate_performance_insights(),
442 }
443 }
444
445 fn capture_stack_trace(&self) -> Result<Vec<StackFrame>> {
448 Ok(vec![StackFrame {
451 function_name: "captured_function".to_string(),
452 module_name: Some("trustformers_debug".to_string()),
453 file_name: Some("profiler.rs".to_string()),
454 line_number: Some(1800),
455 address: None,
456 }])
457 }
458
459 fn get_current_thread_id(&self) -> u64 {
460 1
462 }
463
464 fn get_current_cpu_id(&self) -> Option<u32> {
465 Some(0)
467 }
468
469 fn merge_sample_into_tree(&self, node: &mut FlameGraphNode, sample: &FlameGraphSample) {
470 if sample.stack.is_empty() {
471 node.value += sample.duration_ns;
472 return;
473 }
474
475 let frame = &sample.stack[0];
476 let child =
477 node.children
478 .entry(frame.function_name.clone())
479 .or_insert_with(|| FlameGraphNode {
480 name: frame.function_name.clone(),
481 value: 0,
482 delta: None,
483 children: HashMap::new(),
484 total_value: 0,
485 self_value: 0,
486 percentage: 0.0,
487 color: None,
488 metadata: HashMap::new(),
489 });
490
491 if sample.stack.len() == 1 {
492 child.value += sample.duration_ns;
493 } else {
494 let mut remaining_sample = sample.clone();
495 remaining_sample.stack = sample.stack[1..].to_vec();
496 self.merge_sample_into_tree(child, &remaining_sample);
497 }
498 }
499
500 fn calculate_node_metrics(&self, node: &mut FlameGraphNode) {
501 let mut total_children_value = 0;
502
503 for child in node.children.values_mut() {
504 self.calculate_node_metrics(child);
505 total_children_value += child.total_value;
506 }
507
508 node.total_value = node.value + total_children_value;
509 node.self_value = node.value;
510
511 if node.total_value > 0 && node.name != "root" {
512 let total_for_percentage = if let Some(root) = &self.root_node {
514 root.total_value
515 } else {
516 node.total_value };
518
519 if total_for_percentage > 0 {
520 node.percentage = (node.total_value as f64 / total_for_percentage as f64) * 100.0;
521 }
522 }
523 }
524
525 fn apply_differential_analysis(&self, node: &mut FlameGraphNode) -> Result<()> {
526 if let Some(baseline_samples) = &self.baseline_samples {
527 let mut baseline_root = FlameGraphNode {
529 name: "root".to_string(),
530 value: 0,
531 delta: None,
532 children: HashMap::new(),
533 total_value: 0,
534 self_value: 0,
535 percentage: 100.0,
536 color: None,
537 metadata: HashMap::new(),
538 };
539
540 for sample in baseline_samples {
541 self.merge_sample_into_tree(&mut baseline_root, sample);
542 }
543
544 self.calculate_deltas(node, &baseline_root);
546 }
547 Ok(())
548 }
549
550 fn calculate_deltas(&self, current: &mut FlameGraphNode, baseline: &FlameGraphNode) {
551 let baseline_value =
552 baseline.children.get(¤t.name).map(|n| n.total_value as i64).unwrap_or(0);
553
554 current.delta = Some(current.total_value as i64 - baseline_value);
555
556 for (name, child) in &mut current.children {
557 if let Some(baseline_child) = baseline.children.get(name) {
558 self.calculate_deltas(child, baseline_child);
559 } else {
560 child.delta = Some(child.total_value as i64);
561 }
562 }
563 }
564
565 fn filter_noise_nodes(&self, node: &mut FlameGraphNode) {
566 let threshold = (node.total_value as f64 * self.config.noise_threshold / 100.0) as u64;
567
568 node.children.retain(|_, child| {
569 self.filter_noise_nodes(child);
570 child.total_value >= threshold
571 });
572 }
573
574 fn count_unique_functions(&self, node: &FlameGraphNode) -> u64 {
575 let mut count = 1; for child in node.children.values() {
577 count += self.count_unique_functions(child);
578 }
579 count
580 }
581
582 fn render_svg_node(
583 &self,
584 svg: &mut String,
585 node: &FlameGraphNode,
586 x: i32,
587 y: i32,
588 width: i32,
589 depth: i32,
590 ) -> Result<()> {
591 if width < 1 {
592 return Ok(());
593 }
594
595 let height = 20;
596 let color = self.get_node_color(node);
597
598 svg.push_str(&format!(
599 r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" stroke="white" stroke-width="0.5">
600<title>{}: {:.2}% ({} samples)</title>
601</rect>
602<text x="{}" y="{}" font-size="12" font-family="Verdana" fill="black">{}</text>
603"#,
604 x, y + depth * height, width, height,
605 color,
606 node.name, node.percentage, node.value,
607 x + 2, y + depth * height + 14,
608 if width > 50 { &node.name } else { "" }
609 ));
610
611 let mut child_x = x;
613 for child in node.children.values() {
614 let child_width = if node.total_value > 0 {
615 (width as f64 * child.total_value as f64 / node.total_value as f64) as i32
616 } else {
617 0
618 };
619 if child_width > 0 {
620 self.render_svg_node(svg, child, child_x, y, child_width, depth + 1)?;
621 child_x += child_width;
622 }
623 }
624
625 Ok(())
626 }
627
628 fn get_node_color(&self, node: &FlameGraphNode) -> String {
629 match &self.config.color_scheme {
630 FlameGraphColorScheme::Hot => {
631 let intensity = (node.percentage / 100.0 * 255.0) as u8;
632 format!("rgb({}, {}, 0)", 255, 255 - intensity)
633 },
634 FlameGraphColorScheme::Cool => {
635 let intensity = (node.percentage / 100.0 * 255.0) as u8;
636 format!("rgb(0, {}, {})", intensity, 255)
637 },
638 FlameGraphColorScheme::Memory => {
639 if node.name.contains("alloc") || node.name.contains("malloc") {
640 "#ff6b6b".to_string()
641 } else {
642 "#4ecdc4".to_string()
643 }
644 },
645 FlameGraphColorScheme::Differential => {
646 match node.delta {
647 Some(delta) if delta > 0 => "#ff4444".to_string(), Some(delta) if delta < 0 => "#44ff44".to_string(), _ => "#cccccc".to_string(), }
651 },
652 FlameGraphColorScheme::Java => "#ff9800".to_string(),
653 FlameGraphColorScheme::Random => {
654 let hash = self.hash_string(&node.name);
655 format!("hsl({}, 70%, 60%)", hash % 360)
656 },
657 FlameGraphColorScheme::Custom(colors) => {
658 colors.get(&node.name).cloned().unwrap_or_else(|| "#cccccc".to_string())
659 },
660 }
661 }
662
663 fn hash_string(&self, s: &str) -> u32 {
664 let mut hash = 0u32;
665 for byte in s.bytes() {
666 hash = hash.wrapping_mul(31).wrapping_add(byte as u32);
667 }
668 hash
669 }
670
671 fn convert_to_speedscope_format(&self, root: &FlameGraphNode) -> Result<serde_json::Value> {
672 Ok(serde_json::json!({
674 "version": "0.7.1",
675 "profiles": [{
676 "type": "sampled",
677 "name": self.config.title,
678 "unit": "nanoseconds",
679 "startValue": 0,
680 "endValue": root.total_value,
681 "samples": [],
682 "weights": []
683 }]
684 }))
685 }
686
687 fn convert_to_d3_format(&self, root: &FlameGraphNode) -> Result<serde_json::Value> {
688 Ok(serde_json::to_value(root)?)
689 }
690
691 fn get_hot_functions(&self, limit: usize) -> Vec<HotFunctionInfo> {
692 let mut functions = Vec::new();
693
694 if let Some(root) = &self.root_node {
695 self.collect_hot_functions(root, &mut functions);
696 }
697
698 functions.sort_by_key(|item| std::cmp::Reverse(item.total_time_ns));
699 functions.truncate(limit);
700 functions
701 }
702
703 fn collect_hot_functions(&self, node: &FlameGraphNode, functions: &mut Vec<HotFunctionInfo>) {
704 functions.push(HotFunctionInfo {
705 name: node.name.clone(),
706 total_time_ns: node.total_value,
707 self_time_ns: node.self_value,
708 percentage: node.percentage,
709 call_count: 1, });
711
712 for child in node.children.values() {
713 self.collect_hot_functions(child, functions);
714 }
715 }
716
717 fn get_memory_usage_stats(&self) -> MemoryUsageStats {
718 let memory_samples: Vec<usize> =
719 self.samples.iter().filter_map(|s| s.memory_usage).collect();
720
721 if memory_samples.is_empty() {
722 return MemoryUsageStats::default();
723 }
724
725 let total: usize = memory_samples.iter().sum();
726 let max = memory_samples.iter().max().copied().unwrap_or(0);
727 let min = memory_samples.iter().min().copied().unwrap_or(0);
728 let avg = total / memory_samples.len();
729
730 MemoryUsageStats {
731 peak_memory_bytes: max,
732 avg_memory_bytes: avg,
733 min_memory_bytes: min,
734 total_samples: memory_samples.len(),
735 }
736 }
737
738 fn get_gpu_kernel_stats(&self) -> GpuKernelStats {
739 let gpu_samples: Vec<&FlameGraphSample> =
740 self.samples.iter().filter(|s| s.gpu_kernel.is_some()).collect();
741
742 let total_gpu_time: u64 = gpu_samples.iter().map(|s| s.duration_ns).sum();
743 let unique_kernels: std::collections::HashSet<String> =
744 gpu_samples.iter().filter_map(|s| s.gpu_kernel.clone()).collect();
745
746 GpuKernelStats {
747 total_kernel_time_ns: total_gpu_time,
748 unique_kernels: unique_kernels.len(),
749 total_kernel_calls: gpu_samples.len(),
750 }
751 }
752
753 fn get_differential_analysis(&self) -> Option<DifferentialAnalysis> {
754 if !self.config.differential_mode || self.baseline_samples.is_none() {
755 return None;
756 }
757
758 let current_total: u64 = self.samples.iter().map(|s| s.duration_ns).sum();
759 let baseline_total: u64 =
760 self.baseline_samples.as_ref()?.iter().map(|s| s.duration_ns).sum();
761
762 let performance_change = if baseline_total > 0 {
763 ((current_total as f64 - baseline_total as f64) / baseline_total as f64) * 100.0
764 } else {
765 0.0
766 };
767
768 Some(DifferentialAnalysis {
769 baseline_samples: self.baseline_samples.as_ref()?.len(),
770 current_samples: self.samples.len(),
771 performance_change_percent: performance_change,
772 is_regression: performance_change > 5.0,
773 is_improvement: performance_change < -5.0,
774 })
775 }
776
777 fn generate_performance_insights(&self) -> Vec<String> {
778 let mut insights = Vec::new();
779
780 if let Some(root) = &self.root_node {
781 let hot_functions = self.get_hot_functions(3);
782
783 if let Some(hottest) = hot_functions.first() {
784 if hottest.percentage > 50.0 {
785 insights.push(format!(
786 "Function '{}' dominates execution time ({:.1}%)",
787 hottest.name, hottest.percentage
788 ));
789 }
790 }
791
792 let gpu_stats = self.get_gpu_kernel_stats();
793 if gpu_stats.total_kernel_calls > 0 {
794 let gpu_percentage =
795 (gpu_stats.total_kernel_time_ns as f64 / root.total_value as f64) * 100.0;
796 insights.push(format!(
797 "GPU kernels account for {:.1}% of execution time",
798 gpu_percentage
799 ));
800 }
801
802 if let Some(diff) = self.get_differential_analysis() {
803 if diff.is_regression {
804 insights.push(format!(
805 "Performance regression detected: {:.1}% slower than baseline",
806 diff.performance_change_percent
807 ));
808 } else if diff.is_improvement {
809 insights.push(format!(
810 "Performance improvement: {:.1}% faster than baseline",
811 -diff.performance_change_percent
812 ));
813 }
814 }
815 }
816
817 if insights.is_empty() {
818 insights.push("No significant performance patterns detected".to_string());
819 }
820
821 insights
822 }
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize)]
827pub struct HotFunctionInfo {
828 pub name: String,
829 pub total_time_ns: u64,
830 pub self_time_ns: u64,
831 pub percentage: f64,
832 pub call_count: usize,
833}
834
835#[derive(Debug, Clone, Serialize, Deserialize, Default)]
837pub struct MemoryUsageStats {
838 pub peak_memory_bytes: usize,
839 pub avg_memory_bytes: usize,
840 pub min_memory_bytes: usize,
841 pub total_samples: usize,
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct GpuKernelStats {
847 pub total_kernel_time_ns: u64,
848 pub unique_kernels: usize,
849 pub total_kernel_calls: usize,
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
854pub struct DifferentialAnalysis {
855 pub baseline_samples: usize,
856 pub current_samples: usize,
857 pub performance_change_percent: f64,
858 pub is_regression: bool,
859 pub is_improvement: bool,
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct FlameGraphAnalysisReport {
865 pub total_samples: usize,
866 pub total_duration_ns: u64,
867 pub unique_functions: u64,
868 pub max_stack_depth: u64,
869 pub hot_functions: Vec<HotFunctionInfo>,
870 pub memory_usage_stats: MemoryUsageStats,
871 pub gpu_kernel_stats: GpuKernelStats,
872 pub differential_analysis: Option<DifferentialAnalysis>,
873 pub performance_insights: Vec<String>,
874}
875
876impl Default for FlameGraphConfig {
878 fn default() -> Self {
879 Self {
880 sampling_rate: 1000, min_width: 0.01,
882 color_scheme: FlameGraphColorScheme::Hot,
883 direction: FlameGraphDirection::TopDown,
884 title: "Flame Graph".to_string(),
885 subtitle: None,
886 include_memory: true,
887 include_gpu: true,
888 differential_mode: false,
889 merge_similar_stacks: true,
890 filter_noise: true,
891 noise_threshold: 0.1, }
893 }
894}
895
896impl Profiler {
898 pub fn create_flame_graph_profiler(&self) -> FlameGraphProfiler {
900 let config = FlameGraphConfig {
901 title: "TrustformeRS Debug Flame Graph".to_string(),
902 subtitle: Some("Performance Analysis".to_string()),
903 ..Default::default()
904 };
905 FlameGraphProfiler::new(config)
906 }
907
908 pub async fn start_flame_graph_profiling(&mut self) -> Result<()> {
910 tracing::info!("Starting integrated flame graph profiling");
912 Ok(())
913 }
914
915 pub async fn export_flame_graph(
917 &self,
918 format: FlameGraphExportFormat,
919 output_path: &Path,
920 ) -> Result<()> {
921 let mut flame_profiler = self.create_flame_graph_profiler();
922
923 for event in self.get_events() {
925 match event {
926 ProfileEvent::FunctionCall {
927 function_name,
928 duration,
929 ..
930 } => {
931 let sample = FlameGraphSample {
932 stack: vec![StackFrame {
933 function_name: function_name.clone(),
934 module_name: None,
935 file_name: None,
936 line_number: None,
937 address: None,
938 }],
939 duration_ns: duration.as_nanos() as u64,
940 timestamp: 0,
941 thread_id: 0,
942 cpu_id: None,
943 memory_usage: None,
944 gpu_kernel: None,
945 metadata: HashMap::new(),
946 };
947 flame_profiler.add_sample(sample);
948 },
949 ProfileEvent::LayerExecution {
950 layer_name,
951 layer_type,
952 forward_time,
953 ..
954 } => {
955 let sample = FlameGraphSample {
956 stack: vec![
957 StackFrame {
958 function_name: "neural_network".to_string(),
959 module_name: Some("trustformers".to_string()),
960 file_name: None,
961 line_number: None,
962 address: None,
963 },
964 StackFrame {
965 function_name: format!("{}::{}", layer_type, layer_name),
966 module_name: Some("layers".to_string()),
967 file_name: None,
968 line_number: None,
969 address: None,
970 },
971 ],
972 duration_ns: forward_time.as_nanos() as u64,
973 timestamp: 0,
974 thread_id: 0,
975 cpu_id: None,
976 memory_usage: None,
977 gpu_kernel: None,
978 metadata: HashMap::new(),
979 };
980 flame_profiler.add_sample(sample);
981 },
982 _ => {}, }
984 }
985
986 flame_profiler.build_flame_graph()?;
987 flame_profiler.export(format, output_path).await?;
988 Ok(())
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995
996 fn make_config() -> FlameGraphConfig {
997 FlameGraphConfig {
998 sampling_rate: 100,
999 min_width: 0.1,
1000 color_scheme: FlameGraphColorScheme::Hot,
1001 direction: FlameGraphDirection::TopDown,
1002 title: "Test Profile".to_string(),
1003 subtitle: None,
1004 include_memory: false,
1005 include_gpu: false,
1006 differential_mode: false,
1007 merge_similar_stacks: false,
1008 filter_noise: false,
1009 noise_threshold: 0.01,
1010 }
1011 }
1012
1013 fn make_sample(func_name: &str, duration_ns: u64) -> FlameGraphSample {
1014 FlameGraphSample {
1015 stack: vec![StackFrame {
1016 function_name: func_name.to_string(),
1017 module_name: None,
1018 file_name: None,
1019 line_number: None,
1020 address: None,
1021 }],
1022 duration_ns,
1023 timestamp: 1000,
1024 thread_id: 1,
1025 cpu_id: Some(0),
1026 memory_usage: None,
1027 gpu_kernel: None,
1028 metadata: HashMap::new(),
1029 }
1030 }
1031
1032 fn make_nested_sample(funcs: &[&str], duration_ns: u64) -> FlameGraphSample {
1033 let stack = funcs
1034 .iter()
1035 .map(|name| StackFrame {
1036 function_name: name.to_string(),
1037 module_name: None,
1038 file_name: None,
1039 line_number: None,
1040 address: None,
1041 })
1042 .collect();
1043 FlameGraphSample {
1044 stack,
1045 duration_ns,
1046 timestamp: 1000,
1047 thread_id: 1,
1048 cpu_id: None,
1049 memory_usage: None,
1050 gpu_kernel: None,
1051 metadata: HashMap::new(),
1052 }
1053 }
1054
1055 #[test]
1056 fn test_flame_graph_profiler_creation() {
1057 let profiler = FlameGraphProfiler::new(make_config());
1058 assert!(profiler.samples.is_empty());
1059 assert!(profiler.root_node.is_none());
1060 }
1061
1062 #[test]
1063 fn test_start_sampling() {
1064 let mut profiler = FlameGraphProfiler::new(make_config());
1065 let result = profiler.start_sampling();
1066 assert!(result.is_ok());
1067 assert!(profiler.sampling_timer.is_some());
1068 }
1069
1070 #[test]
1071 fn test_add_sample() {
1072 let mut profiler = FlameGraphProfiler::new(make_config());
1073 profiler.add_sample(make_sample("main", 1000));
1074 assert_eq!(profiler.samples.len(), 1);
1075 }
1076
1077 #[test]
1078 fn test_add_multiple_samples() {
1079 let mut profiler = FlameGraphProfiler::new(make_config());
1080 for i in 0..10 {
1081 profiler.add_sample(make_sample(&format!("func_{}", i), (i + 1) * 100));
1082 }
1083 assert_eq!(profiler.samples.len(), 10);
1084 }
1085
1086 #[test]
1087 fn test_sample_gpu_kernel() {
1088 let mut profiler = FlameGraphProfiler::new(make_config());
1089 profiler.sample_gpu_kernel("matmul_kernel", 5000);
1090 assert_eq!(profiler.samples.len(), 1);
1091 assert_eq!(
1092 profiler.samples[0].gpu_kernel,
1093 Some("matmul_kernel".to_string())
1094 );
1095 }
1096
1097 #[test]
1098 fn test_build_flame_graph_empty() {
1099 let mut profiler = FlameGraphProfiler::new(make_config());
1100 let result = profiler.build_flame_graph();
1101 assert!(result.is_err());
1102 }
1103
1104 #[test]
1105 fn test_build_flame_graph_single_sample() {
1106 let mut profiler = FlameGraphProfiler::new(make_config());
1107 profiler.add_sample(make_sample("main", 1000));
1108 let result = profiler.build_flame_graph();
1109 assert!(result.is_ok());
1110 assert!(profiler.root_node.is_some());
1111 }
1112
1113 #[test]
1114 fn test_build_flame_graph_nested_stacks() {
1115 let mut profiler = FlameGraphProfiler::new(make_config());
1116 profiler.add_sample(make_nested_sample(&["main", "compute", "matmul"], 5000));
1117 profiler.add_sample(make_nested_sample(&["main", "compute", "softmax"], 3000));
1118 profiler.add_sample(make_nested_sample(&["main", "io", "load_data"], 2000));
1119 let result = profiler.build_flame_graph();
1120 assert!(result.is_ok());
1121 let root = profiler.root_node.as_ref().expect("root should exist");
1122 assert!(root.children.contains_key("main"));
1123 }
1124
1125 #[test]
1126 fn test_set_baseline() {
1127 let mut profiler = FlameGraphProfiler::new(make_config());
1128 profiler.add_sample(make_sample("func_a", 100));
1129 profiler.add_sample(make_sample("func_b", 200));
1130 profiler.set_baseline();
1131 assert!(profiler.baseline_samples.is_some());
1132 let baseline = profiler.baseline_samples.as_ref().expect("baseline should exist");
1133 assert_eq!(baseline.len(), 2);
1134 }
1135
1136 #[test]
1137 fn test_performance_counters() {
1138 let mut profiler = FlameGraphProfiler::new(make_config());
1139 let start_result = profiler.start_sampling();
1140 assert!(start_result.is_ok());
1141 profiler.add_sample(make_nested_sample(&["a", "b", "c"], 100));
1142 profiler.add_sample(make_nested_sample(&["a", "d"], 200));
1143 let counter = profiler.performance_counters.get("samples_collected");
1144 assert_eq!(counter, Some(&2));
1145 let depth = profiler.performance_counters.get("stack_depth_max");
1146 assert_eq!(depth, Some(&3));
1147 }
1148
1149 #[test]
1150 fn test_stop_sampling_with_data() {
1151 let mut profiler = FlameGraphProfiler::new(make_config());
1152 let _ = profiler.start_sampling();
1153 profiler.add_sample(make_sample("test_func", 500));
1154 let result = profiler.stop_sampling();
1155 assert!(result.is_ok());
1156 assert!(profiler.sampling_timer.is_none());
1157 assert!(profiler.root_node.is_some());
1158 }
1159
1160 #[test]
1161 fn test_flame_graph_node_structure() {
1162 let mut profiler = FlameGraphProfiler::new(make_config());
1163 profiler.add_sample(make_nested_sample(&["root_fn", "child_fn"], 1000));
1164 profiler.add_sample(make_nested_sample(&["root_fn", "child_fn"], 2000));
1165 let _ = profiler.build_flame_graph();
1166 let root = profiler.root_node.as_ref().expect("root should exist");
1167 assert_eq!(root.name, "root");
1168 }
1169
1170 #[test]
1171 fn test_stack_frame_equality() {
1172 let frame1 = StackFrame {
1173 function_name: "test".to_string(),
1174 module_name: None,
1175 file_name: None,
1176 line_number: None,
1177 address: None,
1178 };
1179 let frame2 = StackFrame {
1180 function_name: "test".to_string(),
1181 module_name: None,
1182 file_name: None,
1183 line_number: None,
1184 address: None,
1185 };
1186 assert_eq!(frame1, frame2);
1187 }
1188
1189 #[test]
1190 fn test_flame_graph_config_differential_mode() {
1191 let mut config = make_config();
1192 config.differential_mode = true;
1193 let mut profiler = FlameGraphProfiler::new(config);
1194 profiler.add_sample(make_sample("func_a", 100));
1195 profiler.set_baseline();
1196 profiler.add_sample(make_sample("func_a", 200));
1197 let result = profiler.build_flame_graph();
1198 assert!(result.is_ok());
1199 }
1200
1201 #[test]
1202 fn test_flame_graph_config_noise_filter() {
1203 let mut config = make_config();
1204 config.filter_noise = true;
1205 config.noise_threshold = 0.05;
1206 let mut profiler = FlameGraphProfiler::new(config);
1207 profiler.add_sample(make_nested_sample(&["main", "big_func"], 10000));
1208 profiler.add_sample(make_nested_sample(&["main", "tiny_func"], 1));
1209 let result = profiler.build_flame_graph();
1210 assert!(result.is_ok());
1211 }
1212
1213 #[test]
1214 fn test_sample_current_stack() {
1215 let mut profiler = FlameGraphProfiler::new(make_config());
1216 let result = profiler.sample_current_stack(500);
1217 assert!(result.is_ok());
1218 assert_eq!(profiler.samples.len(), 1);
1219 }
1220
1221 #[tokio::test]
1222 async fn test_export_no_graph() {
1223 let profiler = FlameGraphProfiler::new(make_config());
1224 let tmp = std::env::temp_dir().join("test_flamegraph_export.json");
1225 let result = profiler.export(FlameGraphExportFormat::JSON, &tmp).await;
1226 assert!(result.is_err());
1227 }
1228
1229 #[tokio::test]
1230 async fn test_export_json() {
1231 let mut profiler = FlameGraphProfiler::new(make_config());
1232 profiler.add_sample(make_sample("test", 100));
1233 let _ = profiler.build_flame_graph();
1234 let tmp = std::env::temp_dir().join("test_flamegraph_export_ok.json");
1235 let result = profiler.export(FlameGraphExportFormat::JSON, &tmp).await;
1236 assert!(result.is_ok());
1237 let _ = tokio::fs::remove_file(&tmp).await;
1238 }
1239
1240 #[test]
1241 fn test_flame_graph_color_schemes() {
1242 let schemes = [
1243 FlameGraphColorScheme::Hot,
1244 FlameGraphColorScheme::Cool,
1245 FlameGraphColorScheme::Java,
1246 FlameGraphColorScheme::Memory,
1247 FlameGraphColorScheme::Differential,
1248 FlameGraphColorScheme::Random,
1249 FlameGraphColorScheme::Custom(HashMap::new()),
1250 ];
1251 assert_eq!(schemes.len(), 7);
1252 }
1253
1254 #[test]
1255 fn test_flame_graph_directions() {
1256 let directions = [FlameGraphDirection::TopDown, FlameGraphDirection::BottomUp];
1257 assert_eq!(directions.len(), 2);
1258 }
1259
1260 #[test]
1261 fn test_flame_graph_export_formats() {
1262 let formats = [
1263 FlameGraphExportFormat::SVG,
1264 FlameGraphExportFormat::InteractiveHTML,
1265 FlameGraphExportFormat::JSON,
1266 FlameGraphExportFormat::Speedscope,
1267 FlameGraphExportFormat::D3,
1268 FlameGraphExportFormat::Folded,
1269 ];
1270 assert_eq!(formats.len(), 6);
1271 }
1272
1273 #[test]
1274 fn test_flame_graph_config_with_subtitle() {
1275 let mut config = make_config();
1276 config.subtitle = Some("Test Subtitle".to_string());
1277 let profiler = FlameGraphProfiler::new(config);
1278 assert!(profiler.samples.is_empty());
1279 }
1280
1281 #[test]
1282 fn test_flame_graph_config_with_gpu_and_memory() {
1283 let mut config = make_config();
1284 config.include_gpu = true;
1285 config.include_memory = true;
1286 let profiler = FlameGraphProfiler::new(config);
1287 assert!(profiler.samples.is_empty());
1288 }
1289
1290 #[test]
1291 fn test_flame_graph_node_creation() {
1292 let node = FlameGraphNode {
1293 name: "test_func".to_string(),
1294 value: 1000,
1295 delta: None,
1296 children: HashMap::new(),
1297 total_value: 1000,
1298 self_value: 500,
1299 percentage: 50.0,
1300 color: Some("#ff0000".to_string()),
1301 metadata: HashMap::new(),
1302 };
1303 assert_eq!(node.name, "test_func");
1304 assert_eq!(node.value, 1000);
1305 assert!((node.percentage - 50.0).abs() < f64::EPSILON);
1306 }
1307
1308 #[test]
1309 fn test_stack_frame_with_all_fields() {
1310 let frame = StackFrame {
1311 function_name: "my_func".to_string(),
1312 module_name: Some("my_module".to_string()),
1313 file_name: Some("src/lib.rs".to_string()),
1314 line_number: Some(42),
1315 address: Some(0x12345678),
1316 };
1317 assert_eq!(frame.function_name, "my_func");
1318 assert_eq!(frame.module_name, Some("my_module".to_string()));
1319 assert_eq!(frame.line_number, Some(42));
1320 }
1321}