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 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 #[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 #[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 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 #[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 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"); 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 #[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 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 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 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 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 #[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 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 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 #[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 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 #[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 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 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 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 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 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}