blob: 0273ba7483b57d254b7a05b0c408c087df41ecd8 [file] [log] [blame] [edit]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{bail, Result};
use glob::glob;
use libchromeos::sys::info;
use regex::Regex;
use std::path::{Path, PathBuf};
use std::str;
use crate::common;
/// Base path for power_limit relative to rootdir.
const DEVICE_POWER_LIMIT_PATH: &str = "sys/class/powercap/intel-rapl:0";
/// Base path for cpufreq relative to rootdir.
const DEVICE_CPUFREQ_PATH: &str = "sys/devices/system/cpu/cpufreq";
/// The threshold divsor for the minimum difference between min and max freq
const CPU_DIFF_THRESHOLD_DIVISOR: i32 = 5;
/// Utility class for controlling device CPU parameters.
/// To be used by resourced-nvpd communication and game mode power steering.
/// resourced-nvpd APIs documented in [`go/resourced-grpcs`](http://go/resourced-grpcs)
#[derive(Debug, Clone)]
pub struct DeviceCpuStatus {
power_limit_0_current_path: PathBuf,
power_limit_0_max: u64,
power_limit_1_current_path: PathBuf,
power_limit_1_max: u64,
energy_curr_path: PathBuf,
energy_max: u64,
cpu_max_freq_path_pattern: String,
cpu_min_freq_path_pattern: String,
cpu_max_freq_default: u64,
cpu_min_freq_default: u64,
// TODO: store static CpuInfo at object creation. Only update current_freq at runtime.
//cpu_info: Vec<CpuStaticInfo>
}
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct CpuStaticInfo {
pub core_num: i64,
pub base_freq_khz: i64,
pub min_freq_khz: i64,
pub max_freq_khz: i64,
}
#[allow(dead_code)]
impl DeviceCpuStatus {
#[inline(always)]
fn get_sysfs_val(&self, path_buf: &Path) -> Result<u64> {
common::read_file_to_u64(path_buf)
}
/// Getter for `power_limit_0` (long-term power limit).
///
/// Return the power_limit_0 value on supported device.
///
/// # Return
///
/// u64 representring power limit 0.
pub fn get_pl0_curr(&self) -> Result<u64> {
self.get_sysfs_val(&self.power_limit_0_current_path)
}
/// Getter for `power_limit_0_max` (maximum long-term power limit).
///
/// Return the power_limit_0_max value on supported device.
///
/// # Return
///
/// u64 representring pl0 max.
pub fn get_pl0_max(&self) -> Result<u64> {
Ok(self.power_limit_0_max)
}
/// Getter for `power_limit_1` (short-term power limit).
///
/// Return the power_limit_1 value on supported device.
///
/// # Return
///
/// u64 representring pl1.
pub fn get_pl1_curr(&self) -> Result<u64> {
self.get_sysfs_val(&self.power_limit_1_current_path)
}
/// Getter for `power_limit_1_max` (maximum long-term power limit).
///
/// Return the power_limit_1_max value on supported device.
///
/// # Return
///
/// u64 representring pl1 max.
pub fn get_pl1_max(&self) -> Result<u64> {
Ok(self.power_limit_1_max)
}
/// Getter for `energy_uj` (energy counter).
///
/// Return the energy_uj value on supported device.
///
/// # Return
///
/// u64 representring energy consumption since last counter reset.
pub fn get_energy_curr(&self) -> Result<u64> {
self.get_sysfs_val(&self.energy_curr_path)
}
/// Getter for `max_energy_range_uj` (energy counter max limit).
///
/// Return the max_energy_range_uj value on supported device.
///
/// # Return
///
/// u64 representring maximum energy consumption that can be stored before overflow.
pub fn get_energy_max(&self) -> Result<u64> {
Ok(self.energy_max)
}
/// Setter for `scaling_max_freq` (per-core max clock).
///
/// Sets the frequency ceiling for all cores.
/// TODO: assumes uniform cores, expand to set p-cores and e-cores to different frequencies.
///
/// # Arguments
///
/// * `val` - Value to set. If it is above max, system will set it to max. If it results in the
/// difference between current min and max to be less than the threshold then
/// the value will remain unchanged.
///
/// # Return
///
/// Result<()>
pub fn set_all_max_cpu_freq(&self, val_max: u64) -> Result<()> {
let threshold = (self.cpu_max_freq_default - self.cpu_min_freq_default)
/ (CPU_DIFF_THRESHOLD_DIVISOR as u64);
info!("Setting All CPU max freq");
let cpus: Result<Vec<_>, _> = glob(&self.cpu_max_freq_path_pattern)?.collect();
let cpus = cpus?;
for curr_cpu in cpus {
let cpu_min_path =
PathBuf::from(str::replace(&curr_cpu.display().to_string(), "max", "min"));
let val_min = common::read_file_to_u64(cpu_min_path)?;
if (val_max - val_min) > threshold {
std::fs::write(curr_cpu, val_max.to_string().as_bytes())?;
} else {
bail!("Requested frequency too close to min");
}
}
Ok(())
}
/// Setter for `scaling_min_freq` (per-core min clock).
///
/// Sets the frequency flooring for all cores.
/// TODO: assumes uniform cores, expand to set p-cores and e-cores to different frequencies.
///
/// # Arguments
///
/// * `val` - Value to set. If it is below min, system will set it to min.
/// If it results in the difference between current min and max to be
/// less than the threshold thenthe value will remain unchanged.
///
/// # Return
///
/// Result<()>
pub fn set_all_min_cpu_freq(&self, val_min: u64) -> Result<()> {
let threshold = (self.cpu_max_freq_default - self.cpu_min_freq_default)
/ (CPU_DIFF_THRESHOLD_DIVISOR as u64);
info!("Setting All CPU min freq");
let cpus: Result<Vec<_>, _> = glob(&self.cpu_min_freq_path_pattern)?.collect();
let cpus = cpus?;
for curr_cpu in cpus {
let cpu_max_path =
PathBuf::from(str::replace(&curr_cpu.display().to_string(), "min", "max"));
let val_max = common::read_file_to_u64(cpu_max_path)?;
if (val_max - val_min) > threshold {
std::fs::write(curr_cpu, val_min.to_string().as_bytes())?;
} else {
bail!("Requested frequency too close to max");
}
}
Ok(())
}
/// Reset all cores to system default max frequency.
///
/// Resets device to system default max frequency. If system isn't reset after modification,
/// max CPU freq will be locked/throttled until next reboot.
///
/// # Return
///
/// Result<()>
pub fn reset_all_max_min_cpu_freq(&self) -> Result<()> {
self.set_all_max_cpu_freq(self.cpu_max_freq_default)?;
self.set_all_min_cpu_freq(self.cpu_min_freq_default)?;
Ok(())
}
/// Returns the current CPU frequency oif the requested core.
///
/// # Arguments
///
/// * `root` - Relative path from which sysfs files are searches.
/// Should be `/` for non-test cases.
///
/// * `core_num` - core number as defined in sysfs.
///
/// # Return
///
/// Result<i64> - integer denoting current frequency in KHz.
pub fn get_core_curr_freq_khz(&self, root: PathBuf, core_num: i64) -> Result<i64> {
let root_pathbuf = root
.join(DEVICE_CPUFREQ_PATH)
.join(format!("policy{core_num}/"))
.join("scaling_cur_freq");
let cpu_cur_freq_path = root_pathbuf.as_path();
Ok(self.get_sysfs_val(cpu_cur_freq_path)? as i64)
}
/// Returns the CPU info for all available CPU cores.
///
/// # Arguments
///
/// * `root` - Relative path from which sysfs files are searches.
/// Should be `/` for non-test cases.
///
/// # Return
///
/// Result<Vec<CpuStaticInfo>> - CpuInfo for all cores (sorted by core_number).
pub fn get_static_cpu_info(&self, root: PathBuf) -> Result<Vec<CpuStaticInfo>> {
let mut res: Vec<CpuStaticInfo> = vec![];
let mut core_num: i64;
let cpu_policy_path = root
.join(DEVICE_CPUFREQ_PATH)
.join("policy*/")
.as_path()
.display()
.to_string();
if let Ok(core_paths) = glob(&cpu_policy_path) {
for core_path in core_paths.flatten() {
let policy_path = core_path.display().to_string();
let re = Regex::new(r".*cpufreq/policy(\d{1,2}).*")?;
if let Some(cap) = re.captures(&policy_path) {
core_num = cap
.get(1)
.map_or(0, |m| m.as_str().parse::<i64>().unwrap_or(0));
} else {
bail!("Couldn't not parse core info.");
}
res.push(CpuStaticInfo {
core_num,
base_freq_khz: common::read_file_to_u64(core_path.join("base_frequency"))?
.try_into()?,
max_freq_khz: common::read_file_to_u64(core_path.join("cpuinfo_max_freq"))?
.try_into()?,
min_freq_khz: common::read_file_to_u64(core_path.join("cpuinfo_min_freq"))?
.try_into()?,
});
}
} else {
bail!("Could not find CPU paths");
}
// Sort by core_number before returning.
res.sort_by(|a, b| a.core_num.cmp(&b.core_num));
Ok(res)
}
/// Create a new DeviceCpuStatus.
///
/// Constructor for new DeviceCpuStatus object. Will check if all associated sysfs path exists
/// and will return object if conditions met. Will return an error if not all sysfs paths are
/// found (i.e: unsupported device family, kernel version, etc.)
///
/// # Arguments
///
/// * `root` - root path relative to sysfs. Will normally be '/' unless unit testing.
///
/// # Return
///
/// New CpuDevice object with associated functionality.
pub fn new(root: PathBuf) -> Result<DeviceCpuStatus> {
info!("Creating CPU device structure");
let power_limit_0_current_path = root
.join(DEVICE_POWER_LIMIT_PATH)
.join("constraint_0_power_limit_uw");
let power_limit_0_max_path = root
.join(DEVICE_POWER_LIMIT_PATH)
.join("constraint_0_max_power_uw");
let power_limit_1_current_path = root
.join(DEVICE_POWER_LIMIT_PATH)
.join("constraint_1_power_limit_uw");
let power_limit_1_max_path = root
.join(DEVICE_POWER_LIMIT_PATH)
.join("constraint_1_max_power_uw");
let energy_curr_path = root.join(DEVICE_POWER_LIMIT_PATH).join("energy_uj");
let energy_max_path = root
.join(DEVICE_POWER_LIMIT_PATH)
.join("max_energy_range_uj");
let cpu_max_freq_path = root
.join(DEVICE_CPUFREQ_PATH)
.join("policy*/scaling_max_freq");
let cpu_max_freq_path_pattern = cpu_max_freq_path.to_str().unwrap_or_default();
let cpu_0_max_path = PathBuf::from(str::replace(cpu_max_freq_path_pattern, "*", "0"));
// always latch baseline max, since local max may have already been modified.
let cpu_min_freq_path = root
.join(DEVICE_CPUFREQ_PATH)
.join("policy*/scaling_min_freq");
let cpu_min_freq_path_pattern = cpu_min_freq_path.to_str().unwrap_or_default();
let cpu_0_min_path = PathBuf::from(str::replace(cpu_min_freq_path_pattern, "*", "0"));
// always latch baseline min, since local min may have already been modified.
if power_limit_0_current_path.exists()
&& power_limit_0_max_path.exists()
&& power_limit_1_current_path.exists()
&& power_limit_1_max_path.exists()
&& energy_curr_path.exists()
&& energy_max_path.exists()
&& cpu_0_max_path.exists()
&& cpu_0_min_path.exists()
&& CPU_DIFF_THRESHOLD_DIVISOR > 0
{
info!("All sysfs file paths found");
Ok(DeviceCpuStatus {
power_limit_0_current_path,
power_limit_0_max: common::read_file_to_u64(power_limit_0_max_path)?,
power_limit_1_current_path,
power_limit_1_max: common::read_file_to_u64(power_limit_1_max_path)?,
energy_curr_path,
energy_max: common::read_file_to_u64(energy_max_path)?,
cpu_max_freq_path_pattern: cpu_max_freq_path_pattern.to_owned(),
cpu_min_freq_path_pattern: cpu_min_freq_path_pattern.to_owned(),
// Todo: Change to vector for ADL heterogeneous cores.
cpu_max_freq_default: common::read_file_to_u64(cpu_0_max_path)?,
cpu_min_freq_default: common::read_file_to_u64(cpu_0_min_path)?,
})
} else {
info!(
"power_limit_0_current_path.exists() {}",
power_limit_0_current_path.exists()
);
info!(
"power_limit_0_max_path.exists() {}",
power_limit_0_max_path.exists()
);
info!(
"power_limit_1_current_path.exists() {}",
power_limit_1_current_path.exists()
);
info!(
"power_limit_1_max_path.exists() {}",
power_limit_1_max_path.exists()
);
info!("energy_curr_path.exists() {}", energy_curr_path.exists());
info!("energy_max_path.exists() {}", energy_max_path.exists());
info!(
"cpu_max_freq_path_pattern.exists() {} (only pattern 0 checked)",
cpu_0_max_path.exists()
);
info!(
"cpu_max_freq_default_path_pattern.exists() {} (only pattern 0 checked)",
cpu_max_freq_path.exists()
);
info!(
"cpu_min_freq_path_pattern.exists() {} (only pattern 0 checked)",
cpu_0_min_path.exists()
);
info!(
"cpu_min_freq_default_path_pattern.exists() {} (only pattern 0 checked)",
cpu_min_freq_path.exists()
);
info!(
"CPU_DIFF_THRESHOLD_DIVISOR == 0 {}",
CPU_DIFF_THRESHOLD_DIVISOR == 0
);
bail!("Could not find all sysfs files for CPU status")
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::test_utils::tests::*;
use anyhow::Result;
use tempfile::tempdir;
const MOCK_NUM_CPU: i32 = 16;
#[test]
fn test_sysfs_files_missing_gives_error() {
let root = tempdir().unwrap();
assert!(DeviceCpuStatus::new(PathBuf::from(root.path())).is_err());
}
#[test]
fn test_get_pl0() -> Result<()> {
let root = tempdir()?;
setup_mock_cpu_dev_dirs(root.path()).unwrap();
setup_mock_cpu_files(root.path()).unwrap();
write_mock_cpu(root.path(), 0, 3200000, 3000000, 400000, 1000000).unwrap();
write_mock_pl0(root.path(), 15000000).unwrap();
let mock_cpu_dev_res = DeviceCpuStatus::new(PathBuf::from(root.path()));
assert!(mock_cpu_dev_res.is_ok());
let mock_cpu_dev = mock_cpu_dev_res?;
assert_eq!(mock_cpu_dev.get_pl0_curr()?, 15000000);
Ok(())
}
#[test]
fn test_cpu_info_parsing() {
let root = tempdir().unwrap();
setup_mock_cpu_dev_dirs(root.path()).unwrap();
setup_mock_cpu_files(root.path()).unwrap();
let mock_cpu_dev = DeviceCpuStatus::new(PathBuf::from(root.path())).unwrap();
let cpu_info = mock_cpu_dev
.get_static_cpu_info(PathBuf::from(root.path()))
.unwrap();
// Test that cores are presorted
for i in 0..MOCK_NUM_CPU {
assert_eq!(cpu_info.get(i as usize).unwrap().core_num, i as i64);
}
// Test all cpu's were inserted with unique core_nums
assert_eq!(
cpu_info
.iter()
.map(|core| core.core_num)
.collect::<HashSet<i64>>()
.len() as i32,
MOCK_NUM_CPU
);
// Test that correct data and paths were picked up
let base_freqs = cpu_info
.iter()
.map(|core| core.base_freq_khz)
.collect::<HashSet<i64>>();
let min_freqs = cpu_info
.iter()
.map(|core| core.min_freq_khz)
.collect::<HashSet<i64>>();
let max_freqs = cpu_info
.iter()
.map(|core| core.max_freq_khz)
.collect::<HashSet<i64>>();
// Leave these extensible. len will be 2 for heterogeneous cores.
assert_eq!(base_freqs.len(), 1);
assert_eq!(*base_freqs.iter().next().unwrap(), 2100000);
assert_eq!(min_freqs.len(), 1);
assert_eq!(*min_freqs.iter().next().unwrap(), 400000);
assert_eq!(max_freqs.len(), 1);
assert_eq!(*max_freqs.iter().next().unwrap(), 4100000);
}
#[test]
fn test_cpu_read_write_reset() -> Result<()> {
let root = tempdir()?;
setup_mock_cpu_dev_dirs(root.path()).unwrap();
setup_mock_cpu_files(root.path()).unwrap();
for cpu in 0..MOCK_NUM_CPU {
write_mock_cpu(root.path(), cpu, 3200000, 3000000, 400000, 1000000).unwrap();
}
let mock_cpu_dev_res = DeviceCpuStatus::new(PathBuf::from(root.path()));
assert!(mock_cpu_dev_res.is_ok());
let mock_cpu_dev = mock_cpu_dev_res?;
assert_eq!(get_cpu0_freq_max(root.path()), 3000000);
mock_cpu_dev.set_all_max_cpu_freq(2000000)?;
assert_eq!(get_cpu0_freq_max(root.path()), 2000000);
mock_cpu_dev.set_all_min_cpu_freq(1200000)?;
assert_eq!(get_cpu0_freq_min(root.path()), 1200000);
mock_cpu_dev.reset_all_max_min_cpu_freq()?;
assert_eq!(get_cpu0_freq_max(root.path()), 3000000);
assert_eq!(get_cpu0_freq_min(root.path()), 1000000);
mock_cpu_dev.set_all_max_cpu_freq(1600000)?;
assert_eq!(get_cpu0_freq_max(root.path()), 1600000);
mock_cpu_dev.set_all_max_cpu_freq(2800000)?;
assert_eq!(get_cpu0_freq_min(root.path()), 1000000);
Ok(())
}
}