about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/db.rs69
-rw-r--r--src/gatherer.rs71
-rwxr-xr-xsrc/main.rs6
3 files changed, 143 insertions, 3 deletions
diff --git a/src/db.rs b/src/db.rs
index f10f9ee..9a0256a 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -4,7 +4,7 @@ use camino::Utf8PathBuf;
 use rusqlite::{params, Connection, OptionalExtension};
 use time::OffsetDateTime;
 
-use crate::gatherer::Meminfo;
+use crate::gatherer::{Cpuinfo, Meminfo};
 
 pub struct Database {
 	db_path: Utf8PathBuf,
@@ -23,6 +23,7 @@ impl Database {
 		let conn = self.conn.lock().unwrap();
 		conn.execute(CREATE_TABLE_HOSTMEM, params![]).unwrap();
 		conn.execute(CREATE_TABLE_HOSTNET, params![]).unwrap();
+		conn.execute(CREATE_TABLE_HOSTCPU, params![]).unwrap();
 	}
 
 	pub fn insert_host_meminfo(&self, meminfo: Meminfo) {
@@ -125,6 +126,42 @@ impl Database {
 		.map(|r| r.unwrap())
 		.collect()
 	}
+
+	pub fn insert_hostcpu(
+		&self,
+		span_sec: usize,
+		user_delta: usize,
+		nice_delta: usize,
+		system_delta: usize,
+	) {
+		let conn = self.conn.lock().unwrap();
+
+		conn.execute(
+			"INSERT INTO stats_hostcpu(span_sec, user_delta, nice_delta, system_delta) VALUES (?1, ?2, ?3, ?4)",
+			params![span_sec, user_delta, nice_delta, system_delta],
+		)
+		.unwrap();
+	}
+
+	pub fn get_last_n_hostcpu(&self, count: usize) -> Vec<DbCpuinfo> {
+		let conn = self.conn.lock().unwrap();
+		let mut stmt = conn
+			.prepare("SELECT * FROM stats_hostcpu ORDER BY stamp DESC LIMIT ?1")
+			.unwrap();
+
+		stmt.query_map(params![count], |row| {
+			Ok(DbCpuinfo {
+				stamp: row.get(0)?,
+				span_sec: row.get(1)?,
+				user_delta: row.get(2)?,
+				nice_delta: row.get(3)?,
+				system_delta: row.get(4)?,
+			})
+		})
+		.unwrap()
+		.map(|r| r.unwrap())
+		.collect()
+	}
 }
 
 pub const CREATE_TABLE_HOSTMEM: &'static str = "\
@@ -142,6 +179,15 @@ pub const CREATE_TABLE_HOSTNET: &'static str = "\
         tx_delta INTEGER NOT NULL
     );";
 
+pub const CREATE_TABLE_HOSTCPU: &'static str = "\
+	CREATE TABLE IF NOT EXISTS stats_hostcpu(
+		stamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+		span_sec INTEGER NOT NULL,
+		user_delta INTEGER NOT NULL,
+		nice_delta INTEGER NOT NULL,
+		system_delta INTEGER NOT NULL
+	);";
+
 #[derive(Clone, Copy, Debug)]
 pub struct DbMeminfo {
 	pub stamp: OffsetDateTime,
@@ -172,3 +218,24 @@ impl DbNetinfo {
 		self.tx_delta as f32 / self.span_sec as f32
 	}
 }
+
+#[derive(Copy, Clone, Debug)]
+pub struct DbCpuinfo {
+	pub stamp: OffsetDateTime,
+	pub span_sec: usize,
+	pub user_delta: usize,
+	pub nice_delta: usize,
+	pub system_delta: usize,
+}
+
+impl DbCpuinfo {
+	/// Returns the avarage usage still in USER_HZ which is
+	/// in 1/100ths sec on "most systems". This is averaged
+	/// across the minute sample rate, so unit is:
+	/// USER_HZ/s
+	pub fn average_usage(&self) -> f32 {
+		let used_sum = self.user_delta + self.nice_delta + self.system_delta;
+		let avg = used_sum as f32 / self.span_sec as f32;
+		avg
+	}
+}
diff --git a/src/gatherer.rs b/src/gatherer.rs
index 163d84b..7289f60 100644
--- a/src/gatherer.rs
+++ b/src/gatherer.rs
@@ -40,6 +40,7 @@ fn task(state: AwakeState) {
 	// I just want a graph on first boot; don't care about divisions just yet
 	make_mem_graph(&state);
 	make_net_graph(&state);
+	make_cpu_graph(&state);
 
 	// If we collected a point less than a minute ago, like after
 	// just being restarted, wait until it's been a minute
@@ -56,6 +57,7 @@ fn task(state: AwakeState) {
 	}
 
 	let mut last_netinfo: Option<Netinfo> = None;
+	let mut last_cpuinfo: Option<Cpuinfo> = None;
 
 	// this is a `let _` because otherwise the attribute was
 	// making the comiler mad
@@ -66,11 +68,14 @@ fn task(state: AwakeState) {
 		// Gather data
 		let meminfo = Meminfo::current();
 		let netinfo = Netinfo::current();
+		let cpuinfo = Cpuinfo::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));
+		tracing::trace!("cpu: user {} // nice {} // system {}", cpuinfo.user, cpuinfo.nice, cpuinfo.system);
 
