testlint_sdk/profiler/
ruby.rs

1#![allow(dead_code)]
2
3use crate::profiler::ProfileResult;
4use std::fs;
5use std::path::Path;
6use std::process::{Command, Stdio};
7
8pub struct RubyProfiler;
9
10impl Default for RubyProfiler {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl RubyProfiler {
17    pub fn new() -> Self {
18        RubyProfiler
19    }
20
21    /// Continuous profiling of Ruby applications using rbspy
22    pub fn profile_continuous(&self, ruby_file: &str) -> Result<ProfileResult, String> {
23        println!("💎 Starting Ruby runtime profiling...");
24        println!("📝 Note: This uses rbspy for CPU profiling");
25
26        // Check if rbspy is installed
27        if !self.is_rbspy_installed() {
28            return Err("rbspy not found. Please install it:\n\
29                 - macOS: brew install rbspy\n\
30                 - Linux: cargo install rbspy\n\
31                 - Or download from: https://rbspy.github.io/"
32                .to_string());
33        }
34
35        self.profile_with_rbspy(ruby_file)
36    }
37
38    /// Profile Ruby script using rbspy
39    fn profile_with_rbspy(&self, ruby_file: &str) -> Result<ProfileResult, String> {
40        println!("🚀 Profiling Ruby script: {}", ruby_file);
41
42        let flamegraph_file = "rbspy-flamegraph.svg";
43
44        let mut cmd = Command::new("rbspy");
45        cmd.args([
46            "record",
47            "--file",
48            flamegraph_file,
49            "--format",
50            "flamegraph",
51            "--",
52            "ruby",
53            ruby_file,
54        ]);
55
56        println!(
57            "Running: rbspy record --file {} --format flamegraph -- ruby {}",
58            flamegraph_file, ruby_file
59        );
60        println!("Profiling until script exits...");
61
62        let output = cmd
63            .output()
64            .map_err(|e| format!("Failed to run rbspy: {}", e))?;
65
66        if !output.status.success() {
67            let stderr = String::from_utf8_lossy(&output.stderr);
68
69            // Check for permission errors
70            if stderr.contains("permission") || stderr.contains("Operation not permitted") {
71                return Err(
72                    "Permission denied. rbspy may require elevated privileges.\n\
73                     Try running with: sudo -E env PATH=$PATH <your command>"
74                        .to_string(),
75                );
76            }
77
78            return Err(format!("rbspy failed: {}", stderr));
79        }
80
81        self.parse_rbspy_output(flamegraph_file)
82    }
83
84    /// Check if rbspy is installed
85    fn is_rbspy_installed(&self) -> bool {
86        Command::new("rbspy")
87            .arg("--version")
88            .stdout(Stdio::null())
89            .stderr(Stdio::null())
90            .status()
91            .is_ok()
92    }
93
94    /// Parse rbspy output and create ProfileResult
95    fn parse_rbspy_output(&self, flamegraph_file: &str) -> Result<ProfileResult, String> {
96        if !Path::new(flamegraph_file).exists() {
97            return Err("Flamegraph not generated. Profiling may have failed.".to_string());
98        }
99
100        let file_size = fs::metadata(flamegraph_file).map(|m| m.len()).unwrap_or(0);
101
102        let mut details = vec![
103            "✓ Profiling completed successfully".to_string(),
104            format!("📊 Flamegraph generated: {}", flamegraph_file),
105            format!("   Size: {} bytes", file_size),
106            "".to_string(),
107            "To view the flamegraph:".to_string(),
108            format!("  open {}", flamegraph_file),
109            "".to_string(),
110            "Flamegraph shows:".to_string(),
111            "  - Ruby method call hierarchy (vertical axis)".to_string(),
112            "  - Time spent in each method (horizontal width)".to_string(),
113            "  - Hot methods appear wider".to_string(),
114            "  - Includes C extension calls".to_string(),
115        ];
116
117        // Try to get some basic stats from stdout
118        if let Ok(svg_content) = fs::read_to_string(flamegraph_file) {
119            let frame_count = svg_content.matches("<g class=\"func_g\"").count();
120            if frame_count > 0 {
121                details.push("".to_string());
122                details.push(format!("📈 Total method frames: {}", frame_count));
123            }
124        }
125
126        Ok(ProfileResult {
127            language: "Ruby".to_string(),
128            details,
129        })
130    }
131
132    /// Profile by attaching to PID
133    pub fn profile_pid(&self, pid: u32) -> Result<ProfileResult, String> {
134        println!("🔍 Attaching to Ruby process PID: {}", pid);
135
136        if !self.is_rbspy_installed() {
137            return Err("rbspy not found. Please install it:\n\
138                 - macOS: brew install rbspy\n\
139                 - Linux: cargo install rbspy\n\
140                 - Or download from: https://rbspy.github.io/"
141                .to_string());
142        }
143
144        let flamegraph_file = format!("rbspy-pid-{}.svg", pid);
145
146        let mut cmd = Command::new("rbspy");
147        cmd.args([
148            "record",
149            "--pid",
150            &pid.to_string(),
151            "--file",
152            &flamegraph_file,
153            "--format",
154            "flamegraph",
155        ]);
156
157        println!(
158            "Running: rbspy record --pid {} --file {} --format flamegraph",
159            pid, flamegraph_file
160        );
161        println!("Press Ctrl+C to stop profiling...");
162
163        let output = cmd
164            .output()
165            .map_err(|e| format!("Failed to run rbspy: {}", e))?;
166
167        if !output.status.success() {
168            let stderr = String::from_utf8_lossy(&output.stderr);
169
170            if stderr.contains("permission") || stderr.contains("Operation not permitted") {
171                return Err(
172                    "Permission denied. rbspy may require elevated privileges.\n\
173                     Try running with: sudo -E env PATH=$PATH <your command>"
174                        .to_string(),
175                );
176            }
177
178            return Err(format!("rbspy failed: {}", stderr));
179        }
180
181        self.parse_rbspy_output(&flamegraph_file)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_profiler_new() {
191        let profiler = RubyProfiler::new();
192        assert_eq!(std::mem::size_of_val(&profiler), 0);
193    }
194
195    #[test]
196    fn test_profiler_default() {
197        let profiler = RubyProfiler;
198        assert_eq!(std::mem::size_of_val(&profiler), 0);
199    }
200}