Skip to main content

solana_core/
system_monitor_service.rs

1#[cfg(target_arch = "x86")]
2use core::arch::x86::{__cpuid, __cpuid_count, __get_cpuid_max, CpuidResult};
3#[cfg(target_arch = "x86_64")]
4use core::arch::x86_64::{__cpuid, __cpuid_count, __get_cpuid_max, CpuidResult};
5#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
6use num_enum::{IntoPrimitive, TryFromPrimitive};
7#[cfg(target_os = "linux")]
8use {
9    agave_xdp::device::NetworkDevice,
10    std::{
11        fs::{self, File},
12        io::{self, BufReader, ErrorKind},
13    },
14};
15use {
16    solana_time_utils::AtomicInterval,
17    std::{
18        collections::HashMap,
19        io::BufRead,
20        sync::{
21            Arc,
22            atomic::{AtomicBool, Ordering},
23        },
24        thread::{self, Builder, JoinHandle, sleep},
25        time::Duration,
26    },
27    sys_info::{Error, LoadAvg},
28};
29
30const MS_PER_S: u64 = 1_000;
31const MS_PER_M: u64 = MS_PER_S * 60;
32const MS_PER_H: u64 = MS_PER_M * 60;
33const SAMPLE_INTERVAL_UDP_MS: u64 = 2 * MS_PER_S;
34const SAMPLE_INTERVAL_OS_NETWORK_LIMITS_MS: u64 = MS_PER_H;
35const SAMPLE_INTERVAL_XDP_NETWORK_CONFIG_MS: u64 = MS_PER_H;
36const SAMPLE_INTERVAL_MEM_MS: u64 = 5 * MS_PER_S;
37const SAMPLE_INTERVAL_CPU_MS: u64 = 10 * MS_PER_S;
38const SAMPLE_INTERVAL_CPU_ID_MS: u64 = MS_PER_H;
39const SAMPLE_INTERVAL_DISK_MS: u64 = 5 * MS_PER_S;
40const SLEEP_INTERVAL: Duration = Duration::from_millis(500);
41
42#[cfg(target_os = "linux")]
43const PROC_NET_SNMP_PATH: &str = "/proc/net/snmp";
44#[cfg(target_os = "linux")]
45const PROC_NET_DEV_PATH: &str = "/proc/net/dev";
46#[cfg(target_os = "linux")]
47const SYS_BLOCK_PATH: &str = "/sys/block";
48#[cfg(target_os = "linux")]
49const PCI_IDS_PATHS: &[&str] = &["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"];
50
51#[derive(Clone, Debug)]
52pub struct XdpNetworkConfigReport {
53    pub zero_copy: bool,
54    pub interface: String,
55}
56
57#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
58struct XdpNetworkConfigMetrics {
59    kernel_version: String,
60    driver: String,
61    vendor: String,
62    model: String,
63}
64
65pub struct SystemMonitorService {
66    thread_hdl: JoinHandle<()>,
67}
68
69#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
70struct UdpStats {
71    in_datagrams: u64,
72    no_ports: u64,
73    in_errors: u64,
74    out_datagrams: u64,
75    rcvbuf_errors: u64,
76    sndbuf_errors: u64,
77    in_csum_errors: u64,
78    ignored_multi: u64,
79}
80
81#[derive(Default)]
82#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
83// These stats are aggregated across all network devices excluding the loopback interface.
84struct NetDevStats {
85    // Number of bytes received
86    rx_bytes: u64,
87    // Number of packets received
88    rx_packets: u64,
89    // Number of receive errors detected by device driver
90    rx_errs: u64,
91    // Number of receive packets dropped by the device driver (not included in error count)
92    rx_drops: u64,
93    // Number of receive FIFO buffer errors
94    rx_fifo: u64,
95    // Number of receive packet framing errors
96    rx_frame: u64,
97    // Number of compressed packets received
98    rx_compressed: u64,
99    // Number of multicast frames received by device driver
100    rx_multicast: u64,
101    // Number of bytes transmitted
102    tx_bytes: u64,
103    // Number of packets transmitted
104    tx_packets: u64,
105    // Number of transmit errors detected by device driver
106    tx_errs: u64,
107    // Number of transmit packets dropped by device driver
108    tx_drops: u64,
109    // Number of transmit FIFO buffer errors
110    tx_fifo: u64,
111    // Number of transmit collisions detected
112    tx_colls: u64,
113    // Number of transmit carrier losses detected by device driver
114    tx_carrier: u64,
115    // Number of compressed packets transmitted
116    tx_compressed: u64,
117}
118
119#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
120struct NetStats {
121    udp_stats: UdpStats,
122    net_dev_stats: NetDevStats,
123}
124
125struct CpuInfo {
126    cpu_num: u32,
127    cpu_freq_mhz: u64,
128    load_avg: LoadAvg,
129    num_threads: u64,
130}
131
132#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
133#[derive(IntoPrimitive)]
134#[repr(i64)]
135enum CpuManufacturer {
136    Other,
137    Intel,
138    Amd,
139}
140
141#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
142#[derive(IntoPrimitive, TryFromPrimitive, PartialEq, PartialOrd)]
143#[repr(u32)]
144// The value passed into cpuid via eax, to control what the result means
145enum CpuidParamValue {
146    Manufacturer = 0,
147    Processor = 1,
148    Cache = 2,
149    SerialNumber = 3,
150    Topology = 4,
151    Unsupported = 5,
152    ThermalAndPower = 6,
153    Extended = 7,
154}
155#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
156const CPUID_PARAM_MAX_SUPPORTED_VALUE: u32 = 7;
157
158#[derive(Default)]
159#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
160// These stats are aggregated across all storage devices excluding internal loopbacks.
161// Fields are cumulative since boot with the exception of 'num_disks' and 'io_in_progress'
162struct DiskStats {
163    reads_completed: u64,
164    reads_merged: u64,
165    sectors_read: u64,
166    time_reading_ms: u64,
167    writes_completed: u64,
168    writes_merged: u64,
169    sectors_written: u64,
170    time_writing_ms: u64,
171    io_in_progress: u64,
172    time_io_ms: u64,
173    // weighted time multiplies time performing IO by number of commands in the queue
174    time_io_weighted_ms: u64,
175    discards_completed: u64,
176    discards_merged: u64,
177    sectors_discarded: u64,
178    time_discarding: u64,
179    flushes_completed: u64,
180    time_flushing: u64,
181    num_disks: u64,
182}
183
184impl UdpStats {
185    fn from_map(udp_stats: &HashMap<String, u64>) -> Self {
186        Self {
187            in_datagrams: *udp_stats.get("InDatagrams").unwrap_or(&0),
188            no_ports: *udp_stats.get("NoPorts").unwrap_or(&0),
189            in_errors: *udp_stats.get("InErrors").unwrap_or(&0),
190            out_datagrams: *udp_stats.get("OutDatagrams").unwrap_or(&0),
191            rcvbuf_errors: *udp_stats.get("RcvbufErrors").unwrap_or(&0),
192            sndbuf_errors: *udp_stats.get("SndbufErrors").unwrap_or(&0),
193            in_csum_errors: *udp_stats.get("InCsumErrors").unwrap_or(&0),
194            ignored_multi: *udp_stats.get("IgnoredMulti").unwrap_or(&0),
195        }
196    }
197}
198
199impl DiskStats {
200    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
201    fn accumulate(&mut self, other: &DiskStats) {
202        self.reads_completed += other.reads_completed;
203        self.reads_merged += other.reads_merged;
204        self.sectors_read += other.sectors_read;
205        self.time_reading_ms += other.time_reading_ms;
206        self.writes_completed += other.writes_completed;
207        self.writes_merged += other.writes_merged;
208        self.sectors_written += other.sectors_written;
209        self.time_writing_ms += other.time_writing_ms;
210        self.io_in_progress += other.io_in_progress;
211        self.time_io_ms += other.time_io_ms;
212        self.time_io_weighted_ms += other.time_io_weighted_ms;
213        self.discards_completed += other.discards_completed;
214        self.discards_merged += other.discards_merged;
215        self.sectors_discarded += other.sectors_discarded;
216        self.time_discarding += other.time_discarding;
217        self.flushes_completed += other.flushes_completed;
218        self.time_flushing += other.time_flushing;
219    }
220}
221
222fn platform_id() -> String {
223    format!(
224        "{}/{}/{}",
225        std::env::consts::FAMILY,
226        std::env::consts::OS,
227        std::env::consts::ARCH
228    )
229}
230
231#[cfg(target_os = "linux")]
232fn read_net_stats() -> Result<NetStats, String> {
233    let file_path_snmp = PROC_NET_SNMP_PATH;
234    let file_snmp = File::open(file_path_snmp).map_err(|e| e.to_string())?;
235    let mut reader_snmp = BufReader::new(file_snmp);
236
237    let file_path_dev = PROC_NET_DEV_PATH;
238    let file_dev = File::open(file_path_dev).map_err(|e| e.to_string())?;
239    let mut reader_dev = BufReader::new(file_dev);
240
241    let udp_stats = parse_udp_stats(&mut reader_snmp)?;
242    let net_dev_stats = parse_net_dev_stats(&mut reader_dev)?;
243    Ok(NetStats {
244        udp_stats,
245        net_dev_stats,
246    })
247}
248
249#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
250fn parse_udp_stats(reader_snmp: &mut impl BufRead) -> Result<UdpStats, String> {
251    let mut udp_lines = Vec::default();
252    for line in reader_snmp.lines() {
253        let line = line.map_err(|e| e.to_string())?;
254        if line.starts_with("Udp:") {
255            udp_lines.push(line);
256            if udp_lines.len() == 2 {
257                break;
258            }
259        }
260    }
261    if udp_lines.len() != 2 {
262        return Err(format!(
263            "parse error, expected 2 lines, num lines: {}",
264            udp_lines.len()
265        ));
266    }
267
268    let pairs: Vec<_> = udp_lines[0]
269        .split_ascii_whitespace()
270        .zip(udp_lines[1].split_ascii_whitespace())
271        .collect();
272    let udp_stats: HashMap<String, u64> = pairs[1..]
273        .iter()
274        .map(|(label, val)| (label.to_string(), val.parse::<u64>().unwrap()))
275        .collect();
276
277    let stats = UdpStats::from_map(&udp_stats);
278    Ok(stats)
279}
280
281#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
282fn parse_net_dev_stats(reader_dev: &mut impl BufRead) -> Result<NetDevStats, String> {
283    let mut stats = NetDevStats::default();
284    for (line_number, line) in reader_dev.lines().enumerate() {
285        if line_number < 2 {
286            // Skip first two lines with header information.
287            continue;
288        }
289
290        let line = line.map_err(|e| e.to_string())?;
291        let values: Vec<_> = line.split_ascii_whitespace().collect();
292
293        if values.len() != 17 {
294            return Err("parse error, expected exactly 17 stat elements".to_string());
295        }
296        if values[0] == "lo:" {
297            // Filter out the loopback network interface as we are only concerned with
298            // external traffic.
299            continue;
300        }
301
302        stats.rx_bytes += values[1].parse::<u64>().map_err(|e| e.to_string())?;
303        stats.rx_packets += values[2].parse::<u64>().map_err(|e| e.to_string())?;
304        stats.rx_errs += values[3].parse::<u64>().map_err(|e| e.to_string())?;
305        stats.rx_drops += values[4].parse::<u64>().map_err(|e| e.to_string())?;
306        stats.rx_fifo += values[5].parse::<u64>().map_err(|e| e.to_string())?;
307        stats.rx_frame += values[6].parse::<u64>().map_err(|e| e.to_string())?;
308        stats.rx_compressed += values[7].parse::<u64>().map_err(|e| e.to_string())?;
309        stats.rx_multicast += values[8].parse::<u64>().map_err(|e| e.to_string())?;
310        stats.tx_bytes += values[9].parse::<u64>().map_err(|e| e.to_string())?;
311        stats.tx_packets += values[10].parse::<u64>().map_err(|e| e.to_string())?;
312        stats.tx_errs += values[11].parse::<u64>().map_err(|e| e.to_string())?;
313        stats.tx_drops += values[12].parse::<u64>().map_err(|e| e.to_string())?;
314        stats.tx_fifo += values[13].parse::<u64>().map_err(|e| e.to_string())?;
315        stats.tx_colls += values[14].parse::<u64>().map_err(|e| e.to_string())?;
316        stats.tx_carrier += values[15].parse::<u64>().map_err(|e| e.to_string())?;
317        stats.tx_compressed += values[16].parse::<u64>().map_err(|e| e.to_string())?;
318    }
319
320    Ok(stats)
321}
322
323#[cfg(target_os = "linux")]
324fn try_resolve_network_device_pci_names(
325    interface: &str,
326    vendor_id: &str,
327    model_id: &str,
328) -> Result<(String, String), String> {
329    let class = match read_network_device_sysfs_value(interface, "class") {
330        Ok(class) => class,
331        Err(err) => {
332            return Err(err.to_string());
333        }
334    };
335    if !normalize_pci_id(&class).is_some_and(|class| class.starts_with("02")) {
336        return Err(format!(
337            "xdp network config device for interface {interface} has non-network PCI class {class}"
338        ));
339    }
340
341    let (Some(vendor_id), Some(model_id)) =
342        (normalize_pci_id(vendor_id), normalize_pci_id(model_id))
343    else {
344        return Err(format!(
345            "failed to normalize xdp network config PCI ids for interface {interface}: \
346             vendor_id={vendor_id} model_id={model_id}"
347        ));
348    };
349
350    match read_pci_database_device_names(&vendor_id, &model_id) {
351        Ok(Some((vendor, model))) => Ok((vendor, model)),
352        Ok(None) => Err(format!(
353            "failed to find xdp network config PCI names for vendor_id={vendor_id} \
354             model_id={model_id}"
355        )),
356        Err(err) => Err(format!(
357            "failed to read PCI database for xdp network config: {err}"
358        )),
359    }
360}
361
362#[cfg(target_os = "linux")]
363fn read_network_device_sysfs_value(interface: &str, file_name: &str) -> io::Result<String> {
364    let path = format!("/sys/class/net/{interface}/device/{file_name}");
365    let value = fs::read_to_string(&path)
366        .map_err(|e| {
367            io::Error::new(
368                e.kind(),
369                format!("Failed to read {file_name} for interface {interface}: {e}"),
370            )
371        })?
372        .trim()
373        .to_string();
374
375    if value.is_empty() {
376        return Err(io::Error::new(
377            ErrorKind::InvalidData,
378            format!("Empty {file_name} for interface {interface}"),
379        ));
380    }
381
382    Ok(value)
383}
384
385#[cfg(target_os = "linux")]
386fn normalize_pci_id(value: &str) -> Option<String> {
387    let value = value
388        .strip_prefix("0x")
389        .or_else(|| value.strip_prefix("0X"))
390        .unwrap_or(value);
391
392    (!value.is_empty() && value.chars().all(|char| char.is_ascii_hexdigit()))
393        .then(|| value.to_ascii_lowercase())
394}
395
396#[cfg(target_os = "linux")]
397fn read_pci_database_device_names(
398    vendor_id: &str,
399    model_id: &str,
400) -> io::Result<Option<(String, String)>> {
401    for path in PCI_IDS_PATHS {
402        let contents = match fs::read_to_string(path) {
403            Ok(contents) => contents,
404            Err(err) if err.kind() == ErrorKind::NotFound => continue,
405            Err(err) => {
406                return Err(io::Error::new(
407                    err.kind(),
408                    format!("Failed to read {path}: {err}"),
409                ));
410            }
411        };
412        return Ok(parse_pci_database_device_names(
413            &contents, vendor_id, model_id,
414        ));
415    }
416
417    Err(io::Error::new(
418        ErrorKind::NotFound,
419        format!("PCI database not found in {}", PCI_IDS_PATHS.join(", ")),
420    ))
421}
422
423#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
424fn parse_pci_database_device_names(
425    contents: &str,
426    vendor_id: &str,
427    model_id: &str,
428) -> Option<(String, String)> {
429    let mut matching_vendor_name: Option<String> = None;
430
431    for line in contents.lines() {
432        if line.is_empty() || line.starts_with('#') || line.starts_with("\t\t") {
433            continue;
434        }
435        //  PCI class-code section starts, done with the vendor/device section.
436        if line.starts_with("C ") {
437            break;
438        }
439
440        if let Some(device_line) = line.strip_prefix('\t') {
441            let Some(vendor_name) = matching_vendor_name.as_ref() else {
442                continue;
443            };
444
445            let Some((device_line_id, device_name)) = parse_pci_ids_line(device_line) else {
446                continue;
447            };
448            if device_line_id.eq_ignore_ascii_case(model_id) {
449                return Some((vendor_name.clone(), device_name.to_string()));
450            }
451            continue;
452        }
453
454        let Some((vendor_line_id, vendor_name)) = parse_pci_ids_line(line) else {
455            continue;
456        };
457        matching_vendor_name = vendor_line_id
458            .eq_ignore_ascii_case(vendor_id)
459            .then(|| vendor_name.to_string());
460    }
461
462    None
463}
464
465#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
466fn parse_pci_ids_line(line: &str) -> Option<(&str, &str)> {
467    let line = line.trim();
468    let id_end = line.find(char::is_whitespace)?;
469    let id = &line[..id_end];
470    let name = line[id_end..].trim();
471    (!id.is_empty() && !name.is_empty()).then_some((id, name))
472}
473
474#[cfg(target_os = "linux")]
475pub fn verify_net_stats_access() -> Result<(), String> {
476    read_net_stats()?;
477    Ok(())
478}
479
480#[cfg(not(target_os = "linux"))]
481pub fn verify_net_stats_access() -> Result<(), String> {
482    Ok(())
483}
484
485#[cfg(target_os = "linux")]
486fn read_disk_stats() -> Result<DiskStats, String> {
487    let mut stats = DiskStats::default();
488    let mut num_disks = 0;
489    let blk_device_dir_iter = std::fs::read_dir(SYS_BLOCK_PATH).map_err(|e| e.to_string())?;
490    blk_device_dir_iter
491        .filter_map(|blk_device_dir| {
492            match blk_device_dir {
493                Ok(blk_device_dir) => {
494                    let blk_device_dir_name = &blk_device_dir.file_name();
495                    let blk_device_dir_name = blk_device_dir_name.to_string_lossy();
496                    if blk_device_dir_name.starts_with("loop")
497                        || blk_device_dir_name.starts_with("dm")
498                        || blk_device_dir_name.starts_with("md")
499                    {
500                        // Filter out loopback devices, dmcrypt volumes, and mdraid volumes
501                        return None;
502                    }
503                    let mut path = blk_device_dir.path();
504                    path.push("stat");
505                    File::open(path).ok()
506                }
507                Err(_) => None,
508            }
509        })
510        .for_each(|file_diskstats| {
511            let mut reader_diskstats = BufReader::new(file_diskstats);
512            stats.accumulate(&parse_disk_stats(&mut reader_diskstats).unwrap_or_default());
513            num_disks += 1;
514        });
515    stats.num_disks = num_disks;
516    Ok(stats)
517}
518
519#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
520fn parse_disk_stats(reader_diskstats: &mut impl BufRead) -> Result<DiskStats, String> {
521    let mut stats = DiskStats::default();
522    let mut line = String::new();
523    reader_diskstats
524        .read_line(&mut line)
525        .map_err(|e| e.to_string())?;
526    let values: Vec<_> = line.split_ascii_whitespace().collect();
527    let num_elements = values.len();
528
529    if num_elements != 11 && num_elements != 15 && num_elements != 17 {
530        return Err("parse error, unknown number of disk stat elements".to_string());
531    }
532
533    stats.reads_completed = values[0].parse::<u64>().map_err(|e| e.to_string())?;
534    stats.reads_merged = values[1].parse::<u64>().map_err(|e| e.to_string())?;
535    stats.sectors_read = values[2].parse::<u64>().map_err(|e| e.to_string())?;
536    stats.time_reading_ms = values[3].parse::<u64>().map_err(|e| e.to_string())?;
537    stats.writes_completed = values[4].parse::<u64>().map_err(|e| e.to_string())?;
538    stats.writes_merged = values[5].parse::<u64>().map_err(|e| e.to_string())?;
539    stats.sectors_written = values[6].parse::<u64>().map_err(|e| e.to_string())?;
540    stats.time_writing_ms = values[7].parse::<u64>().map_err(|e| e.to_string())?;
541    stats.io_in_progress = values[8].parse::<u64>().map_err(|e| e.to_string())?;
542    stats.time_io_ms = values[9].parse::<u64>().map_err(|e| e.to_string())?;
543    stats.time_io_weighted_ms = values[10].parse::<u64>().map_err(|e| e.to_string())?;
544    if num_elements > 11 {
545        // Kernel 4.18+ appends four more fields for discard
546        stats.discards_completed = values[11].parse::<u64>().map_err(|e| e.to_string())?;
547        stats.discards_merged = values[12].parse::<u64>().map_err(|e| e.to_string())?;
548        stats.sectors_discarded = values[13].parse::<u64>().map_err(|e| e.to_string())?;
549        stats.time_discarding = values[14].parse::<u64>().map_err(|e| e.to_string())?;
550    }
551    if num_elements > 15 {
552        // Kernel 5.5+ appends two more fields for flush requests
553        stats.flushes_completed = values[15].parse::<u64>().map_err(|e| e.to_string())?;
554        stats.time_flushing = values[16].parse::<u64>().map_err(|e| e.to_string())?;
555    }
556
557    Ok(stats)
558}
559
560pub struct SystemMonitorStatsReportConfig {
561    pub report_os_memory_stats: bool,
562    pub report_os_network_stats: bool,
563    pub xdp_network_config_report: Option<XdpNetworkConfigReport>,
564    pub report_os_cpu_stats: bool,
565    pub report_os_disk_stats: bool,
566}
567
568#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
569enum InterestingLimit {
570    Recommend(i64),
571    QueryOnly,
572}
573
574#[cfg(target_os = "linux")]
575const INTERESTING_LIMITS: &[(&str, InterestingLimit)] = &[
576    ("net.core.rmem_max", InterestingLimit::Recommend(134217728)),
577    ("net.core.wmem_max", InterestingLimit::Recommend(134217728)),
578    ("vm.max_map_count", InterestingLimit::Recommend(1000000)),
579    ("net.core.optmem_max", InterestingLimit::QueryOnly),
580    ("net.core.netdev_max_backlog", InterestingLimit::QueryOnly),
581];
582
583impl SystemMonitorService {
584    pub fn new(exit: Arc<AtomicBool>, config: SystemMonitorStatsReportConfig) -> Self {
585        info!("Starting SystemMonitorService");
586        let thread_hdl = Builder::new()
587            .name("solSystemMonitr".to_string())
588            .spawn(move || {
589                Self::run(exit, config);
590            })
591            .unwrap();
592
593        Self { thread_hdl }
594    }
595
596    #[cfg(target_os = "linux")]
597    fn linux_get_current_network_limits() -> Vec<(&'static str, &'static InterestingLimit, i64)> {
598        use sysctl::Sysctl;
599
600        fn sysctl_read(name: &str) -> Result<String, sysctl::SysctlError> {
601            let ctl = sysctl::Ctl::new(name)?;
602            let val = ctl.value_string()?;
603            Ok(val)
604        }
605
606        fn normalize_err<E: std::fmt::Display>(key: &str, error: E) -> String {
607            format!("Failed to query value for {key}: {error}")
608        }
609        INTERESTING_LIMITS
610            .iter()
611            .map(|(key, interesting_limit)| {
612                let current_value = sysctl_read(key)
613                    .map_err(|e| normalize_err(key, e))
614                    .and_then(|val| val.parse::<i64>().map_err(|e| normalize_err(key, e)))
615                    .unwrap_or_else(|e| {
616                        error!("{e}");
617                        -1
618                    });
619                (*key, interesting_limit, current_value)
620            })
621            .collect::<Vec<_>>()
622    }
623
624    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
625    fn linux_report_network_limits(
626        current_limits: &[(&'static str, &'static InterestingLimit, i64)],
627    ) -> bool {
628        current_limits
629            .iter()
630            .all(|(key, interesting_limit, current_value)| {
631                datapoint_warn!("os-config", (key, *current_value, i64));
632                match interesting_limit {
633                    InterestingLimit::Recommend(recommended_value)
634                        if current_value < recommended_value =>
635                    {
636                        warn!(
637                            "  {key}: recommended={recommended_value}, current={current_value} \
638                             too small"
639                        );
640                        false
641                    }
642                    InterestingLimit::Recommend(recommended_value) => {
643                        info!("  {key}: recommended={recommended_value} current={current_value}");
644                        true
645                    }
646                    InterestingLimit::QueryOnly => {
647                        info!("  {key}: report-only --  current={current_value}");
648                        true
649                    }
650                }
651            })
652    }
653
654    #[cfg(not(target_os = "linux"))]
655    pub fn check_os_network_limits() -> bool {
656        datapoint_info!("os-config", ("platform", platform_id(), String));
657        true
658    }
659
660    #[cfg(target_os = "linux")]
661    pub fn check_os_network_limits() -> bool {
662        datapoint_info!("os-config", ("platform", platform_id(), String));
663        let current_limits = Self::linux_get_current_network_limits();
664        Self::linux_report_network_limits(&current_limits)
665    }
666
667    #[cfg(target_os = "linux")]
668    fn process_net_stats(net_stats: &mut Option<NetStats>) {
669        match read_net_stats() {
670            Ok(new_stats) => {
671                if let Some(old_stats) = net_stats {
672                    Self::report_net_stats(old_stats, &new_stats);
673                }
674                *net_stats = Some(new_stats);
675            }
676            Err(e) => warn!("read_net_stats: {e}"),
677        }
678    }
679
680    #[cfg(not(target_os = "linux"))]
681    fn process_net_stats(_net_stats: &mut Option<NetStats>) {}
682
683    #[cfg(target_os = "linux")]
684    fn report_net_stats(old_stats: &NetStats, new_stats: &NetStats) {
685        datapoint_info!(
686            "net-stats-validator",
687            (
688                "in_datagrams_delta",
689                new_stats.udp_stats.in_datagrams - old_stats.udp_stats.in_datagrams,
690                i64
691            ),
692            (
693                "no_ports_delta",
694                new_stats.udp_stats.no_ports - old_stats.udp_stats.no_ports,
695                i64
696            ),
697            (
698                "in_errors_delta",
699                new_stats.udp_stats.in_errors - old_stats.udp_stats.in_errors,
700                i64
701            ),
702            (
703                "out_datagrams_delta",
704                new_stats.udp_stats.out_datagrams - old_stats.udp_stats.out_datagrams,
705                i64
706            ),
707            (
708                "rcvbuf_errors_delta",
709                new_stats.udp_stats.rcvbuf_errors - old_stats.udp_stats.rcvbuf_errors,
710                i64
711            ),
712            (
713                "sndbuf_errors_delta",
714                new_stats.udp_stats.sndbuf_errors - old_stats.udp_stats.sndbuf_errors,
715                i64
716            ),
717            (
718                "in_csum_errors_delta",
719                new_stats.udp_stats.in_csum_errors - old_stats.udp_stats.in_csum_errors,
720                i64
721            ),
722            (
723                "ignored_multi_delta",
724                new_stats.udp_stats.ignored_multi - old_stats.udp_stats.ignored_multi,
725                i64
726            ),
727            ("in_errors", new_stats.udp_stats.in_errors, i64),
728            ("rcvbuf_errors", new_stats.udp_stats.rcvbuf_errors, i64),
729            ("sndbuf_errors", new_stats.udp_stats.sndbuf_errors, i64),
730            (
731                "rx_bytes_delta",
732                new_stats
733                    .net_dev_stats
734                    .rx_bytes
735                    .saturating_sub(old_stats.net_dev_stats.rx_bytes),
736                i64
737            ),
738            (
739                "rx_packets_delta",
740                new_stats
741                    .net_dev_stats
742                    .rx_packets
743                    .saturating_sub(old_stats.net_dev_stats.rx_packets),
744                i64
745            ),
746            (
747                "rx_errs_delta",
748                new_stats
749                    .net_dev_stats
750                    .rx_errs
751                    .saturating_sub(old_stats.net_dev_stats.rx_errs),
752                i64
753            ),
754            (
755                "rx_drops_delta",
756                new_stats
757                    .net_dev_stats
758                    .rx_drops
759                    .saturating_sub(old_stats.net_dev_stats.rx_drops),
760                i64
761            ),
762            (
763                "rx_fifo_delta",
764                new_stats
765                    .net_dev_stats
766                    .rx_fifo
767                    .saturating_sub(old_stats.net_dev_stats.rx_fifo),
768                i64
769            ),
770            (
771                "rx_frame_delta",
772                new_stats
773                    .net_dev_stats
774                    .rx_frame
775                    .saturating_sub(old_stats.net_dev_stats.rx_frame),
776                i64
777            ),
778            (
779                "tx_bytes_delta",
780                new_stats
781                    .net_dev_stats
782                    .tx_bytes
783                    .saturating_sub(old_stats.net_dev_stats.tx_bytes),
784                i64
785            ),
786            (
787                "tx_packets_delta",
788                new_stats
789                    .net_dev_stats
790                    .tx_packets
791                    .saturating_sub(old_stats.net_dev_stats.tx_packets),
792                i64
793            ),
794            (
795                "tx_errs_delta",
796                new_stats
797                    .net_dev_stats
798                    .tx_errs
799                    .saturating_sub(old_stats.net_dev_stats.tx_errs),
800                i64
801            ),
802            (
803                "tx_drops_delta",
804                new_stats
805                    .net_dev_stats
806                    .tx_drops
807                    .saturating_sub(old_stats.net_dev_stats.tx_drops),
808                i64
809            ),
810            (
811                "tx_fifo_delta",
812                new_stats
813                    .net_dev_stats
814                    .tx_fifo
815                    .saturating_sub(old_stats.net_dev_stats.tx_fifo),
816                i64
817            ),
818            (
819                "tx_colls_delta",
820                new_stats
821                    .net_dev_stats
822                    .tx_colls
823                    .saturating_sub(old_stats.net_dev_stats.tx_colls),
824                i64
825            ),
826        );
827    }
828
829    fn calc_percent(numerator: u64, denom: u64) -> f64 {
830        if denom == 0 {
831            0.0
832        } else {
833            (numerator as f64 / denom as f64) * 100.0
834        }
835    }
836
837    fn report_mem_stats() {
838        // get mem info (in kb)
839        if let Ok(info) = sys_info::mem_info() {
840            const KB: u64 = 1_024;
841            datapoint_info!(
842                "memory-stats",
843                ("total", info.total * KB, i64),
844                ("swap_total", info.swap_total * KB, i64),
845                ("buffers_bytes", info.buffers * KB, i64),
846                ("cached_bytes", info.cached * KB, i64),
847                (
848                    "free_percent",
849                    Self::calc_percent(info.free, info.total),
850                    f64
851                ),
852                (
853                    "used_bytes",
854                    info.total.saturating_sub(info.avail) * KB,
855                    i64
856                ),
857                (
858                    "avail_percent",
859                    Self::calc_percent(info.avail, info.total),
860                    f64
861                ),
862                (
863                    "buffers_percent",
864                    Self::calc_percent(info.buffers, info.total),
865                    f64
866                ),
867                (
868                    "cached_percent",
869                    Self::calc_percent(info.cached, info.total),
870                    f64
871                ),
872                (
873                    "swap_free_percent",
874                    Self::calc_percent(info.swap_free, info.swap_total),
875                    f64
876                ),
877            )
878        }
879    }
880
881    #[cfg(not(any(target_env = "msvc", target_os = "freebsd")))]
882    fn report_jemalloc_stats() {
883        use jemalloc_ctl::{epoch, stats};
884        // Advance the epoch so jemalloc refreshes its cached stat values.
885        epoch::mib().unwrap().advance().unwrap();
886        let allocated = stats::allocated::mib()
887            .and_then(|m| m.read())
888            .expect("Jemalloc stats is compiled in");
889        let active = stats::active::mib()
890            .and_then(|m| m.read())
891            .expect("Jemalloc stats is compiled in");
892        let resident = stats::resident::mib()
893            .and_then(|m| m.read())
894            .expect("Jemalloc stats is compiled in");
895        let retained = stats::retained::mib()
896            .and_then(|m| m.read())
897            .expect("Jemalloc stats is compiled in");
898        datapoint_info!(
899            "jemalloc_stats",
900            ("allocated_bytes", allocated, i64),
901            ("active_bytes", active, i64),
902            ("resident_bytes", resident, i64),
903            ("retained_bytes", retained, i64),
904            // Freed memory jemalloc is holding but hasn't returned to the OS yet.
905            ("dirty_bytes", resident.saturating_sub(active), i64),
906        );
907    }
908
909    fn cpu_info() -> Result<CpuInfo, Error> {
910        let cpu_num = sys_info::cpu_num()?;
911        let cpu_freq_mhz = sys_info::cpu_speed()?;
912        let load_avg = sys_info::loadavg()?;
913        let num_threads = sys_info::proc_total()?;
914
915        Ok(CpuInfo {
916            cpu_num,
917            cpu_freq_mhz,
918            load_avg,
919            num_threads,
920        })
921    }
922
923    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
924    fn report_cpuid_values() {
925        const CPUID_MANUFACTURER_EBX_INTEL: u32 = 0x756e6547;
926        const CPUID_MANUFACTURER_EDX_INTEL: u32 = 0x49656e69;
927        const CPUID_MANUFACTURER_ECX_INTEL: u32 = 0x6c65746e;
928        const CPUID_MANUFACTURER_EBX_AMD: u32 = 0x68747541;
929        const CPUID_MANUFACTURER_EDX_AMD: u32 = 0x69746e65;
930        const CPUID_MANUFACTURER_ECX_AMD: u32 = 0x444d4163;
931
932        let cpuid_mfr = __cpuid(0);
933        let cpuid_empty = CpuidResult {
934            eax: 0,
935            ebx: 0,
936            ecx: 0,
937            edx: 0,
938        };
939
940        let max_leaf = match CpuidParamValue::try_from(std::cmp::min(
941            cpuid_mfr.eax,
942            CPUID_PARAM_MAX_SUPPORTED_VALUE,
943        )) {
944            Ok(val) => val,
945            Err(_err) => CpuidParamValue::Manufacturer,
946        };
947
948        let mfr_id = if cpuid_mfr.ebx == CPUID_MANUFACTURER_EBX_INTEL
949            && cpuid_mfr.edx == CPUID_MANUFACTURER_EDX_INTEL
950            && cpuid_mfr.ecx == CPUID_MANUFACTURER_ECX_INTEL
951        {
952            CpuManufacturer::Intel // GenuineIntel
953        } else if cpuid_mfr.ebx == CPUID_MANUFACTURER_EBX_AMD
954            && cpuid_mfr.edx == CPUID_MANUFACTURER_EDX_AMD
955            && cpuid_mfr.ecx == CPUID_MANUFACTURER_ECX_AMD
956        {
957            CpuManufacturer::Amd // AuthenticAMD
958        } else {
959            CpuManufacturer::Other // anything else
960        };
961
962        let cpuid_processor = if CpuidParamValue::Processor <= max_leaf {
963            __cpuid(CpuidParamValue::Processor.into())
964        } else {
965            cpuid_empty
966        };
967        let cpuid_cache = if CpuidParamValue::Cache <= max_leaf {
968            __cpuid(CpuidParamValue::Cache.into())
969        } else {
970            cpuid_empty
971        };
972        let cpuid_topology = if CpuidParamValue::Topology <= max_leaf {
973            __cpuid(CpuidParamValue::Topology.into())
974        } else {
975            cpuid_empty
976        };
977        let cpuid_extended_0 = if CpuidParamValue::Extended <= max_leaf {
978            __cpuid_count(CpuidParamValue::Extended.into(), 0)
979        } else {
980            cpuid_empty
981        };
982        let cpuid_extended_1 = if CpuidParamValue::Extended <= max_leaf {
983            if 1 <= __get_cpuid_max(CpuidParamValue::Extended.into()).1 {
984                __cpuid_count(CpuidParamValue::Extended.into(), 1)
985            } else {
986                cpuid_empty
987            }
988        } else {
989            cpuid_empty
990        };
991
992        datapoint_info!(
993            "cpuid-values",
994            ("manufacturer_id", i64::from(mfr_id), i64),
995            ("cpuid_processor_eax", i64::from(cpuid_processor.eax), i64),
996            ("cpuid_processor_ebx", i64::from(cpuid_processor.ebx), i64),
997            ("cpuid_processor_ecx", i64::from(cpuid_processor.ecx), i64),
998            ("cpuid_processor_edx", i64::from(cpuid_processor.edx), i64),
999            ("cpuid_cache_eax", i64::from(cpuid_cache.eax), i64),
1000            ("cpuid_cache_ebx", i64::from(cpuid_cache.ebx), i64),
1001            ("cpuid_cache_ecx", i64::from(cpuid_cache.ecx), i64),
1002            ("cpuid_cache_edx", i64::from(cpuid_cache.edx), i64),
1003            ("cpuid_topology_eax", i64::from(cpuid_topology.eax), i64),
1004            ("cpuid_topology_ebx", i64::from(cpuid_topology.ebx), i64),
1005            ("cpuid_topology_ecx", i64::from(cpuid_topology.ecx), i64),
1006            ("cpuid_topology_edx", i64::from(cpuid_topology.edx), i64),
1007            ("cpuid_extended_0_ebx", i64::from(cpuid_extended_0.ebx), i64),
1008            ("cpuid_extended_0_ecx", i64::from(cpuid_extended_0.ecx), i64),
1009            ("cpuid_extended_0_edx", i64::from(cpuid_extended_0.edx), i64),
1010            ("cpuid_extended_1_eax", i64::from(cpuid_extended_1.eax), i64),
1011        );
1012    }
1013
1014    fn report_cpu_stats() {
1015        if let Ok(info) = Self::cpu_info() {
1016            datapoint_info!(
1017                "cpu-stats",
1018                ("cpu_num", info.cpu_num as i64, i64),
1019                ("cpu0_freq_mhz", info.cpu_freq_mhz as i64, i64),
1020                ("average_load_one_minute", info.load_avg.one, f64),
1021                ("average_load_five_minutes", info.load_avg.five, f64),
1022                ("average_load_fifteen_minutes", info.load_avg.fifteen, f64),
1023                ("total_num_threads", info.num_threads as i64, i64),
1024            )
1025        }
1026    }
1027
1028    #[cfg(target_os = "linux")]
1029    fn process_disk_stats(disk_stats: &mut Option<DiskStats>) {
1030        match read_disk_stats() {
1031            Ok(new_stats) => {
1032                if let Some(old_stats) = disk_stats {
1033                    Self::report_disk_stats(old_stats, &new_stats);
1034                }
1035                *disk_stats = Some(new_stats);
1036            }
1037            Err(e) => warn!("read_disk_stats: {e}"),
1038        }
1039    }
1040
1041    #[cfg(not(target_os = "linux"))]
1042    fn process_disk_stats(_disk_stats: &mut Option<DiskStats>) {}
1043
1044    #[cfg(target_os = "linux")]
1045    fn report_disk_stats(old_stats: &DiskStats, new_stats: &DiskStats) {
1046        datapoint_info!(
1047            "disk-stats",
1048            (
1049                "reads_completed",
1050                new_stats
1051                    .reads_completed
1052                    .saturating_sub(old_stats.reads_completed),
1053                i64
1054            ),
1055            (
1056                "reads_merged",
1057                new_stats
1058                    .reads_merged
1059                    .saturating_sub(old_stats.reads_merged),
1060                i64
1061            ),
1062            (
1063                "sectors_read",
1064                new_stats
1065                    .sectors_read
1066                    .saturating_sub(old_stats.sectors_read),
1067                i64
1068            ),
1069            (
1070                "time_reading_ms",
1071                new_stats
1072                    .time_reading_ms
1073                    .saturating_sub(old_stats.time_reading_ms),
1074                i64
1075            ),
1076            (
1077                "writes_completed",
1078                new_stats
1079                    .writes_completed
1080                    .saturating_sub(old_stats.writes_completed),
1081                i64
1082            ),
1083            (
1084                "writes_merged",
1085                new_stats
1086                    .writes_merged
1087                    .saturating_sub(old_stats.writes_merged),
1088                i64
1089            ),
1090            (
1091                "sectors_written",
1092                new_stats
1093                    .sectors_written
1094                    .saturating_sub(old_stats.sectors_written),
1095                i64
1096            ),
1097            (
1098                "time_writing_ms",
1099                new_stats
1100                    .time_writing_ms
1101                    .saturating_sub(old_stats.time_writing_ms),
1102                i64
1103            ),
1104            ("io_in_progress", new_stats.io_in_progress, i64),
1105            (
1106                "time_io_ms",
1107                new_stats.time_io_ms.saturating_sub(old_stats.time_io_ms),
1108                i64
1109            ),
1110            (
1111                "time_io_weighted_ms",
1112                new_stats
1113                    .time_io_weighted_ms
1114                    .saturating_sub(old_stats.time_io_weighted_ms),
1115                i64
1116            ),
1117            (
1118                "discards_completed",
1119                new_stats
1120                    .discards_completed
1121                    .saturating_sub(old_stats.discards_completed),
1122                i64
1123            ),
1124            (
1125                "discards_merged",
1126                new_stats
1127                    .discards_merged
1128                    .saturating_sub(old_stats.discards_merged),
1129                i64
1130            ),
1131            (
1132                "sectors_discarded",
1133                new_stats
1134                    .sectors_discarded
1135                    .saturating_sub(old_stats.sectors_discarded),
1136                i64
1137            ),
1138            (
1139                "time_discarding",
1140                new_stats
1141                    .time_discarding
1142                    .saturating_sub(old_stats.time_discarding),
1143                i64
1144            ),
1145            (
1146                "flushes_completed",
1147                new_stats
1148                    .flushes_completed
1149                    .saturating_sub(old_stats.flushes_completed),
1150                i64
1151            ),
1152            (
1153                "time_flushing",
1154                new_stats
1155                    .time_flushing
1156                    .saturating_sub(old_stats.time_flushing),
1157                i64
1158            ),
1159            ("num_disks", new_stats.num_disks, i64),
1160        )
1161    }
1162
1163    pub fn run(exit: Arc<AtomicBool>, config: SystemMonitorStatsReportConfig) {
1164        let mut udp_stats = None;
1165        let mut disk_stats = None;
1166        let network_limits_timer = AtomicInterval::default();
1167        let xdp_network_config_timer = AtomicInterval::default();
1168        let udp_timer = AtomicInterval::default();
1169        let mem_timer = AtomicInterval::default();
1170        let cpu_timer = AtomicInterval::default();
1171        let cpuid_timer = AtomicInterval::default();
1172        let disk_timer = AtomicInterval::default();
1173        let mut xdp_network_config_metrics = None;
1174
1175        loop {
1176            if exit.load(Ordering::Relaxed) {
1177                break;
1178            }
1179            if config.report_os_network_stats {
1180                if network_limits_timer.should_update(SAMPLE_INTERVAL_OS_NETWORK_LIMITS_MS) {
1181                    Self::check_os_network_limits();
1182                }
1183                if udp_timer.should_update(SAMPLE_INTERVAL_UDP_MS) {
1184                    Self::process_net_stats(&mut udp_stats);
1185                }
1186            }
1187            if let Some(xdp_network_config_report) = &config.xdp_network_config_report
1188                && xdp_network_config_timer
1189                    .should_update_ext(SAMPLE_INTERVAL_XDP_NETWORK_CONFIG_MS, false)
1190            {
1191                let metrics = xdp_network_config_metrics.get_or_insert_with(|| {
1192                    Self::load_xdp_network_config_metrics(xdp_network_config_report)
1193                });
1194                Self::report_xdp_network_config(xdp_network_config_report, metrics);
1195            }
1196            if config.report_os_memory_stats && mem_timer.should_update(SAMPLE_INTERVAL_MEM_MS) {
1197                Self::report_mem_stats();
1198                #[cfg(not(any(target_env = "msvc", target_os = "freebsd")))]
1199                Self::report_jemalloc_stats();
1200            }
1201            if config.report_os_cpu_stats {
1202                if cpu_timer.should_update(SAMPLE_INTERVAL_CPU_MS) {
1203                    Self::report_cpu_stats();
1204                }
1205                if cpuid_timer.should_update(SAMPLE_INTERVAL_CPU_ID_MS) {
1206                    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
1207                    Self::report_cpuid_values();
1208                }
1209            }
1210            if config.report_os_disk_stats && disk_timer.should_update(SAMPLE_INTERVAL_DISK_MS) {
1211                Self::process_disk_stats(&mut disk_stats);
1212            }
1213            sleep(SLEEP_INTERVAL);
1214        }
1215    }
1216
1217    #[cfg(not(target_os = "linux"))]
1218    fn report_xdp_network_config(
1219        _config: &XdpNetworkConfigReport,
1220        _metrics: &XdpNetworkConfigMetrics,
1221    ) {
1222    }
1223
1224    #[cfg(target_os = "linux")]
1225    fn load_xdp_network_config_metrics(config: &XdpNetworkConfigReport) -> XdpNetworkConfigMetrics {
1226        let Ok(device) = NetworkDevice::new(&config.interface) else {
1227            warn!(
1228                "failed to get xdp network config device for interface {}",
1229                config.interface
1230            );
1231            return XdpNetworkConfigMetrics {
1232                kernel_version: Self::kernel_version(),
1233                driver: "unknown".to_string(),
1234                vendor: "unknown".to_string(),
1235                model: "unknown".to_string(),
1236            };
1237        };
1238        let driver = device.driver().unwrap_or_else(|err| {
1239            warn!(
1240                "failed to get xdp network config driver for interface {}: {err}",
1241                config.interface
1242            );
1243            "unknown".to_string()
1244        });
1245        let vendor_id = read_network_device_sysfs_value(&config.interface, "vendor")
1246            .unwrap_or_else(|err| {
1247                warn!(
1248                    "failed to get xdp network config vendor id for interface {}: {err}",
1249                    config.interface
1250                );
1251                "unknown".to_string()
1252            });
1253        let model_id =
1254            read_network_device_sysfs_value(&config.interface, "device").unwrap_or_else(|err| {
1255                warn!(
1256                    "failed to get xdp network config model id for interface {}: {err}",
1257                    config.interface
1258                );
1259                "unknown".to_string()
1260            });
1261        let (vendor, model) =
1262            match try_resolve_network_device_pci_names(&config.interface, &vendor_id, &model_id) {
1263                Ok((vendor, model)) => (vendor, model),
1264                Err(err) => {
1265                    warn!(
1266                        "failed to resolve xdp network config PCI names for interface {}: {}",
1267                        config.interface, err
1268                    );
1269                    // Fall back to reporting raw PCI IDs if name resolution fails
1270                    let fallback_vendor =
1271                        normalize_pci_id(&vendor_id).unwrap_or_else(|| vendor_id.clone());
1272                    let fallback_model =
1273                        normalize_pci_id(&model_id).unwrap_or_else(|| model_id.clone());
1274                    (fallback_vendor, fallback_model)
1275                }
1276            };
1277
1278        XdpNetworkConfigMetrics {
1279            kernel_version: Self::kernel_version(),
1280            driver,
1281            vendor,
1282            model,
1283        }
1284    }
1285
1286    #[cfg(not(target_os = "linux"))]
1287    fn load_xdp_network_config_metrics(
1288        _config: &XdpNetworkConfigReport,
1289    ) -> XdpNetworkConfigMetrics {
1290        XdpNetworkConfigMetrics {
1291            kernel_version: "unknown".to_string(),
1292            driver: "unknown".to_string(),
1293            vendor: "unknown".to_string(),
1294            model: "unknown".to_string(),
1295        }
1296    }
1297
1298    #[cfg(target_os = "linux")]
1299    fn report_xdp_network_config(
1300        config: &XdpNetworkConfigReport,
1301        metrics: &XdpNetworkConfigMetrics,
1302    ) {
1303        solana_metrics::datapoint_info!(
1304            "xdp-network-config",
1305            "driver" => metrics.driver.clone(),
1306            "zero_copy" => config.zero_copy.to_string(),
1307            ("kernel_version", metrics.kernel_version.clone(), String),
1308            ("vendor", metrics.vendor.clone(), String),
1309            ("model", metrics.model.clone(), String),
1310        );
1311    }
1312
1313    #[cfg(target_os = "linux")]
1314    fn kernel_version() -> String {
1315        let mut utsname = unsafe { std::mem::zeroed::<libc::utsname>() };
1316        if unsafe { libc::uname(&mut utsname) } != 0 {
1317            return format!("unknown: {}", std::io::Error::last_os_error());
1318        }
1319
1320        unsafe { std::ffi::CStr::from_ptr(utsname.release.as_ptr()) }
1321            .to_string_lossy()
1322            .into_owned()
1323    }
1324
1325    pub fn join(self) -> thread::Result<()> {
1326        self.thread_hdl.join()
1327    }
1328}
1329
1330#[cfg(test)]
1331mod tests {
1332    use super::*;
1333
1334    #[test]
1335    fn test_parse_pci_database_device_names() {
1336        let pci_ids = "\
13378086  Intel Corporation
1338\t1593  Ethernet Controller E810-C for QSFP
1339\t\t8086 0001  Ethernet Network Adapter E810-C-Q1
134010ec  Realtek Semiconductor Co., Ltd.
1341\t8136  RTL810xE PCI Express Fast Ethernet controller
1342C 02  Network controller
1343\t00  Ethernet controller
1344";
1345
1346        assert_eq!(
1347            parse_pci_database_device_names(pci_ids, "8086", "1593"),
1348            Some((
1349                "Intel Corporation".to_string(),
1350                "Ethernet Controller E810-C for QSFP".to_string()
1351            ))
1352        );
1353        assert_eq!(
1354            parse_pci_database_device_names(pci_ids, "10EC", "8136"),
1355            Some((
1356                "Realtek Semiconductor Co., Ltd.".to_string(),
1357                "RTL810xE PCI Express Fast Ethernet controller".to_string()
1358            ))
1359        );
1360        assert_eq!(
1361            parse_pci_database_device_names(pci_ids, "8086", "0001"),
1362            None
1363        );
1364        assert_eq!(
1365            parse_pci_database_device_names(pci_ids, "0000", "1593"),
1366            None
1367        );
1368        assert_eq!(parse_pci_database_device_names(pci_ids, "C", "02"), None);
1369    }
1370
1371    #[test]
1372    fn test_parse_udp_stats() {
1373        const MOCK_SNMP: &[u8] =
1374b"Ip: Forwarding DefaultTTL InReceives InHdrErrors InAddrErrors ForwDatagrams InUnknownProtos InDiscards InDelivers OutRequests OutDiscards OutNoRoutes ReasmTimeout ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates
1375Ip: 1 64 357 0 2 0 0 0 355 315 0 6 0 0 0 0 0 0 0
1376Icmp: InMsgs InErrors InCsumErrors InDestUnreachs InTimeExcds InParmProbs InSrcQuenchs InRedirects InEchos InEchoReps InTimestamps InTimestampReps InAddrMasks InAddrMaskReps OutMsgs OutErrors OutDestUnreachs OutTimeExcds OutParmProbs OutSrcQuenchs OutRedirects OutEchos OutEchoReps OutTimestamps OutTimestampReps OutAddrMasks OutAddrMaskReps
1377Icmp: 3 0 0 3 0 0 0 0 0 0 0 0 0 0 7 0 7 0 0 0 0 0 0 0 0 0 0
1378IcmpMsg: InType3 OutType3
1379IcmpMsg: 3 7
1380Tcp: RtoAlgorithm RtoMin RtoMax MaxConn ActiveOpens PassiveOpens AttemptFails EstabResets CurrEstab InSegs OutSegs RetransSegs InErrs OutRsts InCsumErrors
1381Tcp: 1 200 120000 -1 29 1 0 0 5 318 279 0 0 4 0
1382Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors InCsumErrors IgnoredMulti
1383Udp: 27 7 0 30 0 0 0 0
1384UdpLite: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors InCsumErrors IgnoredMulti
1385UdpLite: 0 0 0 0 0 0 0 0" as &[u8];
1386        const UNEXPECTED_DATA: &[u8] = b"unexpected data" as &[u8];
1387
1388        let mut mock_snmp = MOCK_SNMP;
1389        let stats = parse_udp_stats(&mut mock_snmp).unwrap();
1390        assert_eq!(stats.out_datagrams, 30);
1391        assert_eq!(stats.no_ports, 7);
1392
1393        mock_snmp = UNEXPECTED_DATA;
1394        let stats = parse_udp_stats(&mut mock_snmp);
1395        assert!(stats.is_err());
1396    }
1397
1398    #[test]
1399    fn test_parse_net_dev_stats() {
1400        const MOCK_DEV: &[u8] =
1401b"Inter-|   Receive                                                |  Transmit
1402face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
1403lo: 50     1    0    0    0     0          0         0 100 2    1    0    0     0       0          0
1404eno1: 100     1    0    0    0     0          0         0 200 3    2    0    0     0       0          0
1405ens4: 400     4    0    1    0     0          0         0 250 5    0    0    0     0       0          0" as &[u8];
1406        const UNEXPECTED_DATA: &[u8] = b"un
1407expected
1408data" as &[u8];
1409
1410        let mut mock_dev = MOCK_DEV;
1411        let stats = parse_net_dev_stats(&mut mock_dev).unwrap();
1412        assert_eq!(stats.rx_bytes, 500);
1413        assert_eq!(stats.rx_packets, 5);
1414        assert_eq!(stats.rx_errs, 0);
1415        assert_eq!(stats.rx_drops, 1);
1416        assert_eq!(stats.tx_bytes, 450);
1417        assert_eq!(stats.tx_packets, 8);
1418        assert_eq!(stats.tx_errs, 2);
1419        assert_eq!(stats.tx_drops, 0);
1420
1421        let mut mock_dev = UNEXPECTED_DATA;
1422        let stats = parse_net_dev_stats(&mut mock_dev);
1423        assert!(stats.is_err());
1424    }
1425
1426    #[test]
1427    fn test_parse_disk_stats() {
1428        const MOCK_DISK_11: &[u8] =
1429b" 2095701   479815 122620302  1904439 43496218 26953623 3935324729 283313376        0  6101780 285220738" as &[u8];
1430        // Matches kernel 4.18+ format
1431        const MOCK_DISK_15: &[u8] =
1432b" 2095701   479815 122620302  1904439 43496218 26953623 3935324729 283313376        0  6101780 285220738        0        0        0        0" as &[u8];
1433        // Matches kernel 5.5+ format
1434        const MOCK_DISK_17: &[u8] =
1435b" 2095701   479815 122620302  1904439 43496218 26953623 3935324729 283313376        0  6101780 285220738        0        0        0        0    70715     2922" as &[u8];
1436        const UNEXPECTED_DATA_1: &[u8] =
1437b" 2095701   479815 122620302  1904439 43496218 26953623 3935324729 283313376        0  6101780 285220738        0        0        0        0    70715" as &[u8];
1438
1439        const UNEXPECTED_DATA_2: &[u8] = b"un
1440ex
1441pec
1442ted
1443data" as &[u8];
1444
1445        let mut mock_disk = MOCK_DISK_11;
1446        let stats = parse_disk_stats(&mut mock_disk).unwrap();
1447        assert_eq!(stats.reads_completed, 2095701);
1448        assert_eq!(stats.time_io_weighted_ms, 285220738);
1449
1450        let mut mock_disk = MOCK_DISK_15;
1451        let stats = parse_disk_stats(&mut mock_disk).unwrap();
1452        assert_eq!(stats.reads_completed, 2095701);
1453        assert_eq!(stats.time_discarding, 0);
1454
1455        let mut mock_disk = MOCK_DISK_17;
1456        let stats = parse_disk_stats(&mut mock_disk).unwrap();
1457        assert_eq!(stats.reads_completed, 2095701);
1458        assert_eq!(stats.time_flushing, 2922);
1459
1460        let mut mock_disk = UNEXPECTED_DATA_1;
1461        let stats = parse_disk_stats(&mut mock_disk);
1462        assert!(stats.is_err());
1463
1464        let mut mock_disk = UNEXPECTED_DATA_2;
1465        let stats = parse_disk_stats(&mut mock_disk);
1466        assert!(stats.is_err());
1467    }
1468
1469    #[test]
1470    fn test_calc_percent() {
1471        assert!(SystemMonitorService::calc_percent(99, 100) < 100.0);
1472        let one_tb_as_kb = (1u64 << 40) >> 10;
1473        assert!(SystemMonitorService::calc_percent(one_tb_as_kb - 1, one_tb_as_kb) < 100.0);
1474    }
1475}