testlint_sdk/profiler/
java.rs

1#![allow(dead_code)]
2
3use super::{
4    CommonProfileData, FunctionStats, HotFunction, ProfileResult, RuntimeMetrics, StaticMetrics,
5};
6use chrono::Utc;
7use serde_json::Value;
8use std::collections::{HashMap, HashSet};
9use std::fs;
10use std::path::{Path, PathBuf};
11use std::process::{Command, Stdio};
12use std::thread;
13use std::time::Duration;
14
15#[derive(Debug, Clone)]
16struct JavaProfileData {
17    execution_count: HashMap<String, u64>,
18    hot_functions: Vec<(String, u64)>,
19    total_samples: u64,
20}
21
22pub struct JavaProfiler {}
23
24impl Default for JavaProfiler {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl JavaProfiler {
31    pub fn new() -> Self {
32        JavaProfiler {}
33    }
34
35    fn run_java_profiler(&self, java_file: &str) -> Result<JavaProfileData, String> {
36        // Check if Java is installed
37        let java_check = Command::new("java")
38            .arg("-version")
39            .stderr(Stdio::piped())
40            .output();
41
42        if java_check.is_err() {
43            return Err("Java is not installed. Install from: https://adoptium.net/ or https://www.oracle.com/java/".to_string());
44        }
45
46        // Parse Java file path
47        let script_path = Path::new(java_file)
48            .canonicalize()
49            .map_err(|e| format!("Failed to resolve script path: {}", e))?;
50
51        // Check if it's a .java source file or .jar file
52        let is_jar = script_path
53            .extension()
54            .and_then(|ext| ext.to_str())
55            .map(|ext| ext == "jar")
56            .unwrap_or(false);
57
58        let is_class = script_path
59            .extension()
60            .and_then(|ext| ext.to_str())
61            .map(|ext| ext == "class")
62            .unwrap_or(false);
63
64        // For .java files, we need to compile first
65        let class_file = if !is_jar && !is_class {
66            println!("Compiling Java source file...");
67            let compile_output = Command::new("javac")
68                .arg(&script_path)
69                .output()
70                .map_err(|e| format!("Failed to compile Java file: {}", e))?;
71
72            if !compile_output.status.success() {
73                let stderr = String::from_utf8_lossy(&compile_output.stderr);
74                return Err(format!("Java compilation failed:\n{}", stderr));
75            }
76
77            // Derive class file path
78            script_path.with_extension("class")
79        } else {
80            script_path.clone()
81        };
82
83        // Create a temporary directory for profiling
84        let temp_dir = std::env::temp_dir();
85
86        // Use unique filename to avoid conflicts when multiple profiling sessions run in parallel
87        let pid = std::process::id();
88        let timestamp = std::time::SystemTime::now()
89            .duration_since(std::time::UNIX_EPOCH)
90            .unwrap()
91            .as_millis();
92        let profile_filename = format!("java_profile_{}_{}.jfr", pid, timestamp);
93        let profile_file = temp_dir.join(&profile_filename);
94
95        // Record existing .jfr files before starting
96        let existing_profiles: HashSet<PathBuf> = fs::read_dir(&temp_dir)
97            .ok()
98            .map(|entries| {
99                entries
100                    .filter_map(|e| e.ok())
101                    .map(|e| e.path())
102                    .filter(|p| {
103                        p.extension()
104                            .and_then(|ext| ext.to_str())
105                            .map(|ext| ext == "jfr")
106                            .unwrap_or(false)
107                    })
108                    .collect()
109            })
110            .unwrap_or_default();
111
112        println!("Starting Java process with JFR (Java Flight Recorder)...");
113
114        // Determine the main class name
115        let main_class = if is_jar {
116            // For JAR files, use -jar
117            script_path.to_str().unwrap().to_string()
118        } else if is_class {
119            // For .class files, extract class name
120            script_path
121                .file_stem()
122                .and_then(|s| s.to_str())
123                .unwrap_or("Main")
124                .to_string()
125        } else {
126            // For .java files, extract class name
127            script_path
128                .file_stem()
129                .and_then(|s| s.to_str())
130                .unwrap_or("Main")
131                .to_string()
132        };
133
134        // Build Java command with JFR options
135        let mut java_cmd = Command::new("java");
136
137        // Add JFR options (works with Java 8+ but improved in Java 11+)
138        // Java 8 requires UnlockCommercialFeatures, Java 11+ does not
139        java_cmd
140            .arg("-XX:+UnlockCommercialFeatures") // Required for Java 8, ignored in Java 11+
141            .arg("-XX:+FlightRecorder") // Enable Flight Recorder
142            .arg("-XX:+UnlockDiagnosticVMOptions")
143            .arg("-XX:+DebugNonSafepoints")
144            .arg(format!(
145                "-XX:StartFlightRecording=filename={},dumponexit=true,settings=profile",
146                profile_file.display()
147            ));
148
149        if is_jar {
150            java_cmd.arg("-jar").arg(&main_class);
151        } else {
152            // Set classpath to the directory containing the class file
153            if let Some(parent) = class_file.parent() {
154                java_cmd.arg("-cp").arg(parent);
155            }
156            java_cmd.arg(&main_class);
157        }
158
159        // Run the Java program
160        let output = java_cmd
161            .stdout(Stdio::inherit())
162            .stderr(Stdio::piped())
163            .output()
164            .map_err(|e| format!("Failed to start Java process: {}", e))?;
165
166        if !output.status.success() {
167            let stderr = String::from_utf8_lossy(&output.stderr);
168            return Err(format!("Java process failed:\n{}", stderr));
169        }
170
171        // Give it a moment to finish writing the profile
172        thread::sleep(Duration::from_millis(500));
173
174        // Find the generated JFR file
175        let actual_profile = if profile_file.exists() {
176            Some(profile_file.clone())
177        } else {
178            // Look for NEW .jfr files (not in the existing set)
179            fs::read_dir(&temp_dir).ok().and_then(|entries| {
180                entries.filter_map(|e| e.ok()).map(|e| e.path()).find(|p| {
181                    p.extension()
182                        .and_then(|ext| ext.to_str())
183                        .map(|ext| ext == "jfr")
184                        .unwrap_or(false)
185                        && !existing_profiles.contains(p)
186                })
187            })
188        };
189
190        match actual_profile {
191            Some(profile_path) => {
192                let result = self.parse_jfr_profile(profile_path.to_str().unwrap());
193                // Clean up
194                let _ = fs::remove_file(&profile_path);
195                result
196            }
197            None => {
198                Err("JFR profile was not generated. Make sure your Java version supports JFR (Java 8+ with commercial features, or Java 11+ free)".to_string())
199            }
200        }
201    }
202
203    fn parse_jfr_profile(&self, profile_path: &str) -> Result<JavaProfileData, String> {
204        println!("Parsing JFR profile...");
205
206        // JFR files are binary format, we need to convert them to JSON using jfr tool
207        // jfr command is available in JDK 9+ (or with Mission Control in JDK 8)
208
209        let json_output = Command::new("jfr")
210            .arg("print")
211            .arg("--json")
212            .arg(profile_path)
213            .output();
214
215        match json_output {
216            Ok(output) if output.status.success() => {
217                let json_str = String::from_utf8_lossy(&output.stdout);
218                self.parse_jfr_json(&json_str)
219            }
220            Ok(_) => {
221                // jfr command failed, try alternative: jcmd or jmc
222                println!("⚠️  'jfr' command not available or failed");
223                println!("   JFR profile created but parsing requires JDK 9+ jfr tool");
224                println!("   Returning basic profile data");
225
226                // Return minimal data
227                Ok(JavaProfileData {
228                    execution_count: HashMap::new(),
229                    hot_functions: Vec::new(),
230                    total_samples: 0,
231                })
232            }
233            Err(_) => {
234                println!("⚠️  Could not parse JFR file (jfr command not found)");
235                println!("   Install JDK 9+ for full profiling support");
236                println!("   Or use: jcmd <pid> JFR.dump filename=profile.jfr");
237
238                Ok(JavaProfileData {
239                    execution_count: HashMap::new(),
240                    hot_functions: Vec::new(),
241                    total_samples: 0,
242                })
243            }
244        }
245    }
246
247    fn parse_jfr_json(&self, json_str: &str) -> Result<JavaProfileData, String> {
248        // Parse JFR JSON format
249        // JFR events include: jdk.ExecutionSample, jdk.MethodSample, etc.
250
251        let json_value: Value = serde_json::from_str(json_str)
252            .map_err(|e| format!("Failed to parse JFR JSON: {}", e))?;
253
254        let mut execution_count: HashMap<String, u64> = HashMap::new();
255        let mut total_samples = 0u64;
256
257        // JFR JSON has "recording" -> "events" structure
258        if let Some(recording) = json_value.get("recording") {
259            if let Some(events) = recording.get("events").and_then(|e| e.as_array()) {
260                for event in events {
261                    // Look for ExecutionSample or MethodSample events
262                    if let Some(event_type) = event.get("type").and_then(|t| t.as_str()) {
263                        if event_type.contains("ExecutionSample")
264                            || event_type.contains("MethodSample")
265                        {
266                            // Extract stack trace
267                            if let Some(stack_trace) = event.get("stackTrace") {
268                                if let Some(frames) =
269                                    stack_trace.get("frames").and_then(|f| f.as_array())
270                                {
271                                    for frame in frames {
272                                        if let Some(method) = frame.get("method") {
273                                            let class_name = method
274                                                .get("type")
275                                                .and_then(|t| t.get("name"))
276                                                .and_then(|n| n.as_str())
277                                                .unwrap_or("Unknown");
278
279                                            let method_name = method
280                                                .get("name")
281                                                .and_then(|n| n.as_str())
282                                                .unwrap_or("unknown");
283
284                                            let line_number = frame
285                                                .get("lineNumber")
286                                                .and_then(|l| l.as_i64())
287                                                .unwrap_or(-1);
288
289                                            let func_identifier = if line_number > 0 {
290                                                format!(
291                                                    "{}.{}:{}",
292                                                    class_name, method_name, line_number
293                                                )
294                                            } else {
295                                                format!("{}.{}", class_name, method_name)
296                                            };
297
298                                            *execution_count.entry(func_identifier).or_insert(0) +=
299                                                1;
300                                            total_samples += 1;
301                                        }
302                                    }
303                                }
304                            }
305                        }
306                    }
307                }
308            }
309        }
310
311        // Sort and get hot functions
312        let mut hot_functions: Vec<(String, u64)> = execution_count
313            .iter()
314            .map(|(k, v)| (k.clone(), *v))
315            .collect();
316        hot_functions.sort_by(|a, b| b.1.cmp(&a.1));
317        hot_functions.truncate(10);
318
319        Ok(JavaProfileData {
320            execution_count,
321            hot_functions,
322            total_samples,
323        })
324    }
325
326    pub fn profile_continuous(&self, java_file: &str) -> Result<ProfileResult, String> {
327        println!("Starting Java continuous runtime profiling...");
328        println!("File: {}", java_file);
329        println!("Running until process completes...\n");
330
331        let profile_data = self.run_java_profiler(java_file)?;
332
333        let mut details = Vec::new();
334        details.push("=== Runtime Profile (Java Flight Recorder) ===".to_string());
335        details.push(format!(
336            "Total samples collected: {}",
337            profile_data.total_samples
338        ));
339        details.push(format!(
340            "Unique methods executed: {}",
341            profile_data.execution_count.len()
342        ));
343        details.push("\nTop 10 Hot Methods:".to_string());
344
345        for (idx, (method_name, count)) in profile_data.hot_functions.iter().enumerate() {
346            let percentage = if profile_data.total_samples > 0 {
347                (*count as f64 / profile_data.total_samples as f64) * 100.0
348            } else {
349                0.0
350            };
351            details.push(format!(
352                "  {}. {} - {} samples ({:.2}%)",
353                idx + 1,
354                method_name,
355                count,
356                percentage
357            ));
358        }
359
360        if profile_data.total_samples == 0 {
361            details.push("\n⚠️  No profiling data collected.".to_string());
362            details.push("   This may be because:".to_string());
363            details.push(
364                "   - JFR is not available (requires Java 8+ commercial or Java 11+ free)"
365                    .to_string(),
366            );
367            details.push("   - The program ran too quickly".to_string());
368            details.push("   - The 'jfr' command is not installed (requires JDK 9+)".to_string());
369            details.push(
370                "\n   Java Flight Recorder was introduced in Java 8 but required".to_string(),
371            );
372            details.push("   commercial features until Java 11, where it became free.".to_string());
373        }
374
375        Ok(ProfileResult {
376            language: "Java/JVM".to_string(),
377            details,
378        })
379    }
380
381    pub fn profile_pid(&self, pid: u32) -> Result<ProfileResult, String> {
382        println!("🔍 Attaching to Java process PID: {}", pid);
383        println!("📊 Collecting 30-second JFR profile...\n");
384
385        // Check if jcmd is available
386        self.ensure_jcmd_available()?;
387
388        // Check if the process is a Java process
389        self.check_java_process(pid)?;
390
391        let recording_name = format!("profile_{}", pid);
392        let jfr_file = format!("/tmp/java-profile-{}.jfr", pid);
393
394        // Start JFR recording
395        println!("🎬 Starting JFR recording...");
396        let start_output = Command::new("jcmd")
397            .args([
398                &pid.to_string(),
399                "JFR.start",
400                &format!("name={}", recording_name),
401                "settings=profile",
402                "duration=30s",
403            ])
404            .output()
405            .map_err(|e| format!("Failed to start JFR: {}", e))?;
406
407        let start_msg = String::from_utf8_lossy(&start_output.stdout);
408        if start_msg.contains("Started recording") {
409            println!("✓ JFR recording started");
410        } else {
411            return Err(format!(
412                "Failed to start JFR recording:\n{}",
413                String::from_utf8_lossy(&start_output.stderr)
414            ));
415        }
416
417        // Wait for recording to complete
418        println!("⏳ Recording for 30 seconds...");
419        thread::sleep(Duration::from_secs(31));
420
421        // Dump the recording
422        println!("📝 Dumping JFR data...");
423        let dump_output = Command::new("jcmd")
424            .args([
425                &pid.to_string(),
426                "JFR.dump",
427                &format!("name={}", recording_name),
428                &format!("filename={}", jfr_file),
429            ])
430            .output()
431            .map_err(|e| format!("Failed to dump JFR: {}", e))?;
432
433        if !dump_output.status.success() {
434            return Err(format!(
435                "Failed to dump JFR data:\n{}",
436                String::from_utf8_lossy(&dump_output.stderr)
437            ));
438        }
439
440        println!("✓ JFR data dumped to {}", jfr_file);
441
442        // Stop the recording
443        let _ = Command::new("jcmd")
444            .args([
445                &pid.to_string(),
446                "JFR.stop",
447                &format!("name={}", recording_name),
448            ])
449            .output();
450
451        // Parse the JFR file
452        if !Path::new(&jfr_file).exists() {
453            return Err(format!("JFR file {} not found", jfr_file));
454        }
455
456        let profile_data = self.parse_jfr_profile(&jfr_file)?;
457
458        // Clean up
459        let _ = fs::remove_file(&jfr_file);
460
461        // Build result
462        let mut details = Vec::new();
463        details.push(format!(
464            "=== Java Flight Recorder Profile (PID: {}) ===",
465            pid
466        ));
467        details.push(format!("Total samples: {}", profile_data.total_samples));
468        details.push(format!(
469            "Unique methods: {}",
470            profile_data.execution_count.len()
471        ));
472        details.push("\nTop 10 Hot Methods:".to_string());
473
474        for (idx, (method_name, count)) in profile_data.hot_functions.iter().enumerate() {
475            let percentage = if profile_data.total_samples > 0 {
476                (*count as f64 / profile_data.total_samples as f64) * 100.0
477            } else {
478                0.0
479            };
480            details.push(format!(
481                "  {}. {} - {} samples ({:.2}%)",
482                idx + 1,
483                method_name,
484                count,
485                percentage
486            ));
487
488            if idx >= 9 {
489                break;
490            }
491        }
492
493        Ok(ProfileResult {
494            language: "Java/JVM".to_string(),
495            details,
496        })
497    }
498
499    /// Check if jcmd is available
500    fn ensure_jcmd_available(&self) -> Result<(), String> {
501        let output = Command::new("jcmd")
502            .arg("-h")
503            .output()
504            .map_err(|_| "jcmd not found. Please install JDK (Java Development Kit).")?;
505
506        if output.status.success() {
507            Ok(())
508        } else {
509            Err("jcmd is not available. Please install JDK.".to_string())
510        }
511    }
512
513    /// Check if the process is a Java process
514    fn check_java_process(&self, pid: u32) -> Result<(), String> {
515        let output = Command::new("jps")
516            .arg("-l")
517            .output()
518            .map_err(|_| "jps not found. Please install JDK.")?;
519
520        let jps_output = String::from_utf8_lossy(&output.stdout);
521
522        if jps_output.contains(&pid.to_string()) {
523            Ok(())
524        } else {
525            Err(format!(
526                "Process {} is not a Java process or not visible to jps",
527                pid
528            ))
529        }
530    }
531
532    pub fn profile_to_common_format(&self, java_file: &str) -> Result<CommonProfileData, String> {
533        println!("Starting Java runtime profiling for JSON export...");
534
535        let profile_data = self.run_java_profiler(java_file)?;
536
537        // Build function stats from runtime data
538        let mut function_stats = HashMap::new();
539
540        for (method_name, count) in &profile_data.execution_count {
541            let percentage = if profile_data.total_samples > 0 {
542                (*count as f64 / profile_data.total_samples as f64) * 100.0
543            } else {
544                0.0
545            };
546
547            function_stats.insert(
548                method_name.clone(),
549                FunctionStats {
550                    name: method_name.clone(),
551                    execution_count: *count,
552                    percentage,
553                    line_number: None,
554                    file_path: None,
555                },
556            );
557        }
558
559        let hot_functions: Vec<HotFunction> = profile_data
560            .hot_functions
561            .iter()
562            .enumerate()
563            .map(|(idx, (name, samples))| {
564                let percentage = if profile_data.total_samples > 0 {
565                    (*samples as f64 / profile_data.total_samples as f64) * 100.0
566                } else {
567                    0.0
568                };
569                HotFunction {
570                    rank: idx + 1,
571                    name: name.clone(),
572                    samples: *samples,
573                    percentage,
574                }
575            })
576            .collect();
577
578        let runtime_metrics = RuntimeMetrics {
579            total_samples: profile_data.total_samples,
580            execution_duration_secs: 0,
581            functions_executed: profile_data.execution_count.len(),
582            function_stats,
583            hot_functions,
584        };
585
586        let static_metrics = StaticMetrics {
587            file_size_bytes: 0,
588            line_count: 0,
589            function_count: 0,
590            class_count: 0,
591            import_count: 0,
592            complexity_score: 0,
593        };
594
595        Ok(CommonProfileData {
596            language: "Java/JVM".to_string(),
597            source_file: java_file.to_string(),
598            timestamp: Utc::now().to_rfc3339(),
599            static_analysis: static_metrics,
600            runtime_analysis: Some(runtime_metrics),
601        })
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608
609    #[test]
610    fn test_profiler_new() {
611        let profiler = JavaProfiler::new();
612        assert_eq!(std::mem::size_of_val(&profiler), 0);
613    }
614
615    #[test]
616    fn test_profiler_default() {
617        let profiler = JavaProfiler::default();
618        assert_eq!(std::mem::size_of_val(&profiler), 0);
619    }
620}