From 87e0d2b03d0801cc84456e1915258b6009fba906 Mon Sep 17 00:00:00 2001 From: Till Höppner Date: Sat, 8 Oct 2016 19:03:01 +0200 Subject: Measure CPU usage, not frequency --- Cargo.toml | 5 +- src/main.rs | 251 ++++++++++++++++++++++++++++++++++-------------------------- 2 files changed, 146 insertions(+), 110 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d814d6b..1ac3624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,7 @@ version = "0.1.0" [dependencies] clap = "2.13.0" -num = "0.1.36" +env_logger = "0.3.5" +libc = "0.2.16" +log = "0.3.6" +vec_map = "0.6.0" diff --git a/src/main.rs b/src/main.rs index 956285b..60c02e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,127 +1,143 @@ #[macro_use] extern crate clap; -extern crate num; +extern crate vec_map; +extern crate libc; -use std::path::{Path, PathBuf}; -use std::fs::File; -use std::io::{self, Read}; -use std::{iter, thread, ops}; -use std::time::Duration; - -use clap::{Arg, App, AppSettings, SubCommand}; - -use num::NumCast; - -const SYS_PRESENT: &'static str = "/sys/devices/system/cpu/present"; -static FORMAT: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; - -const WINDOW_SIZE: usize = 32; - -#[derive(Debug, Default)] -struct Window { - data: Vec, - idx: usize, - size: usize -} - -impl Window where T: for<'a> iter::Sum<&'a T> + ops::Div + NumCast { - fn new(size: usize) -> Window { - Window { data: Vec::with_capacity(size), idx: 0, size: size } - } - - fn sample(&mut self, value: T) { - if self.data.len() < self.size { self.data.push(value) } - else { self.data[self.idx] = value; } - self.idx = (self.idx + 1) % self.size; - } - - fn elements(&self) -> (&[T], &[T]) { (&self.data[self.idx..], &self.data[..self.idx]) } - fn average(&self) -> T { self.data.iter().sum::() / T::from(self.data.len()).unwrap() } -} - -#[derive(Debug, Default)] -struct Core { - index: u32, - min_freq: u32, - max_freq: u32, - cur_freq: Window -} +#[macro_use] +extern crate log; +extern crate env_logger; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use std::thread; +use std::time::{Instant, Duration}; + +use clap::{Arg, App}; + +use vec_map::VecMap; + +const PROC_STAT: &'static str = "/proc/stat"; +static FORMAT: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + +/// cpu 3357 0 4313 1362393 +/// The amount of time, measured in units of USER_HZ (1/100ths of a +/// second on most architectures, use sysconf(_SC_CLK_TCK) to obtain +/// the right value), that the system spent in various states: +/// +/// user (1) Time spent in user mode. +/// +/// nice (2) Time spent in user mode with low priority (nice). +/// +/// system (3) Time spent in system mode. +/// +/// idle (4) Time spent in the idle task. This value should be +/// USER_HZ times the second entry in the /proc/uptime pseudo- +/// file. +/// +/// iowait (since Linux 2.5.41) +/// (5) Time waiting for I/O to complete. +/// +/// irq (since Linux 2.6.0-test4) +/// (6) Time servicing interrupts. +/// +/// softirq (since Linux 2.6.0-test4) +/// (7) Time servicing softirqs. +/// +/// steal (since Linux 2.6.11) +/// (8) Stolen time, which is the time spent in other operating +/// systems when running in a virtualized environment +/// +/// guest (since Linux 2.6.24) +/// (9) Time spent running a virtual CPU for guest operating +/// systems under the control of the Linux kernel. +/// +/// guest_nice (since Linux 2.6.33) +/// (10) Time spent running a niced guest (virtual CPU for +/// guest operating systems under the control of the Linux ker‐ +/// nel). #[derive(Debug)] -struct System { - cores: Vec, - load: Window +struct Stat { + time: Instant, + total: Option, + cores: VecMap } -impl System { - fn new() -> System { - System { - cores: init_cores(), - load: Window::new(8) +impl Stat { + pub fn read() -> io::Result { + let file = try!(File::open(PROC_STAT)); + let reader = BufReader::new(file); + let mut stat = Stat { time: Instant::now(), total: None, cores: VecMap::new() }; + + for line in reader.lines() { + let line = try!(line); + const OFFSET: usize = 3; // "cpu".len() + if line.starts_with("cpu ") { + stat.total = Some(CPU::from_line(&line[OFFSET..])); + } else if line.starts_with("cpu") { + let num: u64 = line[OFFSET..].split_whitespace().next().and_then(|s| s.trim().parse().ok()).unwrap(); + stat.cores.insert(num as usize, CPU::from_line(&line[OFFSET..])); + } } - } - // Direct printing to avoid allocation - fn print_cores(&self) { - for c in &self.cores { - let load = (c.cur_freq.average() - c.min_freq) as f32 / (c.max_freq - c.min_freq) as f32; - print!("{}", FORMAT[(FORMAT.len() as f32 * load) as usize]); - } - println!(""); + Ok(stat) } - fn print_system(&self) { - let (first, last) = self.load.elements(); - for load in first.iter().chain(last.iter()) { - print!("{}", FORMAT[(FORMAT.len() as f32 * load) as usize]); + pub fn load_since(&self, earlier: &Stat) -> Load { + Load { + duration: self.time.duration_since(earlier.time), + total: match (&self.total, &earlier.total) { + (&Some(ref now), &Some(ref old)) => Some(now.diff(old)), + _ => None + }, + cores: self.cores.iter() + .flat_map(|(idx, core)| earlier.cores.get(idx).map(|ec| (idx, core.diff(ec)))) + .collect() } - println!(""); } } -fn read_into_string>(path: P) -> io::Result { - let mut output = String::new(); - try!(try!(File::open(path)).read_to_string(&mut output)); - Ok(output) +#[derive(Debug, Clone)] +struct Load { + duration: Duration, + total: Option, + cores: VecMap } -fn init_cores() -> Vec { - let present = read_into_string(SYS_PRESENT).expect("Can't read kernel interface to query present cores"); - - // a-b - let mut parts = present.split('-').flat_map(|s| s.trim().parse::().ok()); - let (a, b) = (parts.next().unwrap(), parts.next().unwrap()); - (a..b + 1).map(|idx| { - let cpu_path: PathBuf = format!("/sys/devices/system/cpu/cpu{}/cpufreq", idx).into(); - Core { - index: idx, - min_freq: read_into_string(cpu_path.join("scaling_min_freq")).ok() - .and_then(|s| s.trim().parse().ok()).expect("Can't read min freq"), - max_freq: read_into_string(cpu_path.join("scaling_max_freq")).ok() - .and_then(|s| s.trim().parse().ok()).expect("Can't read max frequency"), - cur_freq: Window::new(WINDOW_SIZE) - } - }).collect() +#[derive(Debug, Clone)] +struct CPU { + user: u64, + nice: u64, + system: u64, + idle: u64 } -fn update(system: &mut System) { - let mut frame_average = 0.; - for c in &mut system.cores { - let cpu_path: PathBuf = format!("/sys/devices/system/cpu/cpu{}/cpufreq/scaling_cur_freq", c.index).into(); - let cur_freq = read_into_string(cpu_path).ok() - .and_then(|s| s.trim().parse().ok()).expect("Can't read current frequency"); - c.cur_freq.sample(cur_freq); - frame_average += (cur_freq - c.min_freq) as f32 / (c.max_freq - c.min_freq) as f32; +impl CPU { + pub fn from_line(line: &str) -> CPU { + fn parse(s: Option<&str>) -> u64 { s.and_then(|s| s.trim().parse().ok()).expect("Couldn't parse CPU stat") } + let mut tok = line.split_whitespace(); + CPU { + user: parse(tok.next()), + nice: parse(tok.next()), + system: parse(tok.next()), + idle: parse(tok.next()) + } + } + + pub fn diff(&self, other: &CPU) -> CPU { + CPU { + user: self.user - other.user, + nice: self.nice - other.nice, + system: self.system - other.system, + idle: self.idle - other.idle + } } - system.load.sample(frame_average / system.cores.len() as f32); } fn main() { + env_logger::init().unwrap(); let matches = App::new("cpuline") .version(crate_version!()) .author(crate_authors!()) - .about("Display CPU usage per-core or over time (Linux-only)") - .setting(AppSettings::SubcommandRequired) .arg(Arg::with_name("interval") .global(true) .short("i") @@ -129,23 +145,40 @@ fn main() { .value_name("MS") .takes_value(true) .default_value("1000")) - .subcommand(SubCommand::with_name("cores")) - .subcommand(SubCommand::with_name("time")) .get_matches(); - let action = match matches.subcommand_name() { - Some("cores") => System::print_cores, - Some("time") => System::print_system, - _ => unreachable!() - }; - let interval = value_t!(matches, "interval", u64).unwrap(); - let mut system = System::new(); + let mut stat = None; + let user_hz: u64 = unsafe { libc::sysconf(libc::_SC_CLK_TCK) as u64 }; + debug!("user_hz({})", user_hz); + loop { - update(&mut system); - action(&system); + let old = stat; + stat = Stat::read().ok(); + + match (&stat, &old) { + (&Some(ref now), &Some(ref old)) => { + let load = now.load_since(&old); + let duration_ticks = load.duration.as_secs() * user_hz + + (load.duration.subsec_nanos() as f64 / 1E9 * user_hz as f64) as u64; + + for (_, core) in load.cores.iter() { + // How many ticks the core was in use + let used = core.user + core.nice + core.system; + // How long this core was used with 0 (not used) to 1 (fully used) + let used_part = used as f32 / duration_ticks as f32; + + let output = FORMAT[((FORMAT.len() - 1) as f32 * used_part) as usize]; + debug!("used_part({}) = used({}) / duration_ticks({}) => '{}'", used_part, used, duration_ticks, output); + print!("{}", output); + } + println!(""); + }, + _ => () + } + thread::sleep(Duration::from_millis(interval)); } } -- cgit v1.2.3