use anyhow::{Context, Result};
use std::{collections::HashMap, fs::read_to_string, io::ErrorKind::NotFound, path::Path};
#[derive(Debug)]
pub struct CpuInfo {
pub platform: String,
pub cores_count: usize,
pub cpu_type: String,
pub model: String,
pub model_number: String,
pub model_version: String,
pub model_revision: String,
pub cache: Option<u64>,
pub bogomips: Option<u64>,
pub speed: u64,
}
impl CpuInfo {
pub fn from_file(cpuinfo_filepath: &Path) -> Result<CpuInfo> {
let mut attributes: HashMap<&str, &str> = HashMap::new();
let mut cores_count = 0;
let raw_cpuinfo = read_to_string(cpuinfo_filepath).with_context(|| {
format!("cannot read CPU info file {:?}", cpuinfo_filepath.display())
})?;
for line in raw_cpuinfo.lines() {
let trimmed_line = line.trim();
if trimmed_line.is_empty() {
continue;
}
if let Some((key, value)) = trimmed_line.split_once(':') {
let key = key.trim_end();
if key == "processor" {
cores_count += 1;
}
attributes.insert(key, value.trim_start());
}
}
let arch = std::env::consts::ARCH;
let speed_str = attributes.get("cpu MHz").copied();
let speed = speed_str
.map(CpuSpeed::try_from)
.transpose()?
.unwrap_or_default()
.get_m_hz_as_int();
let platform = arch.to_string();
let model = attributes
.get("Model")
.or_else(|| attributes.get("model name"))
.unwrap_or(&arch)
.to_string();
let cpu_type = attributes.get("vendor_id").unwrap_or(&arch).to_string();
let model_number = attributes
.remove("cpu family")
.unwrap_or_default()
.to_string();
let model_version = attributes.remove("model").unwrap_or_default().to_string();
let model_revision = attributes
.remove("stepping")
.unwrap_or_default()
.to_string();
let cache = attributes
.remove("cache size")
.map(parse_cache_size)
.transpose()?;
let bogomips = attributes
.remove("bogomips")
.map(parse_bogomips)
.transpose()?;
Ok(CpuInfo {
platform,
cores_count,
cpu_type,
model,
model_number,
model_version,
model_revision,
cache,
bogomips,
speed,
})
}
}
#[derive(Debug, Default)]
pub struct CpuFrequency {
m_hz: u64,
}
impl CpuFrequency {
pub fn from_k_hz_file(max_cpu_frequency_filepath: &Path) -> Result<Self> {
match read_to_string(max_cpu_frequency_filepath) {
Ok(raw_freq) => {
let k_hz: u64 = raw_freq.trim().parse()?;
Ok(Self::from_k_hz(k_hz))
}
Err(e) if e.kind() == NotFound => Ok(Self::from_m_hz(0)),
Err(e) => Err(e).with_context(|| {
format!(
"cannot read CPU frequency file: {:?}",
max_cpu_frequency_filepath.display()
)
}),
}
}
pub const fn from_k_hz(k_hz: u64) -> Self {
Self { m_hz: k_hz / 1000 } }
pub const fn from_m_hz(m_hz: u64) -> Self {
Self { m_hz }
}
pub const fn get_m_hz(&self) -> u64 {
self.m_hz
}
}
#[derive(Debug, Default)]
struct CpuSpeed {
m_hz: f64,
}
impl CpuSpeed {
fn get_m_hz_as_int(&self) -> u64 {
self.m_hz.round() as u64
}
}
impl TryFrom<&str> for CpuSpeed {
type Error = anyhow::Error;
fn try_from(raw: &str) -> Result<Self> {
Ok(CpuSpeed {
m_hz: raw.parse::<f64>()?,
})
}
}
fn parse_cache_size(cache_size: &str) -> Result<u64> {
Ok(cache_size
.strip_suffix(" KB")
.unwrap_or(cache_size)
.parse::<u64>()?)
}
fn parse_bogomips(bogomips: &str) -> Result<u64> {
let bogo_str = bogomips.replace(' ', "");
Ok(bogo_str.parse::<f64>().map(|b| b.round() as u64)?)
}
#[cfg(test)]
mod tests {
use super::{CpuFrequency, CpuInfo};
use crate::helpers::test_utils::get_test_filepath;
#[test]
fn test_parsing_cpuinfo() {
let cpuinfo = CpuInfo::from_file(&get_test_filepath("arm64/rpi4b8g/cpuinfo")).unwrap();
assert_eq!(cpuinfo.platform, std::env::consts::ARCH);
assert_eq!(cpuinfo.cores_count, 4);
assert_eq!(cpuinfo.cpu_type, std::env::consts::ARCH);
assert_eq!(cpuinfo.model, "Raspberry Pi 4 Model B Rev 1.4");
}
#[test]
fn test_read_max_cpu_frequency() {
let cpu_freq =
CpuFrequency::from_k_hz_file(&get_test_filepath("arm64/rpi4b8g/cpuinfo_max_freq"))
.unwrap();
assert_eq!(cpu_freq.m_hz, 600);
}
}