about summary refs log tree commit diff
path: root/src/gatherer.rs
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-02-16 10:17:28 -0600
committergennyble <gen@nyble.dev>2025-02-16 10:17:28 -0600
commit0c5986851dee23e09751ae594d8a43a84a0dab61 (patch)
treeed27d132aa04b86ecabd9cf5bb440b7d87f7bff3 /src/gatherer.rs
parente1b51d7dfc24c443af34410da2fed6aa6db52b62 (diff)
downloadawake-0c5986851dee23e09751ae594d8a43a84a0dab61.tar.gz
awake-0c5986851dee23e09751ae594d8a43a84a0dab61.zip
Network stats
Diffstat (limited to 'src/gatherer.rs')
-rw-r--r--src/gatherer.rs228
1 files changed, 228 insertions, 0 deletions
diff --git a/src/gatherer.rs b/src/gatherer.rs
new file mode 100644
index 0000000..0cceea6
--- /dev/null
+++ b/src/gatherer.rs
@@ -0,0 +1,228 @@
+use std::{
+	fs::File,
+	io::{BufRead, BufReader},
+	sync::mpsc::Sender,
+	thread::JoinHandle,
+	time::Duration,
+};
+
+use regex_lite::Regex;
+use time::OffsetDateTime;
+
+use crate::{griph, AwakeState};
+
+pub struct Gatherer {
+	state: AwakeState,
+	hwnd: Option<JoinHandle<()>>,
+}
+
+impl Gatherer {
+	pub fn new(state: AwakeState) -> Self {
+		Self { state, hwnd: None }
+	}
+
+	pub fn start(&mut self) {
+		let state = self.state.clone();
+		let hwnd = std::thread::spawn(|| task(state));
+		self.hwnd = Some(hwnd);
+	}
+}
+
+fn task(state: AwakeState) {
+	tracing::info!("starting gatherer thread");
+
+	// I just want a graph on first boot; don't care about divisions just yet
+	make_mem_graph(&state);
+	make_net_graph(&state);
+
+	let mut last_netinfo: Option<Netinfo> = None;
+
+	// this is a `let _` because otherwise the attribute was
+	// making the comiler mad
+	#[rustfmt::skip]
+	let _ = loop {
+		tracing::debug!("collecting stats");
+
+		// Gather data
+		let meminfo = Meminfo::current();
+		let netinfo = Netinfo::current();
+
+		// Print traces, y'know, for tracing
+		tracing::trace!("memory: {}MB used / {}MB total", meminfo.usage() / 1000, meminfo.total / 1000);
+		tracing::trace!("net: rx {} / tx {}", data_human_fmt(netinfo.rx_bytes), data_human_fmt(netinfo.tx_bytes));
+
+		if let Some(lni) = last_netinfo {
+			let rx_delta = netinfo.rx_bytes - lni.rx_bytes;
+			let tx_delta = netinfo.tx_bytes - lni.tx_bytes;
+
+			state.database.insert_hostnet(60, rx_delta, tx_delta);
+		}
+		last_netinfo = Some(netinfo);
+
+		// Store stats in database
+		state.database.insert_host_meminfo(meminfo);
+
+		// Only generate graphs every 15 minutes
+		let now = OffsetDateTime::now_utc();
+		if now.minute() % 15 == 0 {
+			make_mem_graph(&state);
+			make_net_graph(&state);
+		}
+
+		std::thread::sleep(Duration::from_secs(60));
+	};
+}
+
+pub fn make_mem_graph(state: &AwakeState) {
+	tracing::debug!("generating meminfo graph");
+
+	let infos = state.database.get_last_n_host_meminfo(256);
+	let max = infos[0].total_kb;
+	let usages: Vec<usize> = infos.into_iter().map(|mi| mi.usage()).collect();
+
+	let gif = griph::make_1line(0, max, &usages);
+
+	let path = state.cache_path.join("current_hostmeminfo.gif");
+	gif.save(path).unwrap();
+}
+
+pub fn make_net_graph(state: &AwakeState) {
+	tracing::debug!("generating netinfo graph");
+
+	let infos = state.database.get_last_n_hostnet(256);
+	let rx_deltas: Vec<usize> = infos
+		.iter()
+		.map(|ni| ni.rx_bytes_per_sec() as usize / 1000)
+		.collect();
+	let tx_deltas: Vec<usize> = infos
+		.iter()
+		.map(|ni| ni.tx_bytes_per_sec() as usize / 1000)
+		.collect();
+
+	for ahh in &tx_deltas {
+		tracing::trace!("ahh: {ahh} kbytes");
+	}
+
+	let gif = griph::make_2line(0, 1000, &rx_deltas, &tx_deltas);
+
+	let path = state.cache_path.join("current_hostnetinfo.gif");
+	gif.save(path).unwrap();
+}
+
+pub struct Meminfo {
+	pub total: usize,
+	pub free: usize,
+	pub avaialable: usize,
+}
+
+impl Meminfo {
+	pub fn current() -> Self {
+		let procinfo = File::open("/proc/meminfo").unwrap();
+		let bread = BufReader::new(procinfo);
+
+		let mut meminfo = Meminfo {
+			total: 0,
+			free: 0,
+			avaialable: 0,
+		};
+
+		for line in bread.lines() {
+			let line = line.unwrap();
+
+			if let Some((raw_key, raw_value_kb)) = line.split_once(':') {
+				let value = if let Some(raw_value) = raw_value_kb.trim().strip_suffix(" kB") {
+					if let Ok(parsed) = raw_value.parse() {
+						parsed
+					} else {
+						continue;
+					}
+				} else {
+					continue;
+				};
+
+				match raw_key.trim() {
+					"MemTotal" => meminfo.total = value,
+					"MemFree" => meminfo.free = value,
+					"MemAvailable" => meminfo.avaialable = value,
+					_ => (),
+				}
+			}
+		}
+
+		meminfo
+	}
+
+	pub fn usage(&self) -> usize {
+		self.total - self.avaialable
+	}
+}
+
+pub struct Netinfo {
+	rx_bytes: usize,
+	tx_bytes: usize,
+}
+
+impl Netinfo {
+	pub fn current() -> Self {
+		let procinfo = File::open("/proc/net/dev").unwrap();
+		let bread = BufReader::new(procinfo);
+
+		let mut netinfo = Self {
+			rx_bytes: 0,
+			tx_bytes: 0,
+		};
+
+		let re = Regex::new(r"[ ]*(\d+)").unwrap();
+		let interface = "eth0:";
+		for line in bread.lines() {
+			let line = line.unwrap();
+			let trim = line.trim();
+
+			let mut captures = if let Some(data) = trim.strip_prefix(interface) {
+				re.captures_iter(data)
+			} else {
+				continue;
+			};
+
+			netinfo.rx_bytes = captures
+				.next()
+				.unwrap()
+				.get(1)
+				.unwrap()
+				.as_str()
+				.parse()
+				.unwrap();
+			netinfo.tx_bytes = captures
+				.skip(7)
+				.next()
+				.unwrap()
+				.get(1)
+				.unwrap()
+				.as_str()
+				.parse()
+				.unwrap();
+			break;
+		}
+
+		netinfo
+	}
+}
+
+fn data_human_fmt(bytes: usize) -> String {
+	let (num, unit) = data_human(bytes);
+	format!("{num}{unit}")
+}
+
+fn data_human(bytes: usize) -> (f32, &'static str) {
+	const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB"];
+
+	let mut wrk = bytes as f32;
+	let mut unit_idx = 0;
+	loop {
+		if wrk < 1500.0 || unit_idx == UNITS.len() - 1 {
+			return (wrk, UNITS[unit_idx]);
+		}
+		wrk /= 1000.0;
+		unit_idx += 1;
+	}
+}