1use byteorder::{BigEndian, ByteOrder, LittleEndian};
19use memmap2::{Mmap, MmapOptions};
20use std::collections::{HashMap, HashSet};
21use std::fmt;
22use std::fs;
23use std::path::PathBuf;
24
25#[derive(Debug)]
27pub enum JvmMonitorError {
28 ProcessNotFound(u32),
30 IoError(std::io::Error),
32 InvalidFormat(String),
34}
35
36impl fmt::Display for JvmMonitorError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 JvmMonitorError::ProcessNotFound(pid) => {
40 write!(f, "JVM process {} not found or access denied", pid)
41 }
42 JvmMonitorError::IoError(e) => write!(f, "IO Error: {}", e),
43 JvmMonitorError::InvalidFormat(msg) => write!(f, "Invalid hsperfdata format: {}", msg),
44 }
45 }
46}
47
48impl std::error::Error for JvmMonitorError {}
49
50impl From<std::io::Error> for JvmMonitorError {
51 fn from(err: std::io::Error) -> Self {
52 JvmMonitorError::IoError(err)
53 }
54}
55
56#[derive(Debug, Clone)]
58pub enum PerfValue {
59 Long(i64),
61 String(String),
63}
64
65#[derive(Debug)]
67struct EntryMeta {
68 data_type: u8,
69 data_offset: usize,
70 vector_length: usize,
71}
72
73#[derive(Debug, Clone)]
75pub struct JavaProcessInfo {
76 pub pid: u32,
78 pub name: String,
80}
81
82pub struct JvmMonitor {
87 mmap: Mmap,
88 is_little_endian: bool,
89 index: HashMap<String, EntryMeta>,
90 timer_frequency: f64,
91}
92
93impl JvmMonitor {
94 pub fn connect(host_pid: u32) -> Result<Self, JvmMonitorError> {
102 let path = Self::find_hsperfdata_file(host_pid)
103 .ok_or(JvmMonitorError::ProcessNotFound(host_pid))?;
104
105 let file = fs::File::open(&path)?;
106 let mmap = unsafe { MmapOptions::new().map(&file)? };
107
108 if mmap.len() < 32 || BigEndian::read_u32(&mmap[0..4]) != 0xcafec0c0 {
109 return Err(JvmMonitorError::InvalidFormat(
110 "Invalid magic number".into(),
111 ));
112 }
113
114 let is_le = mmap[4] == 1;
115 let entry_offset = Self::read_u32(&mmap[24..28], is_le) as usize;
116 let num_entries = Self::read_u32(&mmap[28..32], is_le) as usize;
117
118 let mut index = HashMap::with_capacity(num_entries);
119 let mut cursor = entry_offset;
120
121 for _ in 0..num_entries {
122 if cursor + 20 > mmap.len() {
123 break;
124 }
125 let entry_len = Self::read_u32(&mmap[cursor..cursor + 4], is_le) as usize;
126 let name_offset = Self::read_u32(&mmap[cursor + 4..cursor + 8], is_le) as usize;
127 let vector_len = Self::read_u32(&mmap[cursor + 8..cursor + 12], is_le) as usize;
128 let data_type = mmap[cursor + 12];
129 let data_offset = Self::read_u32(&mmap[cursor + 16..cursor + 20], is_le) as usize;
130
131 let n_start = cursor + name_offset;
132 let mut n_end = n_start;
133 while n_end < mmap.len() && mmap[n_end] != 0 {
134 n_end += 1;
135 }
136 let name = String::from_utf8_lossy(&mmap[n_start..n_end]).into_owned();
137
138 index.insert(
139 name,
140 EntryMeta {
141 data_type,
142 data_offset: cursor + data_offset,
143 vector_length: vector_len,
144 },
145 );
146 cursor += entry_len;
147 }
148
149 let mut monitor = Self {
150 mmap,
151 is_little_endian: is_le,
152 index,
153 timer_frequency: 0.0,
154 };
155 monitor.timer_frequency = monitor.read_long("sun.os.hrt.frequency") as f64;
156 Ok(monitor)
157 }
158
159 pub fn discover_all() -> Result<Vec<JavaProcessInfo>, JvmMonitorError> {
161 let mut processes = Vec::new();
162 let mut seen_host_pids = HashSet::new();
163
164 #[cfg(target_os = "linux")]
166 {
167 if let Ok(entries) = fs::read_dir("/proc") {
168 for entry_result in entries {
169 let entry = match entry_result {
170 Ok(e) => e,
171 Err(_) => continue,
172 };
173 let pid_str = entry.file_name().to_string_lossy().into_owned();
174 if pid_str.chars().all(|c| c.is_ascii_digit()) {
175 let host_pid: u32 = pid_str.parse().unwrap_or(0);
176 if host_pid == 0 || seen_host_pids.contains(&host_pid) {
177 continue;
178 }
179
180 if let Some(ns_pid) = Self::get_ns_pid(host_pid) {
181 let container_tmp =
182 PathBuf::from("/proc").join(&pid_str).join("root/tmp");
183 if let Some(path) = Self::find_perf_file_in_dir(&container_tmp, ns_pid)
184 {
185 if let Some(name) = Self::fast_extract_name(&path) {
186 processes.push(JavaProcessInfo {
187 pid: host_pid,
188 name,
189 });
190 seen_host_pids.insert(host_pid);
191 }
192 }
193 }
194 }
195 }
196 }
197 }
198
199 let base_tmp = Self::get_temp_root();
201
202 let user_env = if cfg!(windows) { "USERNAME" } else { "USER" };
204 if let Ok(user) = std::env::var(user_env) {
205 let user_dir = base_tmp.join(format!("hsperfdata_{}", user));
206 Self::scan_pids_in_folder(&user_dir, &mut processes, &mut seen_host_pids);
207 }
208
209 if processes.is_empty() {
211 if let Ok(entries) = fs::read_dir(&base_tmp) {
212 for entry_result in entries {
213 let entry = match entry_result {
214 Ok(e) => e,
215 Err(_) => continue,
216 };
217 let path = entry.path();
218 if path.is_dir()
219 && path
220 .file_name()
221 .map_or(false, |n| n.to_string_lossy().starts_with("hsperfdata_"))
222 {
223 Self::scan_pids_in_folder(&path, &mut processes, &mut seen_host_pids);
224 }
225 }
226 }
227 }
228
229 processes.sort_by_key(|p| p.pid);
230 Ok(processes)
231 }
232
233 fn scan_pids_in_folder(
234 folder: &PathBuf,
235 results: &mut Vec<JavaProcessInfo>,
236 seen: &mut HashSet<u32>,
237 ) {
238 if let Ok(p_entries) = fs::read_dir(folder) {
239 for p_entry_result in p_entries {
240 let p_entry = match p_entry_result {
241 Ok(entry) => entry,
242 Err(_) => continue,
243 };
244
245 if let Ok(pid) = p_entry.file_name().to_string_lossy().parse::<u32>() {
246 if !seen.contains(&pid) {
247 if let Some(name) = Self::fast_extract_name(&p_entry.path()) {
248 results.push(JavaProcessInfo { pid, name });
249 seen.insert(pid);
250 }
251 }
252 }
253 }
254 }
255 }
256
257 #[cfg(target_os = "linux")]
262 fn get_ns_pid(host_pid: u32) -> Option<u32> {
263 use std::io::{BufRead, BufReader};
264 let file = fs::File::open(format!("/proc/{}/status", host_pid)).ok()?;
265 let reader = BufReader::new(file);
266 for line_result in reader.lines() {
267 match line_result {
268 Ok(line) => {
269 if line.starts_with("NSpid:") {
270 return line.split_whitespace().last().and_then(|s| s.parse().ok());
271 }
272 }
273 Err(_) => break,
274 }
275 }
276 None
277 }
278
279 fn find_hsperfdata_file(host_pid: u32) -> Option<PathBuf> {
280 #[cfg(target_os = "linux")]
281 {
282 let ns_pid = Self::get_ns_pid(host_pid).unwrap_or(host_pid);
283 let container_tmp = PathBuf::from("/proc")
284 .join(host_pid.to_string())
285 .join("root/tmp");
286 if let Some(p) = Self::find_perf_file_in_dir(&container_tmp, ns_pid) {
287 return Some(p);
288 }
289 }
290
291 let base_tmp = Self::get_temp_root();
292 Self::find_perf_file_in_dir(&base_tmp, host_pid)
293 }
294
295 fn find_perf_file_in_dir(base_path: &PathBuf, target_pid: u32) -> Option<PathBuf> {
296 let pid_str = target_pid.to_string();
297 let entries = fs::read_dir(base_path).ok()?;
298 for entry_result in entries {
299 let entry = match entry_result {
300 Ok(e) => e,
301 Err(_) => continue,
302 };
303 let path = entry.path();
304 if path.is_dir()
305 && path
306 .file_name()
307 .map_or(false, |n| n.to_string_lossy().starts_with("hsperfdata_"))
308 {
309 let perf_file = path.join(&pid_str);
310 if perf_file.exists() {
311 return Some(perf_file);
312 }
313 }
314 }
315 None
316 }
317
318 fn fast_extract_name(path: &PathBuf) -> Option<String> {
319 let file = fs::File::open(path).ok()?;
320 let mmap = unsafe { MmapOptions::new().map(&file).ok()? };
321 if mmap.len() < 32 || &mmap[0..4] != &[0xca, 0xfe, 0xc0, 0xc0] {
322 return None;
323 }
324
325 let is_le = mmap[4] == 1;
326 let entry_offset = Self::read_u32(&mmap[24..28], is_le) as usize;
327 let num_entries = Self::read_u32(&mmap[28..32], is_le) as usize;
328 let mut cursor = entry_offset;
329 let target = b"sun.rt.javaCommand";
330
331 for _ in 0..num_entries {
332 if cursor + 20 > mmap.len() {
333 break;
334 }
335 let entry_len = Self::read_u32(&mmap[cursor..cursor + 4], is_le) as usize;
336 let name_offset = Self::read_u32(&mmap[cursor + 4..cursor + 8], is_le) as usize;
337 let data_offset = Self::read_u32(&mmap[cursor + 16..cursor + 20], is_le) as usize;
338
339 let n_start = cursor + name_offset;
340 if n_start + 18 <= mmap.len() && &mmap[n_start..n_start + 18] == target {
341 let d_start = cursor + data_offset;
342 let mut d_end = d_start;
343 while d_end < mmap.len() && mmap[d_end] != 0 && mmap[d_end] != b' ' {
344 d_end += 1;
345 }
346 return Some(String::from_utf8_lossy(&mmap[d_start..d_end]).into_owned());
347 }
348 cursor += entry_len;
349 }
350 None
351 }
352
353 pub fn read_metric(&self, key: &str) -> Option<PerfValue> {
357 let meta = self.index.get(key)?;
358 let start = meta.data_offset;
359
360 if meta.data_type == b'J' && start + 8 <= self.mmap.len() {
361 let val = Self::read_i64(&self.mmap[start..start + 8], self.is_little_endian);
362 Some(PerfValue::Long(val))
363 } else if meta.data_type == b'B'
364 && meta.vector_length > 0
365 && start + meta.vector_length <= self.mmap.len()
366 {
367 let mut end = start;
368 let limit = start + meta.vector_length;
369 while end < limit && self.mmap[end] != 0 {
370 end += 1;
371 }
372 let val = String::from_utf8_lossy(&self.mmap[start..end]).into_owned();
373 Some(PerfValue::String(val))
374 } else {
375 None
376 }
377 }
378
379 pub fn read_long(&self, key: &str) -> i64 {
381 if let Some(PerfValue::Long(v)) = self.read_metric(key) {
382 v
383 } else {
384 0
385 }
386 }
387
388 pub fn read_f64(&self, key: &str) -> f64 {
392 self.read_long(key) as f64
393 }
394
395 pub fn read_string(&self, key: &str) -> String {
397 if let Some(PerfValue::String(v)) = self.read_metric(key) {
398 v
399 } else {
400 "-".to_string()
401 }
402 }
403
404 fn read_long_first_available(&self, candidates: &[&str]) -> i64 {
405 for key in candidates {
406 let val = self.read_long(key);
407 if val > 0 {
408 return val;
409 }
410 }
411 0
412 }
413
414 fn to_seconds(&self, ticks: i64) -> f64 {
416 if self.timer_frequency > 0.0 {
417 ticks as f64 / self.timer_frequency
418 } else {
419 0.0
420 }
421 }
422
423 fn to_kb(&self, bytes: i64) -> f64 {
425 bytes as f64 / 1024.0
426 }
427
428 pub fn get_class_stats(&self) -> ClassStats {
434 ClassStats {
435 loaded: self.read_long("sun.cls.loadedClasses"),
436 bytes: self.to_kb(self.read_long("sun.cls.loadedBytes")),
437 unloaded: self.read_long("sun.cls.unloadedClasses"),
438 unloaded_bytes: self.to_kb(self.read_long("sun.cls.unloadedBytes")),
439 time: self.to_seconds(self.read_long("sun.cls.time")),
440 }
441 }
442
443 pub fn get_compiler_stats(&self) -> CompilerStats {
445 CompilerStats {
446 compiled: self.read_long("sun.ci.totalCompilations"),
447 failed: self.read_long("sun.ci.totalBailouts"),
448 invalid: self.read_long("sun.ci.totalInvalidations"),
449 time: self.to_seconds(self.read_long("sun.ci.totalTime")),
450 failed_type: self.read_string("sun.ci.lastFailedType"),
451 failed_method: self.read_string("sun.ci.lastFailedMethod"),
452 }
453 }
454
455 pub fn get_gc_stats(&self) -> GcStats {
457 let s0c = self.read_long_first_available(&[
458 "sun.gc.generation.0.space.1.capacity",
459 "sun.gc.generation.0.space.1.maxCapacity",
460 ]);
461 let s1c = self.read_long_first_available(&[
462 "sun.gc.generation.0.space.2.capacity",
463 "sun.gc.generation.0.space.2.maxCapacity",
464 ]);
465 let s0u = self.read_long("sun.gc.generation.0.space.1.used");
466 let s1u = self.read_long("sun.gc.generation.0.space.2.used");
467 let ec = self.read_long_first_available(&[
468 "sun.gc.generation.0.space.0.capacity",
469 "sun.gc.generation.0.capacity",
470 ]);
471 let eu = self.read_long("sun.gc.generation.0.space.0.used");
472
473 let oc = self.read_long_first_available(&[
474 "sun.gc.generation.1.space.0.capacity",
475 "sun.gc.generation.1.capacity",
476 "sun.gc.g1.old.capacity",
477 ]);
478 let ou = self.read_long_first_available(&[
479 "sun.gc.generation.1.space.0.used",
480 "sun.gc.generation.1.used",
481 "sun.gc.g1.old.used",
482 ]);
483
484 let mc = self.read_long_first_available(&[
485 "sun.gc.metaspace.capacity",
486 "sun.gc.generation.2.space.0.capacity",
487 "sun.gc.generation.2.capacity",
488 ]);
489 let mu = self.read_long_first_available(&[
490 "sun.gc.metaspace.used",
491 "sun.gc.generation.2.space.0.used",
492 "sun.gc.generation.2.used",
493 ]);
494 let ccsc = self.read_long("sun.gc.compressedclassspace.capacity");
495 let ccsu = self.read_long("sun.gc.compressedclassspace.used");
496
497 let ygc = self.read_long("sun.gc.collector.0.invocations");
498 let ygct = self.read_long("sun.gc.collector.0.time");
499 let fgc = self.read_long("sun.gc.collector.1.invocations");
500 let fgct = self.read_long("sun.gc.collector.1.time");
501
502 let cgc = self.read_long("sun.gc.collector.2.invocations");
504 let cgct = self.read_long("sun.gc.collector.2.time");
505
506 GcStats {
507 s0c: self.to_kb(s0c),
508 s1c: self.to_kb(s1c),
509 s0u: self.to_kb(s0u),
510 s1u: self.to_kb(s1u),
511 ec: self.to_kb(ec),
512 eu: self.to_kb(eu),
513 oc: self.to_kb(oc),
514 ou: self.to_kb(ou),
515 mc: self.to_kb(mc),
516 mu: self.to_kb(mu),
517 ccsc: self.to_kb(ccsc),
518 ccsu: self.to_kb(ccsu),
519 ygc: ygc as u64,
520 ygct: self.to_seconds(ygct),
521 fgc: fgc as u64,
522 fgct: self.to_seconds(fgct),
523 cgc: cgc as u64,
524 cgct: self.to_seconds(cgct),
525 gct: self.to_seconds(ygct + fgct + cgct),
526 lgcc: self.read_string("sun.gc.lastCause"),
527 gcc: self.read_string("sun.gc.cause"),
528 }
529 }
530
531 pub fn get_runtime_stats(&self) -> RuntimeStats {
533 let t_live = self.read_long("java.threads.live");
535 let t_daemon = self.read_long("java.threads.daemon");
536 let mut t_peak = self.read_long_first_available(&[
537 "java.threads.peak",
538 "java.threads.livePeak",
539 "java.threads.peakCount",
540 ]);
541 if t_peak == 0 {
542 t_peak = t_live;
543 }
544
545 let cc_used = self.to_kb(self.read_long_first_available(&[
548 "sun.ci.codeCache.used",
549 "sun.gc.generation.2.space.0.used",
550 "java.ci.totalCodeSize",
551 ]));
552 let cc_cap = self.to_kb(self.read_long_first_available(&[
553 "sun.ci.codeCache.capacity",
554 "sun.ci.codeCache.maxCapacity",
555 "sun.ci.codeCache.maxSize",
556 "sun.gc.generation.2.space.0.capacity",
557 ]));
558
559 let cc_util = if cc_cap > 0.0 { cc_used / cc_cap } else { 0.0 };
560
561 let safepoint_ticks = self
563 .read_long_first_available(&["sun.rt.safepointTime", "sun.threads.vmOperationTime"]);
564 let app_ticks = self.read_long("sun.rt.applicationTime");
565 let safepoints =
566 self.read_long_first_available(&["sun.rt.safepoints", "java.rt.safepoints"]);
567
568 let safepoint_time_s = self.to_seconds(safepoint_ticks);
569 let app_time_s = self.to_seconds(app_ticks);
570
571 let total_ticks = safepoint_ticks + app_ticks;
572 let overhead = if total_ticks > 0 {
573 safepoint_ticks as f64 / total_ticks as f64
574 } else {
575 0.0
576 };
577
578 RuntimeStats {
579 threads_live: t_live,
580 threads_daemon: t_daemon,
581 threads_peak: t_peak,
582 code_cache_used: cc_used,
583 code_cache_capacity: cc_cap,
584 code_cache_utilization: cc_util,
585 safepoints,
586 safepoint_time_s,
587 app_time_s,
588 safepoint_overhead: overhead,
589 }
590 }
591
592 fn read_u32(bytes: &[u8], is_le: bool) -> u32 {
593 if is_le {
594 LittleEndian::read_u32(bytes)
595 } else {
596 BigEndian::read_u32(bytes)
597 }
598 }
599
600 fn read_i64(bytes: &[u8], is_le: bool) -> i64 {
601 if is_le {
602 LittleEndian::read_i64(bytes)
603 } else {
604 BigEndian::read_i64(bytes)
605 }
606 }
607
608 #[cfg(target_os = "linux")]
609 fn get_temp_root() -> PathBuf {
610 PathBuf::from("/tmp")
611 }
612
613 #[cfg(target_os = "macos")]
614 fn get_temp_root() -> PathBuf {
615 std::env::temp_dir()
616 }
617
618 #[cfg(target_os = "windows")]
619 fn get_temp_root() -> PathBuf {
620 std::env::temp_dir()
621 }
622}
623
624#[derive(Debug, Clone, Default)]
628pub struct ClassStats {
629 pub loaded: i64,
631 pub bytes: f64,
633 pub unloaded: i64,
635 pub unloaded_bytes: f64,
637 pub time: f64,
639}
640
641#[derive(Debug, Clone, Default)]
643pub struct CompilerStats {
644 pub compiled: i64,
646 pub failed: i64,
648 pub invalid: i64,
650 pub time: f64,
652 pub failed_type: String,
654 pub failed_method: String,
656}
657
658#[derive(Debug, Clone, Default)]
660pub struct GcStats {
661 pub s0c: f64,
663 pub s1c: f64,
665 pub s0u: f64,
667 pub s1u: f64,
669 pub ec: f64,
671 pub eu: f64,
673 pub oc: f64,
675 pub ou: f64,
677 pub mc: f64,
679 pub mu: f64,
681 pub ccsc: f64,
683 pub ccsu: f64,
685 pub ygc: u64,
687 pub ygct: f64,
689 pub fgc: u64,
691 pub fgct: f64,
693 pub cgc: u64,
695 pub cgct: f64,
697 pub gct: f64,
699 pub lgcc: String,
701 pub gcc: String,
703}
704
705#[derive(Debug, Clone, Default)]
707pub struct RuntimeStats {
708 pub threads_live: i64,
710 pub threads_daemon: i64,
712 pub threads_peak: i64,
714
715 pub code_cache_used: f64,
717 pub code_cache_capacity: f64,
719 pub code_cache_utilization: f64,
721
722 pub safepoints: i64,
724 pub safepoint_time_s: f64,
726 pub app_time_s: f64,
728 pub safepoint_overhead: f64,
730}