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))]
83struct NetDevStats {
85 rx_bytes: u64,
87 rx_packets: u64,
89 rx_errs: u64,
91 rx_drops: u64,
93 rx_fifo: u64,
95 rx_frame: u64,
97 rx_compressed: u64,
99 rx_multicast: u64,
101 tx_bytes: u64,
103 tx_packets: u64,
105 tx_errs: u64,
107 tx_drops: u64,
109 tx_fifo: u64,
111 tx_colls: u64,
113 tx_carrier: u64,
115 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)]
144enum 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))]
160struct 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 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 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 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 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 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 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 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(¤t_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 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 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 ("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 } 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 } else {
959 CpuManufacturer::Other };
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 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 const MOCK_DISK_15: &[u8] =
1432b" 2095701 479815 122620302 1904439 43496218 26953623 3935324729 283313376 0 6101780 285220738 0 0 0 0" as &[u8];
1433 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}