+		// Store stats in database
 		if let Some(lni) = last_netinfo {
 			let rx_delta = netinfo.rx_bytes - lni.rx_bytes;
 			let tx_delta = netinfo.tx_bytes - lni.tx_bytes;
@@ -79,7 +84,15 @@ fn task(state: AwakeState) {
 		}
 		last_netinfo = Some(netinfo);
 
-		// Store stats in database
+		if let Some(lci) = last_cpuinfo {
+			let user_delta = cpuinfo.user - lci.user;
+			let nice_delta = cpuinfo.nice - lci.nice;
+			let system_delta = cpuinfo.system - lci.system;
+
+			state.database.insert_hostcpu(60, user_delta, nice_delta, system_delta);
+		}
+		last_cpuinfo = Some(cpuinfo);
+
 		state.database.insert_host_meminfo(meminfo);
 
 		// Only generate graphs every 15 minutes
@@ -87,6 +100,7 @@ fn task(state: AwakeState) {
 		if now.minute() % 15 == 0 {
 			make_mem_graph(&state);
 			make_net_graph(&state);
+			make_cpu_graph(&state);
 		}
 
 		std::thread::sleep(Duration::from_secs(60));
@@ -159,6 +173,26 @@ pub fn make_net_graph(state: &AwakeState) {
 	gif.save(path).unwrap();
 }
 
+pub fn make_cpu_graph(state: &AwakeState) {
+	tracing::debug!("generating cpuinfo graph");
+
+	let infos = state.database.get_last_n_hostcpu(256);
+
+	let now = OffsetDateTime::now_utc();
+	let cleaned = clean_series(&infos, |cpu| cpu.stamp, now);
+	// Scalling by 10 because the graph system does not like to have a max of only 100 due to some bad programming
+	let mut usages = extract(&cleaned, |cpu| (cpu.average_usage() * 10.0) as usize);
+
+	// Reversing here because we want latest valeus on on the
+	// right side, so last in the array
+	usages.reverse();
+
+	let gif = griph::make_1line(0, 1000, &usages);
+
+	let path = state.cache_path.join("current_hostcpuinfo.gif");
+	gif.save(path).unwrap();
+}
+
 fn clean_series<T, F>(series: &[T], time_extractor: F, end_time: OffsetDateTime) -> [Option<T>; 256]
 where
 	F: Fn(&T) -> OffsetDateTime,
@@ -292,6 +326,41 @@ impl Netinfo {
 	}
 }
 
+/// Not strictly the correct name for this, but that's fine.
+/// Structure for parsed-from /proc/stat
+pub struct Cpuinfo {
+	user: usize,
+	nice: usize,
+	system: usize,
+}
+
+impl Cpuinfo {
+	pub fn current() -> Self {
+		let procinfo = File::open("/proc/stat").unwrap();
+		let bread = BufReader::new(procinfo);
+
+		let mut cpuinfo = Cpuinfo {
+			user: 0,
+			nice: 0,
+			system: 0,
+		};
+
+		for line in bread.lines() {
+			let line = line.unwrap();
+			let Some(data) = line.strip_prefix("cpu  ") else {
+				continue;
+			};
+
+			let mut splits = data.split(' ');
+			cpuinfo.user = splits.next().unwrap().parse().unwrap();
+			cpuinfo.nice = splits.next().unwrap().parse().unwrap();
+			cpuinfo.system = splits.next().unwrap().parse().unwrap();
+		}
+
+		cpuinfo
+	}
+}
+
 fn data_human_fmt(bytes: usize) -> String {
 	let (num, unit) = data_human(bytes);
 	format!("{num}{unit}")
diff --git a/src/main.rs b/src/main.rs
index 6d0f27e..5ba279f 100755
--- a/src/main.rs
+++ b/src/main.rs
@@ -443,7 +443,11 @@ fn template_content(state: AwakeState, frontmatter: &Frontmatter, marked: String
 }
 
 async fn stats(State(state): State<AwakeState>, Path(name): Path<String>) -> Response {
-	const NAMES: &[&'static str] = &["current_hostmeminfo.gif", "current_hostnetinfo.gif"];
+	const NAMES: &[&'static str] = &[
+		"current_hostmeminfo.gif",
+		"current_hostnetinfo.gif",
+		"current_hostcpuinfo.gif",
+	];
 
 	if NAMES.contains(&name.as_str()) {
 		let path = state.cache_path.join(name);