Skip to main content

jmon_rs/
lib.rs

1//! # JMon-rs
2//!
3//! `jmon-rs` is a high-performance, cross-platform JVM monitoring library.
4//! It retrieves real-time JVM metrics (GC, class loading, memory, etc.) by
5//! parsing `hsperfdata` memory-mapped files.
6//!
7//! ## Example
8//!
9//! ```rust,no_run
10//! use jmon_rs::JvmMonitor;
11//!
12//! let pid = 12345;
13//! let monitor = JvmMonitor::connect(pid).expect("Failed to connect");
14//! let gc = monitor.get_gc_stats();
15//! println!("Eden Used: {} KB", gc.eu);
16//! ```
17
18use 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/// Error types for JVM Monitoring operations.
26#[derive(Debug)]
27pub enum JvmMonitorError {
28    /// The specified JVM process ID was not found or access was denied.
29    ProcessNotFound(u32),
30    /// Standard I/O error occurred during file access or mapping.
31    IoError(std::io::Error),
32    /// The hsperfdata file format is invalid or corrupted.
33    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/// Represents a raw performance counter value from the JVM.
57#[derive(Debug, Clone)]
58pub enum PerfValue {
59    /// A 64-bit integer value (e.g., counters, sizes, timestamps).
60    Long(i64),
61    /// A string value (e.g., version strings, GC causes).
62    String(String),
63}
64
65/// Internal metadata for O(1) reads
66#[derive(Debug)]
67struct EntryMeta {
68    data_type: u8,
69    data_offset: usize,
70    vector_length: usize,
71}
72
73/// Information about a discovered Java process, similar to the output of `jps`.
74#[derive(Debug, Clone)]
75pub struct JavaProcessInfo {
76    /// The process ID (PID) of the JVM.
77    pub pid: u32,
78    /// Short name of the application (e.g., the Main class or JAR filename).
79    pub name: String,
80}
81
82/// The main JVM Monitor instance.
83///
84/// Use `JvmMonitor::connect(pid)` to start monitoring a specific process,
85/// or `JvmMonitor::discover_all()` to find all running JVMs.
86pub struct JvmMonitor {
87    mmap: Mmap,
88    is_little_endian: bool,
89    index: HashMap<String, EntryMeta>,
90    timer_frequency: f64,
91}
92
93impl JvmMonitor {
94    /// Connects to a running JVM process by its PID.
95    ///
96    /// This will attempt to find and memory-map the `hsperfdata` file for the given PID.
97    ///
98    /// # Errors
99    /// Returns `JvmMonitorError::ProcessNotFound` if the process is not found.
100    /// Returns `JvmMonitorError::InvalidFormat` if the data file is corrupted.
101    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    /// Discovers all Java processes. Supports Host and Container (Docker/K8s) PIDs.
160    pub fn discover_all() -> Result<Vec<JavaProcessInfo>, JvmMonitorError> {
161        let mut processes = Vec::new();
162        let mut seen_host_pids = HashSet::new();
163
164        // --- 1. Linux Specific: Container discovery via /proc ---
165        #[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        // --- 2. Windows/macOS/Linux Host: Direct path and fallback scan ---
200        let base_tmp = Self::get_temp_root();
201
202        // Fast path: Target current user folder directly to avoid large directory scans (especially on Windows)
203        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        // Fallback: Scan base temp directory for other users' hsperfdata folders
210        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    // ==========================================
258    // Internal Path and PID Resolution
259    // ==========================================
260
261    #[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    /// Reads a raw performance counter value by its full internal name (e.g., "sun.gc.cause").
354    ///
355    /// Returns `None` if the key does not exist or the data type is unsupported.
356    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    /// Reads a 64-bit integer metric. Returns `0` if the key is missing or not a long.
380    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    /// Reads a metric as a double-precision float.
389    ///
390    /// Note: Most JVM counters are stored as `i64`, this method casts them to `f64`.
391    pub fn read_f64(&self, key: &str) -> f64 {
392        self.read_long(key) as f64
393    }
394
395    /// Reads a string metric. Returns `"-"` if the key is missing or not a string.
396    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    // Helper: Convert Ticks to Seconds
415    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    // Helper: Convert Bytes to KB
424    fn to_kb(&self, bytes: i64) -> f64 {
425        bytes as f64 / 1024.0
426    }
427
428    // ==========================================
429    // Public API: High Level Stats
430    // ==========================================
431
432    /// Retrieves class loading statistics, equivalent to `jstat -class`.
433    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    /// Retrieves JIT compiler statistics, equivalent to `jstat -compiler`.
444    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    /// Retrieves garbage collection statistics, equivalent to `jstat -gc` or `jstat -gccause`.
456    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        // ZGC or Shenandoah usually map to collector.2
503        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    /// Retrieves various runtime statistics including threads, code cache, and safepoints.
532    pub fn get_runtime_stats(&self) -> RuntimeStats {
533        // 1. Threads
534        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        // 2. Code Cache
546        // note:sun.ci.codeCache or sun.ci.codeCache.maxSize
547        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        // 3. Safepoints
562        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// ================= Data Structures =================
625
626/// Class loading statistics.
627#[derive(Debug, Clone, Default)]
628pub struct ClassStats {
629    /// Number of classes loaded.
630    pub loaded: i64,
631    /// Total size of classes loaded (KB).
632    pub bytes: f64,
633    /// Number of classes unloaded.
634    pub unloaded: i64,
635    /// Total size of classes unloaded (KB).
636    pub unloaded_bytes: f64,
637    /// Time spent in class loading (seconds).
638    pub time: f64,
639}
640
641/// JIT compiler statistics.
642#[derive(Debug, Clone, Default)]
643pub struct CompilerStats {
644    /// Total number of compilations performed.
645    pub compiled: i64,
646    /// Total number of failed compilations.
647    pub failed: i64,
648    /// Total number of invalidated compilations.
649    pub invalid: i64,
650    /// Total time spent in compilation (seconds).
651    pub time: f64,
652    /// Type of the last failed compilation.
653    pub failed_type: String,
654    /// Name of the last failed method.
655    pub failed_method: String,
656}
657
658/// Garbage Collection statistics.
659#[derive(Debug, Clone, Default)]
660pub struct GcStats {
661    /// Survivor space 0 capacity (KB).
662    pub s0c: f64,
663    /// Survivor space 1 capacity (KB).
664    pub s1c: f64,
665    /// Survivor space 0 used (KB).
666    pub s0u: f64,
667    /// Survivor space 1 used (KB).
668    pub s1u: f64,
669    /// Eden space capacity (KB).
670    pub ec: f64,
671    /// Eden space used (KB).
672    pub eu: f64,
673    /// Old space capacity (KB).
674    pub oc: f64,
675    /// Old space used (KB).
676    pub ou: f64,
677    /// Metaspace capacity (KB).
678    pub mc: f64,
679    /// Metaspace used (KB).
680    pub mu: f64,
681    /// Compressed class space capacity (KB).
682    pub ccsc: f64,
683    /// Compressed class space used (KB).
684    pub ccsu: f64,
685    /// Number of young generation GC events.
686    pub ygc: u64,
687    /// Total time spent in young generation GC (seconds).
688    pub ygct: f64,
689    /// Number of full GC events.
690    pub fgc: u64,
691    /// Total time spent in full GC (seconds).
692    pub fgct: f64,
693    /// Number of concurrent GC events (e.g., ZGC, Shenandoah).
694    pub cgc: u64,
695    /// Total time spent in concurrent GC (seconds).
696    pub cgct: f64,
697    /// Total garbage collection time (seconds).
698    pub gct: f64,
699    /// Last GC cause.
700    pub lgcc: String,
701    /// Current GC cause.
702    pub gcc: String,
703}
704
705/// JVM Runtime, Threads, and Safepoint statistics.
706#[derive(Debug, Clone, Default)]
707pub struct RuntimeStats {
708    /// Number of live threads.
709    pub threads_live: i64,
710    /// Number of daemon threads.
711    pub threads_daemon: i64,
712    /// Peak number of threads.
713    pub threads_peak: i64,
714
715    /// Code Cache memory used (KB).
716    pub code_cache_used: f64,
717    /// Code Cache total capacity (KB).
718    pub code_cache_capacity: f64,
719    /// Code Cache utilization ratio (0.0 to 1.0).
720    pub code_cache_utilization: f64,
721
722    /// Total number of safepoints reached.
723    pub safepoints: i64,
724    /// Total time spent in safepoints (seconds).
725    pub safepoint_time_s: f64,
726    /// Total time spent running the application (seconds).
727    pub app_time_s: f64,
728    /// Percentage of time spent in safepoints (0.0 to 1.0).
729    pub safepoint_overhead: f64,
730}