testlint_sdk/profiler/
go.rs

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    // No static analysis fields needed - only runtime profiling
16}
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
37// Removed LanguageProfiler trait - only runtime profiling methods below
38
39impl GoProfiler {
40    fn instrument_with_pprof_continuous(
41        &self,
42        original_code: &str,
43        profile_file: &Path,
44    ) -> Result<String, String> {
45        // Parse the original code to add pprof instrumentation
46        let lines: Vec<&str> = original_code.lines().collect();
47        let mut instrumented = String::new();
48
49        // Find package declaration
50        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        // Add imports if needed
80        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            // Check if we need to add pprof imports
86            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                // Need to modify imports
93                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        // Find and wrap main function
125        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; // Already processed
132            }
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                // Add profiling setup before main
141                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; // Opening brace of main
164                continue;
165            }
166
167            if in_main {
168                // Count braces to find end of main
169                for c in line.chars() {
170                    if c == '{' {
171                        brace_count += 1;
172                    }
173                    if c == '}' {
174                        brace_count -= 1;
175                    }
176                }
177
178                // Add the line (excluding the first func main() { )
179                if i > main_start_index {
180                    instrumented.push_str(line);
181                    instrumented.push('\n');
182                }
183
184                if brace_count == 0 {
185                    // End of main function
186                    break;
187                }
188            } else {
189                // Copy everything before main as-is
190                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        // Use go tool pprof to analyze the profile
200        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        // Parse pprof output format:
222        // Showing nodes accounting for 123ms, 95.35% of 129ms total
223        //       flat  flat%   sum%        cum   cum%
224        //      50ms 38.76% 38.76%       50ms 38.76%  main.fibonacci
225
226        for line in pprof_output.lines() {
227            let parts: Vec<&str> = line.split_whitespace().collect();
228
229            // Look for lines with function names (they have 6+ parts)
230            if parts.len() >= 6 {
231                // Try to parse the flat time (first column)
232                if let Some(flat_str) = parts.first() {
233                    if flat_str.ends_with("ms") || flat_str.ends_with('s') {
234                        // Extract function name (last part)
235                        if let Some(func_name) = parts.last() {
236                            // Parse the sample count from flat time
237                            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        // Sort and get hot functions
249        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        // Convert time strings like "50ms" or "1.5s" to sample counts
265        // Approximate: 1ms ≈ 1 sample
266        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        // Run pprof runtime profiling continuously
285        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        // Check if Go is installed
322        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        // Convert to absolute path
333        let script_path = Path::new(go_file)
334            .canonicalize()
335            .map_err(|e| format!("Failed to resolve script path: {}", e))?;
336
337        // Create a temporary directory for profiling
338        let temp_dir = std::env::temp_dir();
339
340        // Use unique filenames to avoid conflicts when multiple profiling sessions run in parallel
341        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        // Read the original Go file
351        let original_content = fs::read_to_string(&script_path)
352            .map_err(|e| format!("Failed to read Go file: {}", e))?;
353
354        // Create an instrumented version with pprof (no duration limit)
355        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        // Build the instrumented program
363        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        // Run the instrumented program (it will run until completion or Ctrl+C)
385        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                    // Process was interrupted or failed
394                    println!("\nProcess stopped. Generating profile...");
395                }
396            }
397            Err(e) => {
398                return Err(format!("Failed to run Go program: {}", e));
399            }
400        }
401
402        // Give it a moment to finish writing the profile
403        thread::sleep(Duration::from_millis(500));
404
405        // Parse the pprof output
406        if profile_file.exists() {
407            let result = self.parse_pprof_data(profile_file.to_str().unwrap());
408            // Clean up
409            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        // Run continuous profiling
422        let pprof_data = self.run_go_pprof_continuous(go_file)?;
423
424        // Build function stats from runtime data
425        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, // Continuous profiling - duration not limited
468            functions_executed: pprof_data.execution_count.len(),
469            function_stats,
470            hot_functions,
471        };
472
473        // Minimal static metrics (not the focus)
474        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    /// Profile by attaching to PID (not supported - Go processes need pprof HTTP endpoint)
493    pub fn profile_pid(&self, pid: u32) -> Result<ProfileResult, String> {
494        println!("🔍 Attempting to attach to Go process PID: {}", pid);
495
496        // Go doesn't support direct PID attachment like Python
497        // But we can try to connect to pprof HTTP endpoint if available
498
499        // First, try common pprof ports
500        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    /// Try to fetch pprof data from HTTP endpoint
534    fn fetch_pprof_from_http(&self, pid: u32, port: u16) -> Result<ProfileResult, String> {
535        use std::net::TcpStream;
536
537        // Test if port is open
538        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        // Use curl or wget to fetch 30-second CPU profile
548        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        // Wait for profile collection (30 seconds)
565        println!("⏳ Collecting 30-second CPU profile...");
566        thread::sleep(Duration::from_secs(31));
567
568        // Parse the pprof file
569        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            // Convert to ProfileResult
574            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}