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 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 let script_path = Path::new(java_file)
48 .canonicalize()
49 .map_err(|e| format!("Failed to resolve script path: {}", e))?;
50
51 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 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 script_path.with_extension("class")
79 } else {
80 script_path.clone()
81 };
82
83 let temp_dir = std::env::temp_dir();
85
86 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 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 let main_class = if is_jar {
116 script_path.to_str().unwrap().to_string()
118 } else if is_class {
119 script_path
121 .file_stem()
122 .and_then(|s| s.to_str())
123 .unwrap_or("Main")
124 .to_string()
125 } else {
126 script_path
128 .file_stem()
129 .and_then(|s| s.to_str())
130 .unwrap_or("Main")
131 .to_string()
132 };
133
134 let mut java_cmd = Command::new("java");
136
137 java_cmd
140 .arg("-XX:+UnlockCommercialFeatures") .arg("-XX:+FlightRecorder") .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 if let Some(parent) = class_file.parent() {
154 java_cmd.arg("-cp").arg(parent);
155 }
156 java_cmd.arg(&main_class);
157 }
158
159 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 thread::sleep(Duration::from_millis(500));
173
174 let actual_profile = if profile_file.exists() {
176 Some(profile_file.clone())
177 } else {
178 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 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 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 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 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 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 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 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 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 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 self.ensure_jcmd_available()?;
387
388 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 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 println!("⏳ Recording for 30 seconds...");
419 thread::sleep(Duration::from_secs(31));
420
421 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 let _ = Command::new("jcmd")
444 .args([
445 &pid.to_string(),
446 "JFR.stop",
447 &format!("name={}", recording_name),
448 ])
449 .output();
450
451 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 let _ = fs::remove_file(&jfr_file);
460
461 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 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 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 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}