testlint_sdk/profiler/
rust.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 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    /// Continuous profiling of Rust applications using platform-specific profilers
22    /// Runs until the process exits or is interrupted
23    pub fn profile_continuous(&self, rust_file_or_bin: &str) -> Result<ProfileResult, String> {
24        println!("🦀 Starting Rust runtime profiling...");
25
26        // Try platform-specific profilers first
27        #[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        // Linux: use cargo-flamegraph (which uses perf under the hood)
40        #[cfg(target_os = "linux")]
41        {
42            println!("📝 Linux detected - using cargo-flamegraph (perf-based)");
43
44            // Check if cargo-flamegraph is installed
45            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    /// Profile a Rust source file (compile and run with profiling)
67    fn profile_rust_source(&self, _rust_file: &str) -> Result<ProfileResult, String> {
68        println!("🔨 Building Rust project with profiling...");
69
70        // Check if it's a Cargo project
71        if Path::new("Cargo.toml").exists() {
72            // Run cargo flamegraph
73            let mut cmd = Command::new("cargo");
74            cmd.arg("flamegraph");
75            cmd.arg("--bin");
76
77            // Try to infer binary name from Cargo.toml
78            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    /// Profile a Rust binary executable
102    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    /// Check if cargo-flamegraph is installed
128    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    /// Install cargo-flamegraph
139    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    /// Get binary name from Cargo.toml
159    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        // Simple parsing - look for package.name
164        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    /// Profile on macOS using sample command
177    #[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        // First, build the binary if it's a Cargo project
184        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"); // Sample for 10 seconds
210        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    /// Profile on Windows using Windows Performance Recorder
250    #[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        // First, build the binary if it's a Cargo project
257        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        // Start recording
281        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        // Run the application
298        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        // Stop recording
310        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    /// Parse flamegraph output and create ProfileResult
352    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        // Try to get some basic stats from the SVG
373        if let Ok(svg_content) = fs::read_to_string(flamegraph_path) {
374            // Count number of function frames
375            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    /// Profile by attaching to PID (using perf on Linux)
387    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            // Use perf record to profile the process
393            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}