testlint_sdk/profiler/
ruby.rs1#![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 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 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 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 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 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 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 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 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}