1#![allow(dead_code)]
2
3use super::{
4 CommonProfileData, FunctionStats, HotFunction, ProfileResult, RuntimeMetrics, StaticMetrics,
5};
6use chrono::Utc;
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10use std::process::{Command, Stdio};
11use std::thread;
12use std::time::Duration;
13
14pub struct GoProfiler {
15 }
17
18#[derive(Debug)]
19pub struct GoPprofData {
20 pub execution_count: HashMap<String, u64>,
21 pub hot_functions: Vec<(String, u64)>,
22 pub total_samples: u64,
23}
24
25impl Default for GoProfiler {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl GoProfiler {
32 pub fn new() -> Self {
33 GoProfiler {}
34 }
35}
36
37impl GoProfiler {
40 fn instrument_with_pprof_continuous(
41 &self,
42 original_code: &str,
43 profile_file: &Path,
44 ) -> Result<String, String> {
45 let lines: Vec<&str> = original_code.lines().collect();
47 let mut instrumented = String::new();
48
49 let mut found_package = false;
51 let mut found_imports = false;
52 let mut in_import_block = false;
53 let mut import_end_index = 0;
54
55 for (i, line) in lines.iter().enumerate() {
56 let trimmed = line.trim();
57
58 if trimmed.starts_with("package ") {
59 found_package = true;
60 instrumented.push_str(line);
61 instrumented.push('\n');
62 continue;
63 }
64
65 if found_package && !found_imports {
66 if trimmed.starts_with("import (") {
67 in_import_block = true;
68 } else if in_import_block && trimmed == ")" {
69 in_import_block = false;
70 found_imports = true;
71 import_end_index = i;
72 } else if trimmed.starts_with("import \"") {
73 found_imports = true;
74 import_end_index = i;
75 }
76 }
77 }
78
79 if !found_imports {
81 instrumented.push_str(
82 "\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime/pprof\"\n\t\"time\"\n)\n\n",
83 );
84 } else {
85 let has_os = original_code.contains("\"os\"");
87 let has_pprof = original_code.contains("\"runtime/pprof\"");
88 let has_fmt = original_code.contains("\"fmt\"");
89 let has_time = original_code.contains("\"time\"");
90
91 if !has_os || !has_pprof || !has_fmt || !has_time {
92 for (i, line) in lines.iter().enumerate() {
94 if i <= import_end_index {
95 instrumented.push_str(line);
96 instrumented.push('\n');
97
98 if line.trim().starts_with("import (") {
99 if !has_os {
100 instrumented.push_str("\t\"os\"\n");
101 }
102 if !has_pprof {
103 instrumented.push_str("\t\"runtime/pprof\"\n");
104 }
105 if !has_fmt {
106 instrumented.push_str("\t\"fmt\"\n");
107 }
108 if !has_time {
109 instrumented.push_str("\t\"time\"\n");
110 }
111 }
112 }
113 }
114 } else {
115 for (i, line) in lines.iter().enumerate() {
116 if i <= import_end_index {
117 instrumented.push_str(line);
118 instrumented.push('\n');
119 }
120 }
121 }
122 }
123
124 let mut in_main = false;
126 let mut brace_count = 0;
127 let mut main_start_index = 0;
128
129 for (i, line) in lines.iter().enumerate() {
130 if i <= import_end_index {
131 continue; }
133
134 let trimmed = line.trim();
135
136 if !in_main && trimmed.starts_with("func main()") {
137 in_main = true;
138 main_start_index = i;
139
140 instrumented.push_str("\nfunc main() {\n");
142 instrumented.push_str("\t// CPU Profiling setup\n");
143 instrumented.push_str(&format!(
144 "\tf, err := os.Create(\"{}\")\n",
145 profile_file.display()
146 ));
147 instrumented.push_str("\tif err != nil {\n");
148 instrumented.push_str(
149 "\t\tfmt.Fprintf(os.Stderr, \"could not create CPU profile: %v\\n\", err)\n",
150 );
151 instrumented.push_str("\t\tos.Exit(1)\n");
152 instrumented.push_str("\t}\n");
153 instrumented.push_str("\tdefer f.Close()\n\n");
154 instrumented.push_str("\tif err := pprof.StartCPUProfile(f); err != nil {\n");
155 instrumented.push_str(
156 "\t\tfmt.Fprintf(os.Stderr, \"could not start CPU profile: %v\\n\", err)\n",
157 );
158 instrumented.push_str("\t\tos.Exit(1)\n");
159 instrumented.push_str("\t}\n");
160 instrumented.push_str("\tdefer pprof.StopCPUProfile()\n\n");
161 instrumented.push_str("\t// Original main function code\n");
162
163 brace_count = 1; continue;
165 }
166
167 if in_main {
168 for c in line.chars() {
170 if c == '{' {
171 brace_count += 1;
172 }
173 if c == '}' {
174 brace_count -= 1;
175 }
176 }
177
178 if i > main_start_index {
180 instrumented.push_str(line);
181 instrumented.push('\n');
182 }
183
184 if brace_count == 0 {
185 break;
187 }
188 } else {
189 instrumented.push_str(line);
191 instrumented.push('\n');
192 }
193 }
194
195 Ok(instrumented)
196 }
197
198 fn parse_pprof_data(&self, profile_path: &str) -> Result<GoPprofData, String> {
199 let output = Command::new("go")
201 .arg("tool")
202 .arg("pprof")
203 .arg("-top")
204 .arg("-flat")
205 .arg(profile_path)
206 .output()
207 .map_err(|e| format!("Failed to run pprof: {}", e))?;
208
209 if !output.status.success() {
210 return Err(format!(
211 "pprof analysis failed: {}",
212 String::from_utf8_lossy(&output.stderr)
213 ));
214 }
215
216 let pprof_output = String::from_utf8_lossy(&output.stdout);
217
218 let mut execution_count: HashMap<String, u64> = HashMap::new();
219 let mut total_samples = 0u64;
220
221 for line in pprof_output.lines() {
227 let parts: Vec<&str> = line.split_whitespace().collect();
228
229 if parts.len() >= 6 {
231 if let Some(flat_str) = parts.first() {
233 if flat_str.ends_with("ms") || flat_str.ends_with('s') {
234 if let Some(func_name) = parts.last() {
236 let count = self.parse_time_to_samples(flat_str);
238 if count > 0 {
239 execution_count.insert(func_name.to_string(), count);
240 total_samples += count;
241 }
242 }
243 }
244 }
245 }
246 }
247
248 let mut hot_functions: Vec<(String, u64)> = execution_count
250 .iter()
251 .map(|(k, v)| (k.clone(), *v))
252 .collect();
253 hot_functions.sort_by(|a, b| b.1.cmp(&a.1));
254 hot_functions.truncate(10);
255
256 Ok(GoPprofData {
257 execution_count,
258 hot_functions,
259 total_samples,
260 })
261 }
262
263 fn parse_time_to_samples(&self, time_str: &str) -> u64 {
264 if time_str.ends_with("ms") {
267 time_str
268 .trim_end_matches("ms")
269 .parse::<f64>()
270 .unwrap_or(0.0) as u64
271 } else if time_str.ends_with('s') {
272 let secs = time_str.trim_end_matches('s').parse::<f64>().unwrap_or(0.0);
273 (secs * 1000.0) as u64
274 } else {
275 0
276 }
277 }
278
279 pub fn profile_continuous(&self, go_file: &str) -> Result<ProfileResult, String> {
280 println!("Starting Go continuous runtime profiling with pprof...");
281 println!("File: {}", go_file);
282 println!("Press Ctrl+C to stop and see results...\n");
283
284 let pprof_data = self.run_go_pprof_continuous(go_file)?;
286
287 let mut details = Vec::new();
288 details.push("=== Runtime Profile (pprof) ===".to_string());
289 details.push(format!(
290 "Total samples collected: {}",
291 pprof_data.total_samples
292 ));
293 details.push(format!(
294 "Unique functions executed: {}",
295 pprof_data.execution_count.len()
296 ));
297 details.push("\nTop 10 Hot Functions:".to_string());
298
299 for (idx, (func_name, count)) in pprof_data.hot_functions.iter().enumerate() {
300 let percentage = if pprof_data.total_samples > 0 {
301 (*count as f64 / pprof_data.total_samples as f64) * 100.0
302 } else {
303 0.0
304 };
305 details.push(format!(
306 " {}. {} - {} samples ({:.2}%)",
307 idx + 1,
308 func_name,
309 count,
310 percentage
311 ));
312 }
313
314 Ok(ProfileResult {
315 language: "Go".to_string(),
316 details,
317 })
318 }
319
320 fn run_go_pprof_continuous(&self, go_file: &str) -> Result<GoPprofData, String> {
321 let go_check = Command::new("go")
323 .arg("version")
324 .stdout(Stdio::null())
325 .stderr(Stdio::null())
326 .status();
327
328 if go_check.is_err() {
329 return Err("Go is not installed. Install from: https://go.dev/dl/".to_string());
330 }
331
332 let script_path = Path::new(go_file)
334 .canonicalize()
335 .map_err(|e| format!("Failed to resolve script path: {}", e))?;
336
337 let temp_dir = std::env::temp_dir();
339
340 let pid = std::process::id();
342 let timestamp = std::time::SystemTime::now()
343 .duration_since(std::time::UNIX_EPOCH)
344 .unwrap()
345 .as_millis();
346 let profile_file = temp_dir.join(format!("go_cpu_profile_{}_{}.prof", pid, timestamp));
347
348 println!("Instrumenting Go program for continuous profiling...");
349
350 let original_content = fs::read_to_string(&script_path)
352 .map_err(|e| format!("Failed to read Go file: {}", e))?;
353
354 let instrumented =
356 self.instrument_with_pprof_continuous(&original_content, &profile_file)?;
357 let instrumented_path =
358 temp_dir.join(format!("instrumented_main_{}_{}.go", pid, timestamp));
359 fs::write(&instrumented_path, instrumented)
360 .map_err(|e| format!("Failed to write instrumented file: {}", e))?;
361
362 let build_output = temp_dir.join(format!("go_profiler_binary_{}_{}", pid, timestamp));
364
365 println!("Building instrumented Go program...");
366 let build_result = Command::new("go")
367 .arg("build")
368 .arg("-o")
369 .arg(&build_output)
370 .arg(&instrumented_path)
371 .stderr(Stdio::piped())
372 .status();
373
374 if let Err(e) = build_result {
375 return Err(format!("Failed to build instrumented Go program: {}", e));
376 }
377
378 if !build_output.exists() {
379 return Err("Go build succeeded but binary not found".to_string());
380 }
381
382 println!("Running Go program with continuous CPU profiling...");
383
384 let run_output = Command::new(&build_output)
386 .stdout(Stdio::inherit())
387 .stderr(Stdio::inherit())
388 .status();
389
390 match run_output {
391 Ok(status) => {
392 if !status.success() && status.code() != Some(0) {
393 println!("\nProcess stopped. Generating profile...");
395 }
396 }
397 Err(e) => {
398 return Err(format!("Failed to run Go program: {}", e));
399 }
400 }
401
402 thread::sleep(Duration::from_millis(500));
404
405 if profile_file.exists() {
407 let result = self.parse_pprof_data(profile_file.to_str().unwrap());
408 let _ = fs::remove_file(profile_file);
410 let _ = fs::remove_file(instrumented_path);
411 let _ = fs::remove_file(build_output);
412 result
413 } else {
414 Err("Profile data was not generated".to_string())
415 }
416 }
417
418 pub fn profile_to_common_format(&self, go_file: &str) -> Result<CommonProfileData, String> {
419 println!("Starting Go continuous runtime profiling for JSON export...");
420
421 let pprof_data = self.run_go_pprof_continuous(go_file)?;
423
424 let mut function_stats = HashMap::new();
426
427 for (func_name, count) in &pprof_data.execution_count {
428 let percentage = if pprof_data.total_samples > 0 {
429 (*count as f64 / pprof_data.total_samples as f64) * 100.0
430 } else {
431 0.0
432 };
433
434 function_stats.insert(
435 func_name.clone(),
436 FunctionStats {
437 name: func_name.clone(),
438 execution_count: *count,
439 percentage,
440 line_number: None,
441 file_path: None,
442 },
443 );
444 }
445
446 let hot_functions: Vec<HotFunction> = pprof_data
447 .hot_functions
448 .iter()
449 .enumerate()
450 .map(|(idx, (name, samples))| {
451 let percentage = if pprof_data.total_samples > 0 {
452 (*samples as f64 / pprof_data.total_samples as f64) * 100.0
453 } else {
454 0.0
455 };
456 HotFunction {
457 rank: idx + 1,
458 name: name.clone(),
459 samples: *samples,
460 percentage,
461 }
462 })
463 .collect();
464
465 let runtime_metrics = RuntimeMetrics {
466 total_samples: pprof_data.total_samples,
467 execution_duration_secs: 0, functions_executed: pprof_data.execution_count.len(),
469 function_stats,
470 hot_functions,
471 };
472
473 let static_metrics = StaticMetrics {
475 file_size_bytes: 0,
476 line_count: 0,
477 function_count: 0,
478 class_count: 0,
479 import_count: 0,
480 complexity_score: 0,
481 };
482
483 Ok(CommonProfileData {
484 language: "Go".to_string(),
485 source_file: go_file.to_string(),
486 timestamp: Utc::now().to_rfc3339(),
487 static_analysis: static_metrics,
488 runtime_analysis: Some(runtime_metrics),
489 })
490 }
491
492 pub fn profile_pid(&self, pid: u32) -> Result<ProfileResult, String> {
494 println!("🔍 Attempting to attach to Go process PID: {}", pid);
495
496 for port in [6060, 6061, 6062, 6063, 6064] {
501 if let Ok(profile_data) = self.fetch_pprof_from_http(pid, port) {
502 println!(
503 "✓ Successfully connected to pprof endpoint on port {}",
504 port
505 );
506 return Ok(profile_data);
507 }
508 }
509
510 Err(format!(
511 "Could not attach to Go process PID {}.\n\
512 \n\
513 Go profiling requires the process to expose an HTTP pprof endpoint.\n\
514 \n\
515 To enable profiling in your Go application:\n\
516 \n\
517 1. Import net/http/pprof in your code:\n\
518 import _ \"net/http/pprof\"\n\
519 \n\
520 2. Start an HTTP server:\n\
521 go func() {{\n\
522 log.Println(http.ListenAndServe(\"localhost:6060\", nil))\n\
523 }}()\n\
524 \n\
525 3. Then profile using:\n\
526 go tool pprof http://localhost:6060/debug/pprof/profile\n\
527 \n\
528 Or use the profiler binary on a .go file to instrument and profile from the start.",
529 pid
530 ))
531 }
532
533 fn fetch_pprof_from_http(&self, pid: u32, port: u16) -> Result<ProfileResult, String> {
535 use std::net::TcpStream;
536
537 if TcpStream::connect(format!("127.0.0.1:{}", port)).is_err() {
539 return Err(format!("Port {} not open", port));
540 }
541
542 println!(
543 "📊 Fetching profile from http://localhost:{}/debug/pprof/profile",
544 port
545 );
546
547 let profile_file = format!("/tmp/go-profile-{}.pprof", pid);
549
550 let output = Command::new("curl")
551 .args([
552 "-s",
553 "-o",
554 &profile_file,
555 &format!("http://localhost:{}/debug/pprof/profile?seconds=30", port),
556 ])
557 .output()
558 .map_err(|e| format!("Failed to fetch profile: {}", e))?;
559
560 if !output.status.success() {
561 return Err("Failed to download pprof data".to_string());
562 }
563
564 println!("⏳ Collecting 30-second CPU profile...");
566 thread::sleep(Duration::from_secs(31));
567
568 if Path::new(&profile_file).exists() {
570 let pprof_data = self.parse_pprof_data(&profile_file)?;
571 let _ = fs::remove_file(&profile_file);
572
573 let mut details = Vec::new();
575 details.push(format!("=== Go Profile via HTTP pprof (PID: {}) ===", pid));
576 details.push(format!("Total samples: {}", pprof_data.total_samples));
577 details.push(format!(
578 "Unique functions: {}",
579 pprof_data.execution_count.len()
580 ));
581 details.push("\nTop 10 Hot Functions:".to_string());
582
583 for (idx, (func_name, count)) in pprof_data.hot_functions.iter().enumerate() {
584 let percentage = if pprof_data.total_samples > 0 {
585 (*count as f64 / pprof_data.total_samples as f64) * 100.0
586 } else {
587 0.0
588 };
589 details.push(format!(
590 " {}. {} - {} samples ({:.2}%)",
591 idx + 1,
592 func_name,
593 count,
594 percentage
595 ));
596
597 if idx >= 9 {
598 break;
599 }
600 }
601
602 Ok(ProfileResult {
603 language: "Go".to_string(),
604 details,
605 })
606 } else {
607 Err("Profile file not created".to_string())
608 }
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615
616 #[test]
617 fn test_go_profiler_new() {
618 let profiler = GoProfiler::new();
619 assert_eq!(std::mem::size_of_val(&profiler), 0);
620 }
621
622 #[test]
623 fn test_go_profiler_default() {
624 let profiler = GoProfiler::default();
625 assert_eq!(std::mem::size_of_val(&profiler), 0);
626 }
627}