monitoring kit
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml15
-rw-r--r--cpu/Cargo.toml18
-rw-r--r--cpu/src/main.rs253
-rw-r--r--grapher/Cargo.toml12
-rw-r--r--grapher/src/lib.rs98
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
+}