#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use std::path::Path;
use std::process::Command;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Environment {
pub timestamp_unix: u64,
pub revision_short: String,
pub revision_full: String,
pub cargo_lock_hash: String,
pub cargo_lock_bytes: u64,
pub kernel_ostype: String,
pub kernel_release: String,
pub kernel_version: String,
pub cpu_model: String,
pub cpu_flags: String,
pub cpu_count: usize,
pub cpu_mhz: Option<f64>,
pub memory_kib: u64,
pub governor: String,
pub store_dir: String,
pub store_device: String,
pub store_fstype: String,
pub cache_state: String,
pub command: String,
pub policy_mode: String,
pub uid: u32,
pub hostname: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiskStats {
pub reads: u64,
pub read_sectors: u64,
pub writes: u64,
pub write_sectors: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiskDelta {
pub device: String,
pub reads: u64,
pub read_sectors: u64,
pub writes: u64,
pub write_sectors: u64,
}
impl DiskDelta {
pub fn written_bytes(&self) -> u64 {
self.write_sectors * 512
}
pub fn read_bytes(&self) -> u64 {
self.read_sectors * 512
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub struct StatSummary {
pub count: usize,
pub mean: f64,
pub min: f64,
pub p50: f64,
pub p95: f64,
pub p99: f64,
pub max: f64,
}
impl Environment {
pub fn capture(
repo_root: &Path,
store_dir: &Path,
cache_state: &str,
policy_mode: &str,
) -> Environment {
let (dev, fstype) = mount_of(store_dir);
let cpu = cpu_info();
let clock = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let lock_path = repo_root.join("Cargo.lock");
let (lock_hash, lock_bytes) = std::fs::read(&lock_path)
.map(|b| (blake3::hash(&b).to_hex().to_string(), b.len() as u64))
.unwrap_or_else(|_| (String::new(), 0));
Environment {
timestamp_unix: clock,
revision_short: git_output(&["rev-parse", "--short", "HEAD"]),
revision_full: git_output(&["rev-parse", "HEAD"]),
cargo_lock_hash: lock_hash,
cargo_lock_bytes: lock_bytes,
kernel_ostype: read_trimmed("/proc/sys/kernel/ostype"),
kernel_release: read_trimmed("/proc/sys/kernel/osrelease"),
kernel_version: read_trimmed("/proc/sys/kernel/version"),
cpu_model: cpu.model,
cpu_flags: cpu.flags,
cpu_count: cpu.count,
cpu_mhz: cpu.mhz,
memory_kib: meminfo_total_kib(),
governor: read_trimmed("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"),
store_dir: store_dir.display().to_string(),
store_device: dev,
store_fstype: fstype,
cache_state: cache_state.to_string(),
command: std::env::args().collect::<Vec<_>>().join(" "),
policy_mode: policy_mode.to_string(),
uid: current_uid(),
hostname: read_trimmed("/proc/sys/kernel/hostname"),
}
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".into())
}
}
pub fn diskstats(device: &str) -> Option<DiskStats> {
let body = std::fs::read_to_string("/proc/diskstats").ok()?;
for line in body.lines() {
let mut it = line.split_whitespace();
let (_major, _minor, name) = (it.next()?, it.next()?, it.next()?);
if name != device {
continue;
}
let v: Vec<u64> = it.filter_map(|f| f.parse().ok()).collect();
if v.len() >= 6 {
return Some(DiskStats {
reads: v[0],
read_sectors: v[2],
writes: v[4],
write_sectors: v[6],
});
}
return None;
}
None
}
pub fn disk_delta(device: &str, before: &DiskStats, after: &DiskStats) -> DiskDelta {
DiskDelta {
device: device.to_string(),
reads: after.reads.saturating_sub(before.reads),
read_sectors: after.read_sectors.saturating_sub(before.read_sectors),
writes: after.writes.saturating_sub(before.writes),
write_sectors: after.write_sectors.saturating_sub(before.write_sectors),
}
}
pub fn percentile(sorted: &[f64], p: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
let idx = ((p / 100.0) * sorted.len() as f64).ceil() as usize;
sorted[idx.saturating_sub(1).min(sorted.len() - 1)]
}
pub fn summary(vals: &[f64]) -> StatSummary {
if vals.is_empty() {
return StatSummary::default();
}
let mut sorted = vals.to_vec();
sorted.sort_by(|a, b| a.total_cmp(b));
let mean = sorted.iter().sum::<f64>() / sorted.len() as f64;
StatSummary {
count: sorted.len(),
mean,
min: sorted[0],
p50: percentile(&sorted, 50.0),
p95: percentile(&sorted, 95.0),
p99: percentile(&sorted, 99.0),
max: *sorted.last().unwrap(),
}
}
pub fn mount_of(path: &Path) -> (String, String) {
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let target = canon.display().to_string();
let mut best: Option<(String, String, usize)> = None;
if let Ok(body) = std::fs::read_to_string("/proc/mounts") {
for line in body.lines() {
let mut it = line.split_whitespace();
let dev = it.next().unwrap_or_default().to_string();
let mp = it.next().unwrap_or_default().replace("\\040", " ");
let fstype = it.next().unwrap_or_default().to_string();
if mp.len() >= best.as_ref().map(|b| b.2).unwrap_or(0)
&& (target == mp || target.starts_with(&format!("{mp}/")))
{
best = Some((dev, fstype, mp.len()));
}
}
}
best.map(|(d, f, _)| (d, f))
.unwrap_or_else(|| ("unknown".into(), "unknown".into()))
}
struct CpuInfo {
model: String,
flags: String,
count: usize,
mhz: Option<f64>,
}
fn cpu_info() -> CpuInfo {
let body = std::fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
let mut model = String::new();
let mut flags = String::new();
let mut mhz = None;
let mut count = 0usize;
let mut first = true;
for line in body.lines() {
if let Some(v) = line.strip_prefix("model name") {
if first {
model = v.trim_start_matches(':').trim().to_string();
}
} else if let Some(v) = line.strip_prefix("flags") {
if first {
flags = v.trim_start_matches(':').trim().to_string();
}
} else if let Some(v) = line.strip_prefix("cpu MHz") {
if first {
mhz = v.trim_start_matches(':').trim().parse().ok();
}
} else if let Some(v) = line.strip_prefix("max MHz") {
if first {
mhz = v.trim_start_matches(':').trim().parse().ok();
}
} else if line.starts_with("processor") {
count += 1;
}
if line.trim().is_empty() {
first = false;
}
}
if count == 0 {
count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
}
CpuInfo {
model,
flags,
count,
mhz,
}
}
fn meminfo_total_kib() -> u64 {
std::fs::read_to_string("/proc/meminfo")
.ok()
.and_then(|body| {
body.lines()
.find(|l| l.starts_with("MemTotal"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse().ok())
})
.unwrap_or(0)
}
fn read_trimmed(path: &str) -> String {
std::fs::read_to_string(path)
.map(|s| s.trim().to_string())
.unwrap_or_default()
}
fn current_uid() -> u32 {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|body| {
body.lines()
.find(|l| l.starts_with("Uid:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse().ok())
})
.unwrap_or(0)
}
fn git_output(args: &[&str]) -> String {
Command::new("git")
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
}
pub fn map_to_csv(prefix: &str, m: &BTreeMap<String, u64>) -> Vec<String> {
m.iter().map(|(k, v)| format!("{prefix},{k},{v}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentile_nearest_rank() {
let s = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
assert_eq!(percentile(&s, 50.0), 5.0);
assert_eq!(percentile(&s, 100.0), 10.0);
assert_eq!(percentile(&s, 0.0), 1.0);
assert_eq!(percentile(&[], 50.0), 0.0);
}
#[test]
fn summary_basics() {
let s = summary(&[1.0, 2.0, 3.0]);
assert_eq!(s.count, 3);
assert_eq!(s.min, 1.0);
assert_eq!(s.max, 3.0);
assert!((s.mean - 2.0).abs() < 1e-9);
}
#[test]
fn disk_delta_saturates() {
let b = DiskStats {
reads: 10,
read_sectors: 20,
writes: 5,
write_sectors: 8,
};
let a = DiskStats {
reads: 13,
read_sectors: 21,
writes: 5,
write_sectors: 10,
};
let d = disk_delta("test", &b, &a);
assert_eq!(d.reads, 3);
assert_eq!(d.write_sectors, 2);
assert_eq!(d.written_bytes(), 1024);
assert_eq!(d.read_bytes(), 512);
}
#[test]
fn mount_of_returns_something() {
let (dev, fstype) = mount_of(Path::new("/"));
assert!(dev.starts_with("/dev/") || dev == "/" || !dev.is_empty());
assert!(!fstype.is_empty());
}
}