1#![allow(dead_code)]
2
3use crate::profiler::ProfileResult;
4use std::fs;
5use std::path::Path;
6use std::process::{Command, Stdio};
7
8pub struct RustProfiler;
9
10impl Default for RustProfiler {
11 fn default() -> Self {
12 Self::new()
13 }
14}
15
16impl RustProfiler {
17 pub fn new() -> Self {
18 RustProfiler
19 }
20
21 pub fn profile_continuous(&self, rust_file_or_bin: &str) -> Result<ProfileResult, String> {
24 println!("🦀 Starting Rust runtime profiling...");
25
26 #[cfg(target_os = "macos")]
28 {
29 println!("📝 macOS detected - using sample/Instruments for profiling");
30 self.profile_macos(rust_file_or_bin)
31 }
32
33 #[cfg(target_os = "windows")]
34 {
35 println!("📝 Windows detected - using Windows Performance Recorder");
36 self.profile_windows(rust_file_or_bin)
37 }
38
39 #[cfg(target_os = "linux")]
41 {
42 println!("📝 Linux detected - using cargo-flamegraph (perf-based)");
43
44 if !self.is_flamegraph_installed() {
46 println!("⚠️ cargo-flamegraph not found. Attempting to install...");
47 self.install_flamegraph()?;
48 }
49
50 let path = Path::new(rust_file_or_bin);
51 let is_source = path.extension().is_some_and(|ext| ext == "rs");
52
53 if is_source {
54 self.profile_rust_source(rust_file_or_bin)
55 } else {
56 self.profile_rust_binary(rust_file_or_bin)
57 }
58 }
59
60 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
61 {
62 Err("Rust profiling is not supported on this platform".to_string())
63 }
64 }
65
66 fn profile_rust_source(&self, _rust_file: &str) -> Result<ProfileResult, String> {
68 println!("🔨 Building Rust project with profiling...");
69
70 if Path::new("Cargo.toml").exists() {
72 let mut cmd = Command::new("cargo");
74 cmd.arg("flamegraph");
75 cmd.arg("--bin");
76
77 let bin_name = self.get_binary_name()?;
79 cmd.arg(&bin_name);
80
81 println!("Running: cargo flamegraph --bin {}", bin_name);
82 println!("Press Ctrl+C to stop profiling and generate flamegraph...");
83
84 let output = cmd
85 .output()
86 .map_err(|e| format!("Failed to run cargo flamegraph: {}", e))?;
87
88 if !output.status.success() {
89 return Err(format!(
90 "cargo flamegraph failed: {}",
91 String::from_utf8_lossy(&output.stderr)
92 ));
93 }
94
95 self.parse_flamegraph_output()
96 } else {
97 Err("Not a Cargo project. Please run from a Cargo project directory.".to_string())
98 }
99 }
100
101 fn profile_rust_binary(&self, binary_path: &str) -> Result<ProfileResult, String> {
103 println!("🚀 Profiling Rust binary: {}", binary_path);
104
105 let mut cmd = Command::new("cargo");
106 cmd.arg("flamegraph");
107 cmd.arg("--");
108 cmd.arg(binary_path);
109
110 println!("Running: cargo flamegraph -- {}", binary_path);
111 println!("Press Ctrl+C to stop profiling and generate flamegraph...");
112
113 let output = cmd
114 .output()
115 .map_err(|e| format!("Failed to run cargo flamegraph: {}", e))?;
116
117 if !output.status.success() {
118 return Err(format!(
119 "cargo flamegraph failed: {}",
120 String::from_utf8_lossy(&output.stderr)
121 ));
122 }
123
124 self.parse_flamegraph_output()
125 }
126
127 fn is_flamegraph_installed(&self) -> bool {
129 Command::new("cargo")
130 .arg("flamegraph")
131 .arg("--help")
132 .stdout(Stdio::null())
133 .stderr(Stdio::null())
134 .status()
135 .is_ok()
136 }
137
138 fn install_flamegraph(&self) -> Result<(), String> {
140 println!("Installing cargo-flamegraph...");
141
142 let output = Command::new("cargo")
143 .args(["install", "flamegraph"])
144 .output()
145 .map_err(|e| format!("Failed to install cargo-flamegraph: {}", e))?;
146
147 if !output.status.success() {
148 return Err(format!(
149 "Failed to install cargo-flamegraph: {}",
150 String::from_utf8_lossy(&output.stderr)
151 ));
152 }
153
154 println!("✓ cargo-flamegraph installed successfully");
155 Ok(())
156 }
157
158 fn get_binary_name(&self) -> Result<String, String> {
160 let cargo_toml = fs::read_to_string("Cargo.toml")
161 .map_err(|e| format!("Failed to read Cargo.toml: {}", e))?;
162
163 for line in cargo_toml.lines() {
165 if line.trim().starts_with("name") {
166 if let Some(name) = line.split('=').nth(1) {
167 let name = name.trim().trim_matches('"').trim_matches('\'');
168 return Ok(name.to_string());
169 }
170 }
171 }
172
173 Err("Could not determine binary name from Cargo.toml".to_string())
174 }
175
176 #[cfg(target_os = "macos")]
178 fn profile_macos(&self, binary: &str) -> Result<ProfileResult, String> {
179 println!("🍎 Profiling on macOS using 'sample' command...");
180
181 let output_file = "rust_profile.txt";
182
183 let binary_path = if Path::new("Cargo.toml").exists() {
185 println!("Building Rust project...");
186 let build_output = Command::new("cargo")
187 .args(["build", "--release"])
188 .output()
189 .map_err(|e| format!("Failed to build: {}", e))?;
190
191 if !build_output.status.success() {
192 return Err(format!(
193 "Build failed: {}",
194 String::from_utf8_lossy(&build_output.stderr)
195 ));
196 }
197
198 let bin_name = self.get_binary_name()?;
199 format!("target/release/{}", bin_name)
200 } else {
201 binary.to_string()
202 };
203
204 println!("Running: sample {} 10 -file {}", binary_path, output_file);
205 println!("Profiling for 10 seconds...");
206
207 let mut cmd = Command::new("sample");
208 cmd.arg(&binary_path);
209 cmd.arg("10"); cmd.arg("-file");
211 cmd.arg(output_file);
212
213 let output = cmd
214 .output()
215 .map_err(|e| format!("Failed to run sample command: {}", e))?;
216
217 if !output.status.success() {
218 return Err(format!(
219 "sample failed: {}",
220 String::from_utf8_lossy(&output.stderr)
221 ));
222 }
223
224 Ok(ProfileResult {
225 language: "Rust".to_string(),
226 details: vec![
227 "✓ Profiling completed successfully".to_string(),
228 format!("📊 Profile data saved to {}", output_file),
229 "".to_string(),
230 "macOS 'sample' command output includes:".to_string(),
231 " - Call tree showing function hierarchy".to_string(),
232 " - Sample counts per function".to_string(),
233 " - Binary image information".to_string(),
234 "".to_string(),
235 format!("To view the profile: open {}", output_file),
236 "".to_string(),
237 "For GUI profiling, use Instruments:".to_string(),
238 format!(" instruments -t 'Time Profiler' {}", binary_path),
239 ],
240 })
241 }
242
243 #[cfg(not(target_os = "macos"))]
244 #[allow(dead_code)]
245 fn profile_macos(&self, _binary: &str) -> Result<ProfileResult, String> {
246 Err("macOS profiling is only available on macOS".to_string())
247 }
248
249 #[cfg(target_os = "windows")]
251 fn profile_windows(&self, binary: &str) -> Result<ProfileResult, String> {
252 println!("🪟 Profiling on Windows using Windows Performance Recorder...");
253
254 let output_file = "rust_profile.etl";
255
256 let binary_path = if Path::new("Cargo.toml").exists() {
258 println!("Building Rust project...");
259 let build_output = Command::new("cargo")
260 .args(["build", "--release"])
261 .output()
262 .map_err(|e| format!("Failed to build: {}", e))?;
263
264 if !build_output.status.success() {
265 return Err(format!(
266 "Build failed: {}",
267 String::from_utf8_lossy(&build_output.stderr)
268 ));
269 }
270
271 let bin_name = self.get_binary_name()?;
272 format!("target\\release\\{}.exe", bin_name)
273 } else {
274 binary.to_string()
275 };
276
277 println!("Starting Windows Performance Recorder...");
278 println!("Run your application, then press Ctrl+C to stop recording.");
279
280 let mut cmd = Command::new("wpr");
282 cmd.args(["-start", "CPU"]);
283
284 let output = cmd
285 .output()
286 .map_err(|e| format!("Failed to start WPR: {}. Is WPR installed?", e))?;
287
288 if !output.status.success() {
289 return Err(format!(
290 "WPR failed to start: {}",
291 String::from_utf8_lossy(&output.stderr)
292 ));
293 }
294
295 println!("Recording started. Running {}...", binary_path);
296
297 let app_output = Command::new(&binary_path)
299 .output()
300 .map_err(|e| format!("Failed to run application: {}", e))?;
301
302 if !app_output.status.success() {
303 println!(
304 "⚠️ Application exited with error: {}",
305 String::from_utf8_lossy(&app_output.stderr)
306 );
307 }
308
309 println!("Stopping Windows Performance Recorder...");
311 let mut stop_cmd = Command::new("wpr");
312 stop_cmd.args(["-stop", output_file]);
313
314 let stop_output = stop_cmd
315 .output()
316 .map_err(|e| format!("Failed to stop WPR: {}", e))?;
317
318 if !stop_output.status.success() {
319 return Err(format!(
320 "WPR failed to stop: {}",
321 String::from_utf8_lossy(&stop_output.stderr)
322 ));
323 }
324
325 Ok(ProfileResult {
326 language: "Rust".to_string(),
327 details: vec![
328 "✓ Profiling completed successfully".to_string(),
329 format!("📊 Profile data saved to {}", output_file),
330 "".to_string(),
331 "To analyze the profile with Windows Performance Analyzer (WPA):".to_string(),
332 format!(" wpa {}", output_file),
333 "".to_string(),
334 "WPA provides:".to_string(),
335 " - Detailed CPU usage analysis".to_string(),
336 " - Call stacks and flame graphs".to_string(),
337 " - Function-level performance metrics".to_string(),
338 "".to_string(),
339 "Note: WPA is part of the Windows Performance Toolkit".to_string(),
340 " Download from: https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install".to_string(),
341 ],
342 })
343 }
344
345 #[cfg(not(target_os = "windows"))]
346 #[allow(dead_code)]
347 fn profile_windows(&self, _binary: &str) -> Result<ProfileResult, String> {
348 Err("Windows profiling is only available on Windows".to_string())
349 }
350
351 fn parse_flamegraph_output(&self) -> Result<ProfileResult, String> {
353 let flamegraph_path = "flamegraph.svg";
354
355 if !Path::new(flamegraph_path).exists() {
356 return Err("Flamegraph not generated. Profiling may have failed.".to_string());
357 }
358
359 let mut details = vec![
360 "✓ Profiling completed successfully".to_string(),
361 format!("📊 Flamegraph generated: {}", flamegraph_path),
362 "".to_string(),
363 "To view the flamegraph:".to_string(),
364 format!(" open {}", flamegraph_path),
365 "".to_string(),
366 "Flamegraph shows:".to_string(),
367 " - Function call hierarchy (vertical axis)".to_string(),
368 " - Time spent in each function (horizontal width)".to_string(),
369 " - Hot functions appear wider".to_string(),
370 ];
371
372 if let Ok(svg_content) = fs::read_to_string(flamegraph_path) {
374 let frame_count = svg_content.matches("<g class=\"func_g\"").count();
376 details.push("".to_string());
377 details.push(format!("📈 Total function frames: {}", frame_count));
378 }
379
380 Ok(ProfileResult {
381 language: "Rust".to_string(),
382 details,
383 })
384 }
385
386 pub fn profile_pid(&self, _pid: u32) -> Result<ProfileResult, String> {
388 #[cfg(target_os = "linux")]
389 {
390 println!("🔍 Attaching to Rust process PID: {}", _pid);
391
392 let mut cmd = Command::new("perf");
394 cmd.args(["record", "-F", "99", "-p", &_pid.to_string(), "-g"]);
395
396 println!("Running: perf record -F 99 -p {} -g", _pid);
397 println!("Press Ctrl+C to stop profiling...");
398
399 let output = cmd
400 .output()
401 .map_err(|e| format!("Failed to run perf: {}. Is perf installed?", e))?;
402
403 if !output.status.success() {
404 return Err(format!(
405 "perf failed: {}",
406 String::from_utf8_lossy(&output.stderr)
407 ));
408 }
409
410 Ok(ProfileResult {
411 language: "Rust".to_string(),
412 details: vec![
413 "✓ Profiling completed successfully".to_string(),
414 "📊 Profile data saved to perf.data".to_string(),
415 "".to_string(),
416 "To view the profile:".to_string(),
417 " perf report".to_string(),
418 "".to_string(),
419 "Or generate a flamegraph:".to_string(),
420 " perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg"
421 .to_string(),
422 ],
423 })
424 }
425
426 #[cfg(not(target_os = "linux"))]
427 {
428 Err("PID profiling for Rust is only supported on Linux (requires perf)".to_string())
429 }
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_profiler_new() {
439 let profiler = RustProfiler::new();
440 assert_eq!(std::mem::size_of_val(&profiler), 0);
441 }
442
443 #[test]
444 fn test_profiler_default() {
445 let profiler = RustProfiler;
446 assert_eq!(std::mem::size_of_val(&profiler), 0);
447 }
448}