diff --git a/fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.stat b/fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.stat deleted file mode 100644 index 065a839..0000000 --- a/fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.stat +++ /dev/null @@ -1,2 +0,0 @@ -user 404 -system 749 diff --git a/fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.usage b/fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.usage deleted file mode 100644 index cfe8384..0000000 --- a/fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.usage +++ /dev/null @@ -1 +0,0 @@ -13953750329 diff --git a/fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.stat b/fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.stat new file mode 100644 index 0000000..da15820 --- /dev/null +++ b/fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.stat @@ -0,0 +1,2 @@ +user 14934 +system 98 diff --git a/fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.usage b/fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.usage new file mode 100644 index 0000000..56ca04f --- /dev/null +++ b/fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.usage @@ -0,0 +1 @@ +152657213021 diff --git a/fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.stat b/fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.stat new file mode 100644 index 0000000..5be1b68 --- /dev/null +++ b/fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.stat @@ -0,0 +1,2 @@ +user 17783 +system 121 diff --git a/fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.usage b/fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.usage new file mode 100644 index 0000000..f5d38c2 --- /dev/null +++ b/fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.usage @@ -0,0 +1 @@ +182405617026 diff --git a/src/cpu/cgroup.rs b/src/cpu/cgroup.rs new file mode 100644 index 0000000..caf8c1d --- /dev/null +++ b/src/cpu/cgroup.rs @@ -0,0 +1,351 @@ +use super::super::{Result,calculate_time_difference,time_adjusted}; + +/// Measurement of cpu stats at a certain time +#[derive(Debug,PartialEq)] +pub struct CgroupCpuMeasurement { + pub precise_time_ns: u64, + pub stat: CgroupCpuStat +} + +impl CgroupCpuMeasurement { + pub fn calculate_per_minute(&self, next_measurement: &CgroupCpuMeasurement) -> Result { + let time_difference = calculate_time_difference(self.precise_time_ns, next_measurement.precise_time_ns)?; + + Ok(CgroupCpuStat { + total_usage: time_adjusted("total_usage", next_measurement.stat.total_usage, self.stat.total_usage, time_difference)?, + user: time_adjusted("user", next_measurement.stat.user, self.stat.user, time_difference)?, + system: time_adjusted("system", next_measurement.stat.system, self.stat.system, time_difference)? + }) + } +} + +/// Container CPU stats for a minute +#[derive(Debug,PartialEq)] +pub struct CgroupCpuStat { + pub total_usage: u64, + pub user: u64, + pub system: u64 +} + +impl CgroupCpuStat { + /// Calculate the weight of the various components in percentages + pub fn in_percentages(&self) -> CgroupCpuStatPercentages { + CgroupCpuStatPercentages { + total_usage: self.percentage_of_total(self.total_usage), + user: self.percentage_of_total(self.user), + system: self.percentage_of_total(self.system) + } + } + + fn percentage_of_total(&self, value: u64) -> f32 { + // 60_000_000_000 being the total value. This is 60 seconds expressed in nanoseconds. + (value as f32 / 60_000_000_000.0) * 100.0 + } +} + +/// Cgroup Cpu stats converted to percentages +#[derive(Debug,PartialEq)] +pub struct CgroupCpuStatPercentages { + pub total_usage: f32, + pub user: f32, + pub system: f32 +} + +/// Read the current CPU stats of the container. +#[cfg(target_os = "linux")] +pub fn read() -> Result { + os::read() +} + +#[cfg(target_os = "linux")] +mod os { + use std::path::Path; + use std::io::BufRead; + use time; + use super::super::super::{Result,file_to_buf_reader,parse_u64,path_to_string,read_file_value_as_u64,dir_exists}; + use super::{CgroupCpuMeasurement,CgroupCpuStat}; + use error::ProbeError; + + const CPU_SYS_NUMBER_OF_FIELDS: usize = 2; + + pub fn read() -> Result { + let sys_fs_dir = Path::new("/sys/fs/cgroup/cpuacct/"); + if dir_exists(sys_fs_dir) { + read_and_parse_sys_stat(&sys_fs_dir) + } else { + let message = format!("Directory `{}` not found", sys_fs_dir.to_str().unwrap_or("unknown path")); + Err(ProbeError::UnexpectedContent(message)) + } + } + + pub fn read_and_parse_sys_stat(path: &Path) -> Result { + let time = time::precise_time_ns(); + let reader = file_to_buf_reader(&path.join("cpuacct.stat"))?; + let total_usage = read_file_value_as_u64(&path.join("cpuacct.usage"))?; + + let mut cpu = CgroupCpuStat { + total_usage: total_usage, + user: 0, + system: 0 + }; + + let mut fields_encountered = 0; + for line in reader.lines() { + let line = line.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; + let segments: Vec<&str> = line.split_whitespace().collect(); + let value = parse_u64(&segments[1])?; + fields_encountered += match segments[0] { + "user" => { + cpu.user = value * 10_000_000; + 1 + }, + "system" => { + cpu.system = value * 10_000_000; + 1 + }, + _ => 0 + }; + + if fields_encountered == CPU_SYS_NUMBER_OF_FIELDS { + break + } + } + + if fields_encountered != CPU_SYS_NUMBER_OF_FIELDS { + return Err(ProbeError::UnexpectedContent("Did not encounter all expected fields".to_owned())) + } + let measurement = CgroupCpuMeasurement { + precise_time_ns: time, + stat: cpu + }; + Ok(measurement) + } +} + +#[cfg(test)] +mod test { + use super::{CgroupCpuMeasurement,CgroupCpuStat}; + use super::os::read_and_parse_sys_stat; + use std::path::Path; + use error::ProbeError; + + #[test] + fn test_read() { + assert!(super::read().is_ok()); + } + + #[test] + fn test_read_sys_measurement() { + let measurement = read_and_parse_sys_stat(&Path::new("fixtures/linux/sys/fs/cgroup/cpuacct_1/")).unwrap(); + let cpu = measurement.stat; + assert_eq!(cpu.total_usage, 152657213021); + assert_eq!(cpu.user, 149340000000); + assert_eq!(cpu.system, 980000000); + } + + #[test] + fn test_read_sys_wrong_path() { + match read_and_parse_sys_stat(&Path::new("bananas")) { + Err(ProbeError::IO(_, _)) => (), + r => panic!("Unexpected result: {:?}", r) + } + } + + #[test] + fn test_read_and_parse_sys_stat_incomplete() { + match read_and_parse_sys_stat(&Path::new("fixtures/linux/sys/fs/cgroup/cpuacct_incomplete/")) { + Err(ProbeError::UnexpectedContent(_)) => (), + r => panic!("Unexpected result: {:?}", r) + } + } + + #[test] + fn test_read_and_parse_sys_stat_garbage() { + let path = Path::new("fixtures/linux/sys/fs/cgroup/cpuacct_garbage/"); + match read_and_parse_sys_stat(&path) { + Err(ProbeError::UnexpectedContent(_)) => (), + r => panic!("Unexpected result: {:?}", r) + } + } + + #[test] + fn test_calculate_per_minute_wrong_times() { + let measurement1 = CgroupCpuMeasurement { + precise_time_ns: 90_000_000_000, + stat: CgroupCpuStat { + total_usage: 0, + user: 0, + system: 0 + } + }; + + let measurement2 = CgroupCpuMeasurement { + precise_time_ns: 60_000_000_000, + stat: CgroupCpuStat { + total_usage: 0, + user: 0, + system: 0 + } + }; + + match measurement1.calculate_per_minute(&measurement2) { + Err(ProbeError::InvalidInput(_)) => (), + r => panic!("Unexpected result: {:?}", r) + } + } + + + #[test] + fn test_cgroup_calculate_per_minute_full_minute() { + let measurement1 = CgroupCpuMeasurement { + precise_time_ns: 60_000_000_000, + stat: CgroupCpuStat { + total_usage: 6380, + user: 1000, + system: 1200 + } + }; + + let measurement2 = CgroupCpuMeasurement { + precise_time_ns: 120_000_000_000, + stat: CgroupCpuStat { + total_usage: 6440, + user: 1006, + system: 1206 + } + }; + + let expected = CgroupCpuStat { + total_usage: 60, + user: 6, + system: 6 + }; + + let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); + + assert_eq!(stat, expected); + } + + #[test] + fn test_calculate_per_minute_partial_minute() { + let measurement1 = CgroupCpuMeasurement { + precise_time_ns: 60_000_000_000, + stat: CgroupCpuStat { + total_usage: 1_000_000_000, + user: 10000_000_000, + system: 12000_000_000 + } + }; + + let measurement2 = CgroupCpuMeasurement { + precise_time_ns: 90_000_000_000, + stat: CgroupCpuStat { + total_usage: 1_500_000_000, + user: 10060_000_000, + system: 12060_000_000 + } + }; + + let expected = CgroupCpuStat { + total_usage: 1_000_000_000, + user: 120_000_000, + system: 120_000_000 + }; + + let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); + + assert_eq!(stat, expected); + } + + #[test] + fn test_calculate_per_minute_values_lower() { + let measurement1 = CgroupCpuMeasurement { + precise_time_ns: 60_000_000_000, + stat: CgroupCpuStat { + total_usage: 63800_000_000, + user: 10000_000_000, + system: 12000_000_000 + } + }; + + let measurement2 = CgroupCpuMeasurement { + precise_time_ns: 90_000_000_000, + stat: CgroupCpuStat { + total_usage: 10400_000_000, + user: 1060_000_000, + system: 1260_000_000 + } + }; + + match measurement1.calculate_per_minute(&measurement2) { + Err(ProbeError::UnexpectedContent(_)) => (), + r => panic!("Unexpected result: {:?}", r) + } + } + + #[test] + fn test_in_percentages() { + let stat = CgroupCpuStat { + total_usage: 24000000000, + user: 16800000000, + system: 1200000000 + }; + + let in_percentages = stat.in_percentages(); + + // Rounding in the floating point calculations can vary, so check if this + // is in the correct range. + assert!(in_percentages.total_usage > 39.9); + assert!(in_percentages.total_usage <= 40.0); + + assert!(in_percentages.user > 27.9); + assert!(in_percentages.user <= 28.0); + + assert!(in_percentages.system > 1.9); + assert!(in_percentages.system <= 2.0); + } + + #[test] + fn test_in_percentages_fractions() { + let stat = CgroupCpuStat { + total_usage: 24000000000, + user: 17100000000, + system: 900000000 + }; + + let in_percentages = stat.in_percentages(); + + // Rounding in the floating point calculations can vary, so check if this + // is in the correct range. + assert!(in_percentages.total_usage > 39.9); + assert!(in_percentages.total_usage <= 40.0); + + assert!(in_percentages.user > 28.4); + assert!(in_percentages.user <= 28.5); + + assert!(in_percentages.system > 1.4); + assert!(in_percentages.system <= 1.5); + } + + #[test] + fn test_in_percentages_integration() { + let mut measurement1 = read_and_parse_sys_stat(&Path::new("fixtures/linux/sys/fs/cgroup/cpuacct_1/")).unwrap(); + measurement1.precise_time_ns = 375953965125920; + let mut measurement2 = read_and_parse_sys_stat(&Path::new("fixtures/linux/sys/fs/cgroup/cpuacct_2/")).unwrap(); + measurement2.precise_time_ns = 376013815302920; + + let stat = measurement1.calculate_per_minute(&measurement2).unwrap(); + let in_percentages = stat.in_percentages(); + + // Rounding in the floating point calculations can vary, so check if this + // is in the correct range. + assert!(in_percentages.total_usage > 49.70); + assert!(in_percentages.total_usage < 49.71); + + assert!(in_percentages.user > 47.60); + assert!(in_percentages.user < 47.61); + + assert!(in_percentages.system > 0.38); + assert!(in_percentages.system < 0.39); + } +} diff --git a/src/cpu/mod.rs b/src/cpu/mod.rs new file mode 100644 index 0000000..1b2a0b7 --- /dev/null +++ b/src/cpu/mod.rs @@ -0,0 +1,2 @@ +pub mod proc; +pub mod cgroup; diff --git a/src/cpu.rs b/src/cpu/proc.rs similarity index 72% rename from src/cpu.rs rename to src/cpu/proc.rs index acba6d4..498721d 100644 --- a/src/cpu.rs +++ b/src/cpu/proc.rs @@ -1,6 +1,4 @@ -use super::{Result,calculate_time_difference}; - -const CPU_SYS_NUMBER_OF_FIELDS: usize = 2; +use super::super::{Result,calculate_time_difference,time_adjusted}; /// Measurement of cpu stats at a certain time #[derive(Debug,PartialEq)] @@ -17,17 +15,17 @@ impl CpuMeasurement { let time_difference = calculate_time_difference(self.precise_time_ns, next_measurement.precise_time_ns)?; Ok(CpuStat { - total: super::time_adjusted("total", next_measurement.stat.total, self.stat.total, time_difference)?, - user: super::time_adjusted("user", next_measurement.stat.user, self.stat.user, time_difference)?, - nice: super::time_adjusted("nice", next_measurement.stat.nice, self.stat.nice, time_difference)?, - system: super::time_adjusted("system", next_measurement.stat.system, self.stat.system, time_difference)?, - idle: super::time_adjusted("idle", next_measurement.stat.idle, self.stat.idle, time_difference)?, - iowait: super::time_adjusted("iowait", next_measurement.stat.iowait, self.stat.iowait, time_difference)?, - irq: super::time_adjusted("irq", next_measurement.stat.irq, self.stat.irq, time_difference)?, - softirq: super::time_adjusted("softirq", next_measurement.stat.softirq, self.stat.softirq, time_difference)?, - steal: super::time_adjusted("steal", next_measurement.stat.steal, self.stat.steal, time_difference)?, - guest: super::time_adjusted("guest", next_measurement.stat.guest, self.stat.guest, time_difference)?, - guestnice: super::time_adjusted("guestnice", next_measurement.stat.guestnice, self.stat.guestnice, time_difference)? + total: time_adjusted("total", next_measurement.stat.total, self.stat.total, time_difference)?, + user: time_adjusted("user", next_measurement.stat.user, self.stat.user, time_difference)?, + nice: time_adjusted("nice", next_measurement.stat.nice, self.stat.nice, time_difference)?, + system: time_adjusted("system", next_measurement.stat.system, self.stat.system, time_difference)?, + idle: time_adjusted("idle", next_measurement.stat.idle, self.stat.idle, time_difference)?, + iowait: time_adjusted("iowait", next_measurement.stat.iowait, self.stat.iowait, time_difference)?, + irq: time_adjusted("irq", next_measurement.stat.irq, self.stat.irq, time_difference)?, + softirq: time_adjusted("softirq", next_measurement.stat.softirq, self.stat.softirq, time_difference)?, + steal: time_adjusted("steal", next_measurement.stat.steal, self.stat.steal, time_difference)?, + guest: time_adjusted("guest", next_measurement.stat.guest, self.stat.guest, time_difference)?, + guestnice: time_adjusted("guestnice", next_measurement.stat.guestnice, self.stat.guestnice, time_difference)? }) } } @@ -91,19 +89,13 @@ pub fn read() -> Result { os::read() } -/// Read the current CPU stats of the container. -#[cfg(target_os = "linux")] -pub fn read_from_container() -> Result { - os::read_from_container() -} - #[cfg(target_os = "linux")] mod os { use std::path::Path; use std::io::BufRead; use time; - use super::super::{Result,file_to_buf_reader,parse_u64,path_to_string,read_file_value_as_u64,dir_exists}; - use super::{CpuMeasurement,CpuStat,CPU_SYS_NUMBER_OF_FIELDS}; + use super::super::super::{Result,file_to_buf_reader,parse_u64,path_to_string}; + use super::{CpuMeasurement,CpuStat}; use error::ProbeError; #[inline] @@ -111,16 +103,6 @@ mod os { read_and_parse_proc_stat(&Path::new("/proc/stat")) } - pub fn read_from_container() -> Result { - let sys_fs_dir = Path::new("/sys/fs/cgroup/cpuacct/"); - if dir_exists(sys_fs_dir) { - read_and_parse_sys_stat(&sys_fs_dir) - } else { - let message = format!("Directory `{}` not found", sys_fs_dir.to_str().unwrap_or("unknown path")); - Err(ProbeError::UnexpectedContent(message)) - } - } - pub fn read_and_parse_proc_stat(path: &Path) -> Result { let mut line = String::new(); // columns: user nice system idle iowait irq softirq @@ -166,70 +148,12 @@ mod os { stat: cpu }) } - - pub fn read_and_parse_sys_stat(path: &Path) -> Result { - let time = time::precise_time_ns(); - let reader = file_to_buf_reader(&path.join("cpuacct.stat"))?; - let total = nano_to_user(read_file_value_as_u64(&path.join("cpuacct.usage"))?); - - let mut cpu = CpuStat { - total: total, - user: 0, - system: 0, - nice: 0, - idle: 0, - iowait: 0, - irq: 0, - softirq: 0, - steal: 0, - guest: 0, - guestnice: 0 - }; - - let mut fields_encountered = 0; - for line in reader.lines() { - let line = line.map_err(|e| ProbeError::IO(e, path_to_string(path)))?; - let segments: Vec<&str> = line.split_whitespace().collect(); - let value = parse_u64(&segments[1])?; - fields_encountered += match segments[0] { - "user" => { - cpu.user = value; - 1 - }, - "system" => { - cpu.system = value; - 1 - }, - _ => 0 - }; - - if fields_encountered == CPU_SYS_NUMBER_OF_FIELDS { - break - } - } - - if fields_encountered != CPU_SYS_NUMBER_OF_FIELDS { - return Err(ProbeError::UnexpectedContent("Did not encounter all expected fields".to_owned())) - } - - Ok(CpuMeasurement { - precise_time_ns: time, - stat: cpu - }) - } - - // [CPU usage] times are expressed in ticks of 1/100th of a second, also called "user jiffies". - // There are USER_HZ “jiffies” per second, and on x86 systems, USER_HZ is 100. - // See: https://docs.docker.com/config/containers/runmetrics/#cpu-metrics-cpuacctstat - fn nano_to_user(value: u64) -> u64 { - value.checked_div(10_000_000).unwrap_or(0) - } } #[cfg(test)] mod test { use super::{CpuMeasurement,CpuStat,CpuStatPercentages}; - use super::os::{read_and_parse_proc_stat,read_and_parse_sys_stat}; + use super::os::read_and_parse_proc_stat; use std::path::Path; use error::ProbeError; @@ -238,11 +162,6 @@ mod test { assert!(super::read().is_ok()); } - #[test] - fn test_read_from_container() { - assert!(super::read_from_container().is_ok()); - } - #[test] fn test_read_proc_measurement() { let measurement = read_and_parse_proc_stat(&Path::new("fixtures/linux/cpu/proc_stat")).unwrap(); @@ -302,48 +221,6 @@ mod test { } } - #[test] - fn test_read_sys_measurement() { - let measurement = read_and_parse_sys_stat(&Path::new("fixtures/linux/sys/fs/cgroup/cpuacct/")).unwrap(); - let cpu = measurement.stat; - assert_eq!(cpu.total, 1395); - assert_eq!(cpu.user, 404); - assert_eq!(cpu.nice, 0); - assert_eq!(cpu.system, 749); - assert_eq!(cpu.idle, 0); - assert_eq!(cpu.iowait, 0); - assert_eq!(cpu.irq, 0); - assert_eq!(cpu.softirq, 0); - assert_eq!(cpu.steal, 0); - assert_eq!(cpu.guest, 0); - assert_eq!(cpu.guestnice, 0); - } - - #[test] - fn test_read_sys_wrong_path() { - match read_and_parse_sys_stat(&Path::new("bananas")) { - Err(ProbeError::IO(_, _)) => (), - r => panic!("Unexpected result: {:?}", r) - } - } - - #[test] - fn test_read_and_parse_sys_stat_incomplete() { - match read_and_parse_sys_stat(&Path::new("fixtures/linux/sys/fs/cgroup/cpuacct_incomplete/")) { - Err(ProbeError::UnexpectedContent(_)) => (), - r => panic!("Unexpected result: {:?}", r) - } - } - - #[test] - fn test_read_and_parse_sys_stat_garbage() { - let path = Path::new("fixtures/linux/sys/fs/cgroup/cpuacct_garbage/"); - match read_and_parse_sys_stat(&path) { - Err(ProbeError::UnexpectedContent(_)) => (), - r => panic!("Unexpected result: {:?}", r) - } - } - #[test] fn test_calculate_per_minute_wrong_times() { let measurement1 = CpuMeasurement {