Skip to content

Commit

Permalink
Merge pull request #38 from appsignal/container-cpu-runtime-metrics
Browse files Browse the repository at this point in the history
Fix container CPU runtime metrics
  • Loading branch information
tombruijn committed Jun 6, 2019
2 parents e2ac041 + 413b4ea commit a17933f
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 141 deletions.
2 changes: 0 additions & 2 deletions fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.stat

This file was deleted.

1 change: 0 additions & 1 deletion fixtures/linux/sys/fs/cgroup/cpuacct/cpuacct.usage

This file was deleted.

2 changes: 2 additions & 0 deletions fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.stat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
user 14934
system 98
1 change: 1 addition & 0 deletions fixtures/linux/sys/fs/cgroup/cpuacct_1/cpuacct.usage
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
152657213021
2 changes: 2 additions & 0 deletions fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.stat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
user 17783
system 121
1 change: 1 addition & 0 deletions fixtures/linux/sys/fs/cgroup/cpuacct_2/cpuacct.usage
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
182405617026
351 changes: 351 additions & 0 deletions src/cpu/cgroup.rs
Original file line number Diff line number Diff line change
@@ -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<CgroupCpuStat> {
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<CgroupCpuMeasurement> {
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<CgroupCpuMeasurement> {
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<CgroupCpuMeasurement> {
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);
}
}
2 changes: 2 additions & 0 deletions src/cpu/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod proc;
pub mod cgroup;

0 comments on commit a17933f

Please sign in to comment.