testlint_sdk/profiler/
cpp.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 CppProfiler;
9
10impl Default for CppProfiler {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl CppProfiler {
17    pub fn new() -> Self {
18        CppProfiler
19    }
20
21    /// Continuous profiling of C++ applications using platform-specific profilers
22    /// Runs until the process exits or is interrupted
23    pub fn profile_continuous(&self, cpp_binary: &str) -> Result<ProfileResult, String> {
24        println!("⚙️  Starting C++ runtime profiling...");
25        println!("📝 Note: Binary should be compiled with -g for debugging symbols");
26
27        // Try platform-specific profilers first
28        #[cfg(target_os = "macos")]
29        {
30            println!("📝 macOS detected - using sample/Instruments for profiling");
31            self.profile_macos(cpp_binary)
32        }
33
34        #[cfg(target_os = "windows")]
35        {
36            println!("📝 Windows detected - using Windows Performance Recorder");
37            self.profile_windows(cpp_binary)
38        }
39
40        // Linux: Try perf first, then fall back to valgrind
41        #[cfg(target_os = "linux")]
42        {
43            println!("📝 Linux detected - using perf for profiling");
44            if self.is_perf_available() {
45                return self.profile_with_perf(cpp_binary);
46            }
47
48            // Try valgrind/callgrind (cross-platform fallback)
49            if self.is_valgrind_available() {
50                return self.profile_with_valgrind(cpp_binary);
51            }
52
53            Err("No profiling tool found. Please install:\n\
54                 - Linux: perf (apt-get install linux-tools-generic)\n\
55                 - Any OS: valgrind (apt-get install valgrind / brew install valgrind)"
56                .to_string())
57        }
58
59        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
60        {
61            Err("C++ profiling is not supported on this platform".to_string())
62        }
63    }
64
65    /// Profile on macOS using sample command
66    #[cfg(target_os = "macos")]
67    fn profile_macos(&self, binary: &str) -> Result<ProfileResult, String> {
68        println!("🍎 Profiling on macOS using 'sample' command...");
69
70        let output_file = "cpp_profile.txt";
71
72        // Check if binary exists
73        if !Path::new(binary).exists() {
74            return Err(format!("Binary not found: {}", binary));
75        }
76
77        println!("Running: sample {} 10 -file {}", binary, output_file);
78        println!("Profiling for 10 seconds...");
79
80        let mut cmd = Command::new("sample");
81        cmd.arg(binary);
82        cmd.arg("10"); // Sample for 10 seconds
83        cmd.arg("-file");
84        cmd.arg(output_file);
85
86        let output = cmd
87            .output()
88            .map_err(|e| format!("Failed to run sample command: {}", e))?;
89
90        if !output.status.success() {
91            return Err(format!(
92                "sample failed: {}",
93                String::from_utf8_lossy(&output.stderr)
94            ));
95        }
96
97        Ok(ProfileResult {
98            language: "C++".to_string(),
99            details: vec![
100                "✓ Profiling completed successfully".to_string(),
101                format!("📊 Profile data saved to {}", output_file),
102                "".to_string(),
103                "macOS 'sample' command output includes:".to_string(),
104                "  - Call tree showing function hierarchy".to_string(),
105                "  - Sample counts per function".to_string(),
106                "  - Binary image information".to_string(),
107                "".to_string(),
108                format!("To view the profile: open {}", output_file),
109                "".to_string(),
110                "For GUI profiling, use Instruments:".to_string(),
111                format!("  instruments -t 'Time Profiler' {}", binary),
112            ],
113        })
114    }
115
116    #[cfg(not(target_os = "macos"))]
117    #[allow(dead_code)]
118    fn profile_macos(&self, _binary: &str) -> Result<ProfileResult, String> {
119        Err("macOS profiling is only available on macOS".to_string())
120    }
121
122    /// Profile on Windows using Windows Performance Recorder
123    #[cfg(target_os = "windows")]
124    fn profile_windows(&self, binary: &str) -> Result<ProfileResult, String> {
125        println!("🪟 Profiling on Windows using Windows Performance Recorder...");
126
127        let output_file = "cpp_profile.etl";
128
129        // Check if binary exists
130        let binary_path = if !binary.ends_with(".exe") {
131            format!("{}.exe", binary)
132        } else {
133            binary.to_string()
134        };
135
136        if !Path::new(&binary_path).exists() {
137            return Err(format!("Binary not found: {}", binary_path));
138        }
139
140        println!("Starting Windows Performance Recorder...");
141        println!("Run your application, then press Ctrl+C to stop recording.");
142
143        // Start recording
144        let mut cmd = Command::new("wpr");
145        cmd.args(["-start", "CPU"]);
146
147        let output = cmd
148            .output()
149            .map_err(|e| format!("Failed to start WPR: {}. Is WPR installed?", e))?;
150
151        if !output.status.success() {
152            return Err(format!(
153                "WPR failed to start: {}",
154                String::from_utf8_lossy(&output.stderr)
155            ));
156        }
157
158        println!("Recording started. Running {}...", binary_path);
159
160        // Run the application
161        let app_output = Command::new(&binary_path)
162            .output()
163            .map_err(|e| format!("Failed to run application: {}", e))?;
164
165        if !app_output.status.success() {
166            println!(
167                "⚠️  Application exited with error: {}",
168                String::from_utf8_lossy(&app_output.stderr)
169            );
170        }
171
172        // Stop recording
173        println!("Stopping Windows Performance Recorder...");
174        let mut stop_cmd = Command::new("wpr");
175        stop_cmd.args(["-stop", output_file]);
176
177        let stop_output = stop_cmd
178            .output()
179            .map_err(|e| format!("Failed to stop WPR: {}", e))?;
180
181        if !stop_output.status.success() {
182            return Err(format!(
183                "WPR failed to stop: {}",
184                String::from_utf8_lossy(&stop_output.stderr)
185            ));
186        }
187
188        Ok(ProfileResult {
189            language: "C++".to_string(),
190            details: vec![
191                "✓ Profiling completed successfully".to_string(),
192                format!("📊 Profile data saved to {}", output_file),
193                "".to_string(),
194                "To analyze the profile with Windows Performance Analyzer (WPA):".to_string(),
195                format!("  wpa {}", output_file),
196                "".to_string(),
197                "WPA provides:".to_string(),
198                "  - Detailed CPU usage analysis".to_string(),
199                "  - Call stacks and flame graphs".to_string(),
200                "  - Function-level performance metrics".to_string(),
201                "".to_string(),
202                "Note: WPA is part of the Windows Performance Toolkit".to_string(),
203                "  Download from: https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install".to_string(),
204            ],
205        })
206    }
207
208    #[cfg(not(target_os = "windows"))]
209    #[allow(dead_code)]
210    fn profile_windows(&self, _binary: &str) -> Result<ProfileResult, String> {
211        Err("Windows profiling is only available on Windows".to_string())
212    }
213
214    /// Profile using perf (Linux)
215    #[cfg(target_os = "linux")]
216    fn profile_with_perf(&self, binary: &str) -> Result<ProfileResult, String> {
217        println!("🔍 Profiling with perf...");
218
219        let perf_data = "perf.data";
220
221        let mut cmd = Command::new("perf");
222        cmd.args(["record", "-F", "99", "-g", "--", binary]);
223
224        println!("Running: perf record -F 99 -g -- {}", binary);
225        println!("Profiling until program exits...");
226
227        let output = cmd
228            .output()
229            .map_err(|e| format!("Failed to run perf: {}", e))?;
230
231        if !output.status.success() {
232            let stderr = String::from_utf8_lossy(&output.stderr);
233
234            if stderr.contains("permission") || stderr.contains("Operation not permitted") {
235                return Err("Permission denied. perf may require elevated privileges.\n\
236                     Try: sudo sysctl kernel.perf_event_paranoid=0\n\
237                     Or run with: sudo -E env PATH=$PATH <your command>"
238                    .to_string());
239            }
240
241            return Err(format!("perf failed: {}", stderr));
242        }
243
244        self.parse_perf_output(perf_data)
245    }
246
247    #[cfg(not(target_os = "linux"))]
248    #[allow(dead_code)]
249    fn profile_with_perf(&self, _binary: &str) -> Result<ProfileResult, String> {
250        Err("perf is only available on Linux".to_string())
251    }
252
253    /// Profile using valgrind/callgrind (cross-platform)
254    fn profile_with_valgrind(&self, binary: &str) -> Result<ProfileResult, String> {
255        println!("🔍 Profiling with valgrind/callgrind...");
256
257        let callgrind_file = "callgrind.out";
258
259        let mut cmd = Command::new("valgrind");
260        cmd.args([
261            "--tool=callgrind",
262            &format!("--callgrind-out-file={}", callgrind_file),
263            binary,
264        ]);
265
266        println!(
267            "Running: valgrind --tool=callgrind --callgrind-out-file={} {}",
268            callgrind_file, binary
269        );
270        println!("Profiling until program exits (this may be slow)...");
271
272        let output = cmd
273            .output()
274            .map_err(|e| format!("Failed to run valgrind: {}", e))?;
275
276        if !output.status.success() {
277            return Err(format!(
278                "valgrind failed: {}",
279                String::from_utf8_lossy(&output.stderr)
280            ));
281        }
282
283        // Show valgrind output
284        let stderr = String::from_utf8_lossy(&output.stderr);
285        if !stderr.is_empty() {
286            println!("\n--- Valgrind Output ---");
287            println!("{}", stderr);
288            println!("--- End Output ---\n");
289        }
290
291        self.parse_callgrind_output(callgrind_file)
292    }
293
294    /// Check if perf is available
295    #[cfg(target_os = "linux")]
296    fn is_perf_available(&self) -> bool {
297        Command::new("perf")
298            .arg("--version")
299            .stdout(Stdio::null())
300            .stderr(Stdio::null())
301            .status()
302            .is_ok()
303    }
304
305    #[cfg(not(target_os = "linux"))]
306    #[allow(dead_code)]
307    fn is_perf_available(&self) -> bool {
308        false
309    }
310
311    /// Check if valgrind is available
312    fn is_valgrind_available(&self) -> bool {
313        Command::new("valgrind")
314            .arg("--version")
315            .stdout(Stdio::null())
316            .stderr(Stdio::null())
317            .status()
318            .is_ok()
319    }
320
321    /// Parse perf output
322    #[cfg(target_os = "linux")]
323    fn parse_perf_output(&self, perf_data: &str) -> Result<ProfileResult, String> {
324        if !Path::new(perf_data).exists() {
325            return Err("perf.data not generated. Profiling may have failed.".to_string());
326        }
327
328        let file_size = fs::metadata(perf_data).map(|m| m.len()).unwrap_or(0);
329
330        let details = vec![
331            "✓ Profiling completed successfully".to_string(),
332            format!("📊 Profile data saved to: {}", perf_data),
333            format!("   Size: {} bytes", file_size),
334            "".to_string(),
335            "To analyze the profile:".to_string(),
336            "".to_string(),
337            "1. Interactive report:".to_string(),
338            "   perf report".to_string(),
339            "".to_string(),
340            "2. Generate flamegraph:".to_string(),
341            "   perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg".to_string(),
342            "   (Requires: https://github.com/brendangregg/FlameGraph)".to_string(),
343            "".to_string(),
344            "3. Text report:".to_string(),
345            "   perf report --stdio".to_string(),
346            "".to_string(),
347            "Profile contains:".to_string(),
348            "  - CPU sampling data (99 Hz)".to_string(),
349            "  - Call graphs with stack traces".to_string(),
350            "  - Hot functions and paths".to_string(),
351            "  - Hardware performance counters".to_string(),
352        ];
353
354        Ok(ProfileResult {
355            language: "C++".to_string(),
356            details,
357        })
358    }
359
360    #[cfg(not(target_os = "linux"))]
361    #[allow(dead_code)]
362    fn parse_perf_output(&self, _perf_data: &str) -> Result<ProfileResult, String> {
363        Err("perf is only available on Linux".to_string())
364    }
365
366    /// Parse callgrind output
367    fn parse_callgrind_output(&self, callgrind_file: &str) -> Result<ProfileResult, String> {
368        if !Path::new(callgrind_file).exists() {
369            return Err("Callgrind file not generated. Profiling may have failed.".to_string());
370        }
371
372        let file_size = fs::metadata(callgrind_file).map(|m| m.len()).unwrap_or(0);
373
374        // Try to extract basic stats
375        let stats = self.analyze_callgrind(callgrind_file)?;
376
377        let mut details = vec![
378            "✓ Profiling completed successfully".to_string(),
379            format!("📊 Callgrind file generated: {}", callgrind_file),
380            format!("   Size: {} bytes", file_size),
381            "".to_string(),
382        ];
383
384        details.extend(stats);
385
386        details.extend(vec![
387            "".to_string(),
388            "To analyze the profile:".to_string(),
389            "".to_string(),
390            "1. Using KCacheGrind (Linux/macOS):".to_string(),
391            format!("   kcachegrind {}", callgrind_file),
392            "".to_string(),
393            "2. Using QCacheGrind (macOS):".to_string(),
394            "   brew install qcachegrind".to_string(),
395            format!("   qcachegrind {}", callgrind_file),
396            "".to_string(),
397            "3. Text summary:".to_string(),
398            format!("   callgrind_annotate {}", callgrind_file),
399            "".to_string(),
400            "Callgrind file contains:".to_string(),
401            "  - Instruction counts per function".to_string(),
402            "  - Call graph relationships".to_string(),
403            "  - Cache simulation data".to_string(),
404            "  - Branch prediction statistics".to_string(),
405        ]);
406
407        Ok(ProfileResult {
408            language: "C++".to_string(),
409            details,
410        })
411    }
412
413    /// Analyze callgrind file for basic stats
414    fn analyze_callgrind(&self, callgrind_file: &str) -> Result<Vec<String>, String> {
415        let content = fs::read_to_string(callgrind_file)
416            .map_err(|e| format!("Failed to read callgrind file: {}", e))?;
417
418        let mut function_count = 0;
419        let mut total_instructions = 0u64;
420
421        for line in content.lines() {
422            if line.starts_with("fn=") {
423                function_count += 1;
424            } else if !line.starts_with("#") && !line.starts_with("fl=") && !line.starts_with("fn=")
425            {
426                // Lines with instruction counts
427                if let Some(count_str) = line.split_whitespace().last() {
428                    if let Ok(count) = count_str.parse::<u64>() {
429                        total_instructions += count;
430                    }
431                }
432            }
433        }
434
435        Ok(vec![
436            "Profile Statistics:".to_string(),
437            format!("  - Functions profiled: {}", function_count),
438            format!("  - Total instructions: {}", total_instructions),
439        ])
440    }
441
442    /// Profile by attaching to PID
443    pub fn profile_pid(&self, pid: u32) -> Result<ProfileResult, String> {
444        println!("🔍 Attaching to C++ process PID: {}", pid);
445
446        #[cfg(target_os = "linux")]
447        {
448            if !self.is_perf_available() {
449                return Err(
450                    "perf not found. Install with: apt-get install linux-tools-generic".to_string(),
451                );
452            }
453
454            let perf_data = format!("perf_pid_{}.data", pid);
455
456            let mut cmd = Command::new("perf");
457            cmd.args([
458                "record",
459                "-F",
460                "99",
461                "-g",
462                "-p",
463                &pid.to_string(),
464                "-o",
465                &perf_data,
466            ]);
467
468            println!("Running: perf record -F 99 -g -p {} -o {}", pid, perf_data);
469            println!("Press Ctrl+C to stop profiling...");
470
471            let output = cmd
472                .output()
473                .map_err(|e| format!("Failed to run perf: {}", e))?;
474
475            if !output.status.success() {
476                let stderr = String::from_utf8_lossy(&output.stderr);
477
478                if stderr.contains("permission") || stderr.contains("Operation not permitted") {
479                    return Err("Permission denied. perf may require elevated privileges.\n\
480                         Try: sudo sysctl kernel.perf_event_paranoid=0\n\
481                         Or run with: sudo -E env PATH=$PATH <your command>"
482                        .to_string());
483                }
484
485                return Err(format!("perf failed: {}", stderr));
486            }
487
488            self.parse_perf_output(&perf_data)
489        }
490
491        #[cfg(not(target_os = "linux"))]
492        {
493            Err("PID profiling for C++ is only supported on Linux (requires perf)".to_string())
494        }
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn test_profiler_new() {
504        let profiler = CppProfiler::new();
505        assert_eq!(std::mem::size_of_val(&profiler), 0);
506    }
507
508    #[test]
509    fn test_profiler_default() {
510        let profiler = CppProfiler;
511        assert_eq!(std::mem::size_of_val(&profiler), 0);
512    }
513}