monitoring kit
init
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.toml | 15 | ||||
| -rw-r--r-- | cpu/Cargo.toml | 18 | ||||
| -rw-r--r-- | cpu/src/main.rs | 253 | ||||
| -rw-r--r-- | grapher/Cargo.toml | 12 | ||||
| -rw-r--r-- | grapher/src/lib.rs | 98 |
6 files changed, 398 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea4708f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.lock +logs*
\ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2ca3a24 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +members = ["cpu", "grapher"] +resolver = "2" + +[profile.release] +debug = 2 +opt-level = 3 +# lto = "thin" +incremental = true + +[profile.dev.build-override] +opt-level = 3 + +[profile.release.build-override] +opt-level = 3 diff --git a/cpu/Cargo.toml b/cpu/Cargo.toml new file mode 100644 index 0000000..66745ff --- /dev/null +++ b/cpu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cpu" +version = "1.0.0" +edition = "2021" +authors = ["bend-n <[email protected]>"] +repository = "https://github.com/bend-n/monitorkit" +license = "MIT" +rust-version = "1.85" + +[dependencies] +anyhow = "1.0.97" +atools = "0.1.6" +collar = "1.0.1" +comat = "0.1.3" +parking_lot = "0.12.3" +regex = "1.11.1" +termion = "4.0.4" +grapher = { path = "../grapher" } diff --git a/cpu/src/main.rs b/cpu/src/main.rs new file mode 100644 index 0000000..adc0e29 --- /dev/null +++ b/cpu/src/main.rs @@ -0,0 +1,253 @@ +#![feature( + let_chains, + iter_array_chunks, + array_chunks, + generic_const_exprs, + portable_simd, + iter_chain +)] +use anyhow::{anyhow, bail, ensure, Context, Result}; +use atools::prelude::*; +use collar::CollectArray; +use comat::cwrite; +use grapher::Grapher; +use parking_lot::Mutex; +use std::array; +use std::collections::HashMap; +use std::fmt::Display; +use std::fs::{read_to_string as read, File}; +use std::io::{stdout, Read}; +use std::io::{Seek, Write}; +use std::mem::replace; +use std::path::Path; +use std::sync::OnceLock; +use std::thread::sleep; +use std::time::Duration; +use termion::color::*; +use termion::cursor::Hide; +use termion::raw::IntoRawMode; +use termion::screen::IntoAlternateScreen; +use termion::{async_stdin, clear, cursor, style}; + +#[derive(Copy, Clone, Debug)] +enum ViewCore { + All(u64), + One(u64), +} +impl Display for ViewCore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ViewCore::All(x) => write!(f, "..#{x}"), + ViewCore::One(x) => write!(f, "#{x}"), + } + } +} + +static CORE: OnceLock<ViewCore> = OnceLock::new(); + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct CpuInfo { + pub name: String, + pub speed: f64, + pub count: u64, +} + +struct Temps(File); +impl Temps { + fn load() -> Result<Self> { + for hwmon in std::fs::read_dir("/sys/class/hwmon/")?.filter_map(Result::ok) { + if !read(Path::join(&hwmon.path(), "name"))?.starts_with("coretemp") { + continue; + } + for f in std::fs::read_dir(hwmon.path())?.filter_map(Result::ok) { + if let Some(n) = f.file_name().to_str() + && n.starts_with("temp") + && n.ends_with("input") + { + let i = n + .bytes() + .filter(u8::is_ascii_digit) + .fold(0, |acc, x| acc * 10 + (x - b'0') as u64); + + let name = read(Path::join(&hwmon.path(), format!("temp{i}_label"))) + .context("alas")?; + if let Some(c) = core() { + if !name.contains(&c.to_string()) { + continue; + } + } else if !name.contains("Package") { + continue; + } + + let f = File::open(f.path())?; + return Ok(Self(f)); + } + } + } + bail!("h") + } + fn read(&mut self) -> Result<f32> { + let mut o = String::default(); + self.0.seek(std::io::SeekFrom::Start(0))?; + self.0.read_to_string(&mut o)?; + Ok(o.trim().parse::<f32>().context("reading temps")? / 1000.0) + } +} + +impl CpuInfo { + fn read() -> Result<Self> { + let x = String::from_utf8( + std::process::Command::new("lscpu") + .env("LC_ALL", "C") + .output() + .context("lscpuless")? + .stdout, + ) + .context("unable to parse lscpu output to UTF-8")?; + let x = x + .lines() + .filter_map(|l| l.split_once(":").map(|(a, b)| (a, b.trim()))) + .collect::<HashMap<_, _>>(); + let name = x["Model name"] + .replace("Core", "") + .replace("(TM)", "") + .replace("Intel", "") + .replace("(R)", ""); + let name = regex::Regex::new(r"@ [0-9]+\.[0-9]+GHz") + .unwrap() + .replace(&name, ""); + let name = regex::Regex::new(r"[0-9]+th Gen") + .unwrap() + .replace(&name, "") + .replace(" ", " ") + .trim() + .replace(" ", " ") + .to_lowercase(); + Ok(Self { + name: name, + speed: x["CPU max MHz"].parse::<f64>().context("cpu mhz??")? / 1000.0, + count: x["CPU(s)"].parse()?, + }) + } +} + +fn sped(core: u64) -> Result<f64> { + Ok(read(format!( + "/sys/devices/system/cpu/cpu{core}/cpufreq/scaling_cur_freq" + )) + .context("speed")? + .replace('\n', "") + .parse::<u64>() + .context("reading speeds")? as f64 + / 1e6) +} + +fn speed() -> Result<f64> { + Ok(match *CORE.get().unwrap() { + ViewCore::All(x) => (0..x).map(sped).sum::<Result<f64, _>>()? / x as f64, + ViewCore::One(x) => sped(x)?, + }) +} + +fn core() -> Option<u64> { + CORE.get().copied().and_then(|x| match x { + ViewCore::One(x) => Some(x), + _ => None, + }) +} + +fn usage() -> Result<f64> { + let x = read("/proc/stat")?; + let x = x + .lines() + .nth(core().map_or(0, |x| x as usize + 1)) + .ok_or(anyhow!("no procstat"))?; + + // https://www.linuxhowtos.org/System/procstat.htm + let x @ [_user, _nice, _system, idle, iowait, _irq, _softirq] = x + .split_whitespace() + .skip(1) + .map(|x| x.parse::<u64>()) + .try_collect_array()?; + + static LAST: Mutex<[u64; 2]> = Mutex::new([0; 2]); + let [pi, pt] = &mut *LAST.lock(); + + let idle = idle + iowait; + let tot = x.sum(); + + let idle = (idle - replace(pi, idle)) as f64; + let tot = (tot - replace(pt, tot)) as f64; + let r = (tot - idle) / tot; + Ok((r == r).then_some(r).unwrap_or(0.0)) +} + +fn main() -> Result<()> { + fn inter([a, b, c]: [f32; 3], [d, e, f]: [f32; 3], fc: f32) -> [f32; 3] { + [a + (d - a) * fc, b + (e - b) * fc, c + (f - c) * fc] + } + + let info = CpuInfo::read()?; + let core = std::env::args() + .nth(1) + .and_then(|x| x.parse::<u64>().ok()) + .map_or(ViewCore::All(info.count), ViewCore::One); + CORE.set(core).unwrap(); + match core { + ViewCore::One(x) => ensure!(x < info.count, "not enough cores"), + _ => (), + } + let mut t = Temps::load()?; + + let mut g = Grapher::new()?; + g.push_point(usage()?.max(0.01)); + + let mut d = 0.1; + + let mut stdout = stdout().into_raw_mode()?.into_alternate_screen()?; + let mut stdin = async_stdin(); + write!(stdout, "{}{}{}", Hide, clear::All, style::Reset).unwrap(); + + 'out: loop { + let (_, h) = termion::terminal_size()?; + + let mut key = 0; + while stdin.read(array::from_mut(&mut key)).unwrap() != 0 { + match key { + b'q' => break 'out, + b'+' => d = (d + 0.1f32).min(1.0), + b'-' if d >= 0.2 => d -= 0.1, + b'-' if d >= 0.02 => d -= 0.01, + b'-' => d = 0.00833, + _ => (), + } + } + + g.draw(|y| { + inter( + [243, 64, 64].map(|x| x as f32 / 255.0), // red + [228, 197, 63].map(|x| x as f32 / 255.0), // yellow + y as f32 / (h - 1) as f32, + ) + .map(|x| (x * 255.0) as u8) + })?; + + write!(g.buffer, "{}{}", White.fg_str(), cursor::Goto(1, 1))?; + let name = &*info.name; + let speed = speed()?; + let temp = t.read()?; + let fps = (1f32 / d).round(); + cwrite!( + g.buffer, + " {fps}fps ──── {name} {core} @ {speed:.2} GHz ──── {red}{temp}{reset} °C", + )?; + stdout.write_all(&g.buffer)?; + stdout.flush()?; + + g.push_point(usage()?.max(0.01)); + + sleep(Duration::from_secs_f32(d)); + } + println!("\x1B[?7l"); + Ok(()) +} diff --git a/grapher/Cargo.toml b/grapher/Cargo.toml new file mode 100644 index 0000000..890709e --- /dev/null +++ b/grapher/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "grapher" +version = "1.0.0" +edition = "2021" +authors = ["bend-n <[email protected]>"] +repository = "https://github.com/bend-n/monitorkit" +license = "MIT" +rust-version = "1.85" + +[dependencies] +anyhow = "1.0.97" +termion = "4.0.4" diff --git a/grapher/src/lib.rs b/grapher/src/lib.rs new file mode 100644 index 0000000..09f2fbb --- /dev/null +++ b/grapher/src/lib.rs @@ -0,0 +1,98 @@ +#![feature(let_chains, iter_array_chunks, array_chunks, portable_simd, iter_chain)] +use anyhow::Result; +use std::collections::VecDeque; +use std::io::Write; +use std::iter::zip; +use std::simd::prelude::*; +use termion::color::*; +use termion::{clear, cursor}; + +pub struct Grapher { + pub buffer: Vec<u8>, + pub data: VecDeque<f64>, +} +impl Grapher { + pub fn new() -> Result<Self> { + Ok(Self { + buffer: Vec::with_capacity(1 << 20), + data: VecDeque::from(vec![0.0; termion::terminal_size()?.1 as usize * 2]), + }) + } + pub fn push_point(&mut self, x: f64) { + self.data.push_front(x); + self.data.pop_back(); + } + + pub fn draw(&mut self, mut color: impl FnMut(u16) -> [u8; 3]) -> Result<&mut Vec<u8>> { + let Grapher { + buffer: output, + data, + } = self; + output.clear(); + let (w, h) = termion::terminal_size()?; + if w * 2 < data.len() as u16 { + for _ in 0..data.len() as u16 - w * 2 { + data.pop_back(); + } + } + if w * 2 > data.len() as u16 { + for _ in 0..w * 2 - data.len() as u16 { + data.push_back(0.0); + } + } + assert_eq!(data.len(), (w * 2) as usize); + + let string = data + .iter() + .rev() + .array_chunks::<2>() + .map(|column| { + let mut data = [vec![false; (h * 4) as usize], vec![false; (h * 4) as usize]]; + for (e, c) in zip(column, &mut data) { + let clen = c.len(); + c[clen - ((h as f64 * e * 4.0).round().min(h as f64 * 4.0) as usize)..] + .fill(true); + } + let a = zip(data[0].array_chunks::<4>(), data[1].array_chunks::<4>()) + .map(|(a, b)| braille([*a, *b])) + .collect::<Vec<u8>>(); + assert_eq!(a.len(), h as usize); + a + }) + .collect::<Vec<_>>(); + assert_eq!(string.len(), w as usize); + write!( + output, + "\x1B[?7h{}{}{}", + clear::All, + Blue.fg_str(), + cursor::Goto(1, 1) + )?; + for y in 0..h as usize { + let [r, g, b] = color(y as u16); + write!(output, "{}", Rgb(r, g, b).fg_string())?; + + for x in 0..w as usize { + output.extend(bl(string[x][y])); + } + if y as u16 != h - 1 { + output.extend(b"\r\n"); + } + } + Ok(output) + } +} + +fn braille(dots: [[bool; 4]; 2]) -> u8 { + let x = unsafe { dots.as_ptr().cast::<u8x8>().read_unaligned() }; + let x = simd_swizzle!(x, [0, 1, 2, /* */ 4, 5, 6, /* */ 3, 7]); + x.simd_eq(Simd::splat(1)).to_bitmask() as u8 +} + +fn bl(x: u8) -> [u8; 3] { + let mut b = [0; 3]; + char::from_u32(0x2800 + x as u32) + .unwrap() + .encode_utf8(&mut b); + b +} |