steamos-manager/src/power.rs
2024-08-13 20:16:14 -07:00

1063 lines
34 KiB
Rust

/*
* Copyright © 2023 Collabora Ltd.
* Copyright © 2024 Valve Software
*
* SPDX-License-Identifier: MIT
*/
use anyhow::{anyhow, bail, ensure, Result};
use lazy_static::lazy_static;
use regex::Regex;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use strum::{Display, EnumString};
use tokio::fs::{self, try_exists, File};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tracing::{error, warn};
use crate::hardware::is_deck;
use crate::{path, write_synced};
const GPU_HWMON_PREFIX: &str = "/sys/class/hwmon";
const GPU_HWMON_NAME: &str = "amdgpu";
const CPU_PREFIX: &str = "/sys/devices/system/cpu/cpufreq";
const CPU0_NAME: &str = "policy0";
const CPU_POLICY_NAME: &str = "policy";
const GPU_POWER_PROFILE_SUFFIX: &str = "device/pp_power_profile_mode";
const GPU_PERFORMANCE_LEVEL_SUFFIX: &str = "device/power_dpm_force_performance_level";
const GPU_CLOCKS_SUFFIX: &str = "device/pp_od_clk_voltage";
const GPU_CLOCK_LEVELS_SUFFIX: &str = "device/pp_dpm_sclk";
const CPU_SCALING_GOVERNOR_SUFFIX: &str = "scaling_governor";
const CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX: &str = "scaling_available_governors";
const TDP_LIMIT1: &str = "power1_cap";
const TDP_LIMIT2: &str = "power2_cap";
lazy_static! {
static ref GPU_POWER_PROFILE_REGEX: Regex =
Regex::new(r"^\s*(?<value>[0-9]+)\s+(?<name>[0-9A-Za-z_]+)(?<active>\*)?").unwrap();
static ref GPU_CLOCK_LEVELS_REGEX: Regex =
Regex::new(r"^\s*(?<index>[0-9]+): (?<value>[0-9]+)Mhz").unwrap();
}
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
#[strum(serialize_all = "snake_case")]
pub enum GPUPowerProfile {
// Currently firmware exposes these values, though
// deck doesn't support them yet
#[strum(serialize = "3d_full_screen")]
FullScreen = 1,
Video = 3,
VR = 4,
Compute = 5,
Custom = 6,
// Currently only capped and uncapped are supported on
// deck hardware/firmware. Add more later as needed
Capped = 8,
Uncapped = 9,
}
impl TryFrom<u32> for GPUPowerProfile {
type Error = &'static str;
fn try_from(v: u32) -> Result<Self, Self::Error> {
match v {
x if x == GPUPowerProfile::FullScreen as u32 => Ok(GPUPowerProfile::FullScreen),
x if x == GPUPowerProfile::Video as u32 => Ok(GPUPowerProfile::Video),
x if x == GPUPowerProfile::VR as u32 => Ok(GPUPowerProfile::VR),
x if x == GPUPowerProfile::Compute as u32 => Ok(GPUPowerProfile::Compute),
x if x == GPUPowerProfile::Custom as u32 => Ok(GPUPowerProfile::Custom),
x if x == GPUPowerProfile::Capped as u32 => Ok(GPUPowerProfile::Capped),
x if x == GPUPowerProfile::Uncapped as u32 => Ok(GPUPowerProfile::Uncapped),
_ => Err("No GPUPowerProfile for value"),
}
}
}
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
#[strum(serialize_all = "snake_case")]
pub enum GPUPerformanceLevel {
Auto,
Low,
High,
Manual,
ProfilePeak,
}
#[derive(Display, EnumString, Hash, Eq, PartialEq, Debug, Copy, Clone)]
#[strum(serialize_all = "lowercase")]
pub enum CPUScalingGovernor {
Conservative,
OnDemand,
UserSpace,
PowerSave,
Performance,
SchedUtil,
}
async fn read_gpu_sysfs_contents<S: AsRef<Path>>(suffix: S) -> Result<String> {
// Read a given suffix for the GPU
let base = find_hwmon().await?;
fs::read_to_string(base.join(suffix.as_ref()))
.await
.map_err(|message| anyhow!("Error opening sysfs file for reading {message}"))
}
async fn write_gpu_sysfs_contents<S: AsRef<Path>>(suffix: S, data: &[u8]) -> Result<()> {
let base = find_hwmon().await?;
write_synced(base.join(suffix), data)
.await
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))
}
async fn read_cpu_sysfs_contents<S: AsRef<Path>>(suffix: S) -> Result<String> {
let base = path(CPU_PREFIX).join(CPU0_NAME);
fs::read_to_string(base.join(suffix.as_ref()))
.await
.map_err(|message| anyhow!("Error opening sysfs file for reading {message}"))
}
async fn write_cpu_governor_sysfs_contents(contents: String) -> Result<()> {
// Iterate over all policyX paths
let mut dir = fs::read_dir(path(CPU_PREFIX)).await?;
let mut wrote_stuff = false;
loop {
let base = match dir.next_entry().await? {
Some(entry) => {
let file_name = entry
.file_name()
.into_string()
.map_err(|_| anyhow!("Unable to convert path to string"))?;
if !file_name.starts_with(CPU_POLICY_NAME) {
continue;
}
entry.path()
}
None => {
ensure!(
wrote_stuff,
"No data written, unable to find any policyX sysfs paths"
);
return Ok(());
}
};
// Write contents to each one
wrote_stuff = true;
write_synced(base.join(CPU_SCALING_GOVERNOR_SUFFIX), contents.as_bytes())
.await
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))?
}
}
pub(crate) async fn get_gpu_power_profile() -> Result<GPUPowerProfile> {
// check which profile is current and return if possible
let contents = read_gpu_sysfs_contents(GPU_POWER_PROFILE_SUFFIX).await?;
// NOTE: We don't filter based on is_deck here because the sysfs
// firmware support setting the value to no-op values.
let lines = contents.lines();
for line in lines {
let caps = GPU_POWER_PROFILE_REGEX.captures(line);
let caps = match caps {
Some(caps) => caps,
None => continue,
};
let name = &caps["name"].to_lowercase();
if caps.name("active").is_some() {
match GPUPowerProfile::from_str(name.as_str()) {
Ok(v) => {
return Ok(v);
}
Err(e) => bail!("Unable to parse value for GPU power profile: {e}"),
}
}
}
bail!("Unable to determine current GPU power profile");
}
pub(crate) async fn get_available_gpu_power_profiles() -> Result<Vec<(u32, String)>> {
let contents = read_gpu_sysfs_contents(GPU_POWER_PROFILE_SUFFIX).await?;
let deck = is_deck().await?;
let mut map = Vec::new();
let lines = contents.lines();
for line in lines {
let caps = GPU_POWER_PROFILE_REGEX.captures(line);
let caps = match caps {
Some(caps) => caps,
None => continue,
};
let value: u32 = caps["value"]
.parse()
.map_err(|message| anyhow!("Unable to parse value for GPU power profile: {message}"))?;
let name = &caps["name"];
if deck {
// Deck is designed to operate in one of the CAPPED or UNCAPPED power profiles,
// the other profiles aren't correctly tuned for the hardware.
if value == GPUPowerProfile::Capped as u32 || value == GPUPowerProfile::Uncapped as u32
{
map.push((value, name.to_string()));
} else {
// Got unsupported value, so don't include it
}
} else {
// Do basic validation to ensure our enum is up to date?
map.push((value, name.to_string()));
}
}
Ok(map)
}
pub(crate) async fn set_gpu_power_profile(value: GPUPowerProfile) -> Result<()> {
let profile = (value as u32).to_string();
write_gpu_sysfs_contents(GPU_POWER_PROFILE_SUFFIX, profile.as_bytes()).await
}
pub(crate) async fn get_available_gpu_performance_levels() -> Result<Vec<GPUPerformanceLevel>> {
let base = find_hwmon().await?;
if !try_exists(base.join(GPU_PERFORMANCE_LEVEL_SUFFIX)).await? {
Ok(Vec::new())
} else {
Ok(vec![
GPUPerformanceLevel::Auto,
GPUPerformanceLevel::Low,
GPUPerformanceLevel::High,
GPUPerformanceLevel::Manual,
GPUPerformanceLevel::ProfilePeak,
])
}
}
pub(crate) async fn get_gpu_performance_level() -> Result<GPUPerformanceLevel> {
let level = read_gpu_sysfs_contents(GPU_PERFORMANCE_LEVEL_SUFFIX).await?;
Ok(GPUPerformanceLevel::from_str(level.trim())?)
}
pub(crate) async fn set_gpu_performance_level(level: GPUPerformanceLevel) -> Result<()> {
let level: String = level.to_string();
write_gpu_sysfs_contents(GPU_PERFORMANCE_LEVEL_SUFFIX, level.as_bytes()).await
}
pub(crate) async fn get_available_cpu_scaling_governors() -> Result<Vec<CPUScalingGovernor>> {
let contents = read_cpu_sysfs_contents(CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX).await?;
// Get the list of supported governors from cpu0
let mut result = Vec::new();
let words = contents.split_whitespace();
for word in words {
match CPUScalingGovernor::from_str(word) {
Ok(governor) => result.push(governor),
Err(message) => warn!("Error parsing governor {message}"),
}
}
Ok(result)
}
pub(crate) async fn get_cpu_scaling_governor() -> Result<CPUScalingGovernor> {
// get the current governor from cpu0 (assume all others are the same)
let contents = read_cpu_sysfs_contents(CPU_SCALING_GOVERNOR_SUFFIX).await?;
let contents = contents.trim();
CPUScalingGovernor::from_str(contents).map_err(|message| {
anyhow!(
"Error converting CPU scaling governor sysfs file contents to enumeration: {message}"
)
})
}
pub(crate) async fn set_cpu_scaling_governor(governor: CPUScalingGovernor) -> Result<()> {
// Set the given governor on all cpus
let name = governor.to_string();
write_cpu_governor_sysfs_contents(name).await
}
pub(crate) async fn get_gpu_clocks_range() -> Result<(u32, u32)> {
let contents = read_gpu_sysfs_contents(GPU_CLOCK_LEVELS_SUFFIX).await?;
let lines = contents.lines();
let mut min = 1000000;
let mut max = 0;
for line in lines {
let caps = GPU_CLOCK_LEVELS_REGEX.captures(line);
let caps = match caps {
Some(caps) => caps,
None => continue,
};
let value: u32 = caps["value"]
.parse()
.map_err(|message| anyhow!("Unable to parse value for GPU power profile: {message}"))?;
if value < min {
min = value;
}
if value > max {
max = value;
}
}
ensure!(min <= max, "Could not read any clocks");
Ok((min, max))
}
pub(crate) async fn set_gpu_clocks(clocks: u32) -> Result<()> {
// Set GPU clocks to given value valid
// Only used when GPU Performance Level is manual, but write whenever called.
let base = find_hwmon().await?;
let mut myfile = File::create(base.join(GPU_CLOCKS_SUFFIX))
.await
.inspect_err(|message| error!("Error opening sysfs file for writing: {message}"))?;
let data = format!("s 0 {clocks}\n");
myfile
.write(data.as_bytes())
.await
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))?;
myfile.flush().await?;
let data = format!("s 1 {clocks}\n");
myfile
.write(data.as_bytes())
.await
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))?;
myfile.flush().await?;
myfile
.write("c\n".as_bytes())
.await
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))?;
myfile.flush().await?;
Ok(())
}
pub(crate) async fn get_gpu_clocks() -> Result<u32> {
let base = find_hwmon().await?;
let clocks_file = File::open(base.join(GPU_CLOCKS_SUFFIX)).await?;
let mut reader = BufReader::new(clocks_file);
loop {
let mut line = String::new();
if reader.read_line(&mut line).await? == 0 {
break;
}
if line != "OD_SCLK:\n" {
continue;
}
let mut line = String::new();
if reader.read_line(&mut line).await? == 0 {
break;
}
let mhz = match line.split_whitespace().nth(1) {
Some(mhz) if mhz.ends_with("Mhz") => mhz.trim_end_matches("Mhz"),
_ => break,
};
return Ok(mhz.parse()?);
}
Ok(0)
}
async fn find_hwmon() -> Result<PathBuf> {
let mut dir = fs::read_dir(path(GPU_HWMON_PREFIX)).await?;
loop {
let base = match dir.next_entry().await? {
Some(entry) => entry.path(),
None => bail!("hwmon not found"),
};
let file_name = base.join("name");
let name = fs::read_to_string(file_name.as_path())
.await?
.trim()
.to_string();
if name == GPU_HWMON_NAME {
return Ok(base);
}
}
}
pub(crate) async fn get_tdp_limit() -> Result<u32> {
let base = find_hwmon().await?;
let power1cap = fs::read_to_string(base.join(TDP_LIMIT1)).await?;
let power1cap: u32 = power1cap.trim_end().parse()?;
Ok(power1cap / 1000000)
}
pub(crate) async fn set_tdp_limit(limit: u32) -> Result<()> {
// Set TDP limit given if within range (3-15)
// Returns false on error or out of range
ensure!((3..=15).contains(&limit), "Invalid limit");
let data = format!("{limit}000000");
let base = find_hwmon().await?;
write_synced(base.join(TDP_LIMIT1), data.as_bytes())
.await
.inspect_err(|message| {
error!("Error opening sysfs power1_cap file for writing TDP limits {message}")
})?;
if let Ok(mut power2file) = File::create(base.join(TDP_LIMIT2)).await {
power2file
.write(data.as_bytes())
.await
.inspect_err(|message| error!("Error writing to power2_cap file: {message}"))?;
power2file.flush().await?;
}
Ok(())
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
use crate::hardware::test::fake_model;
use crate::hardware::HardwareVariant;
use crate::{enum_roundtrip, testing};
use anyhow::anyhow;
use tokio::fs::{create_dir_all, read_to_string, remove_dir, write};
pub async fn setup() -> Result<()> {
// Use hwmon5 just as a test. We needed a subfolder of GPU_HWMON_PREFIX
// and this is as good as any.
let base = path(GPU_HWMON_PREFIX).join("hwmon5");
let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX);
// Creates hwmon path, including device subpath
create_dir_all(filename.parent().unwrap()).await?;
// Writes name file as addgpu so find_hwmon() will find it.
write_synced(base.join("name"), GPU_HWMON_NAME.as_bytes()).await?;
Ok(())
}
pub async fn create_nodes() -> Result<()> {
setup().await?;
let base = find_hwmon().await?;
let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX);
write(filename.as_path(), "auto\n").await?;
let filename = base.join(GPU_POWER_PROFILE_SUFFIX);
let contents = " 1 3D_FULL_SCREEN
3 VIDEO*
4 VR
5 COMPUTE
6 CUSTOM
8 CAPPED
9 UNCAPPED";
write(filename.as_path(), contents).await?;
let filename = base.join(TDP_LIMIT1);
write(filename.as_path(), "15000000\n").await?;
Ok(())
}
pub async fn write_clocks(mhz: u32) {
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_CLOCKS_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
let contents = format!(
"OD_SCLK:
0: {mhz}Mhz
1: {mhz}Mhz
OD_RANGE:
SCLK: 200Mhz 1600Mhz
CCLK: 1400Mhz 3500Mhz
CCLK_RANGE in Core0:
0: 1400Mhz
1: 3500Mhz\n"
);
write(filename.as_path(), contents).await.expect("write");
}
pub async fn read_clocks() -> Result<String, std::io::Error> {
let base = find_hwmon().await.unwrap();
read_to_string(base.join(GPU_CLOCKS_SUFFIX)).await
}
pub fn format_clocks(mhz: u32) -> String {
format!("s 0 {mhz}\ns 1 {mhz}\nc\n")
}
#[tokio::test]
async fn test_get_gpu_performance_level() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX);
assert!(get_gpu_performance_level().await.is_err());
write(filename.as_path(), "auto\n").await.expect("write");
assert_eq!(
get_gpu_performance_level().await.unwrap(),
GPUPerformanceLevel::Auto
);
write(filename.as_path(), "low\n").await.expect("write");
assert_eq!(
get_gpu_performance_level().await.unwrap(),
GPUPerformanceLevel::Low
);
write(filename.as_path(), "high\n").await.expect("write");
assert_eq!(
get_gpu_performance_level().await.unwrap(),
GPUPerformanceLevel::High
);
write(filename.as_path(), "manual\n").await.expect("write");
assert_eq!(
get_gpu_performance_level().await.unwrap(),
GPUPerformanceLevel::Manual
);
write(filename.as_path(), "profile_peak\n")
.await
.expect("write");
assert_eq!(
get_gpu_performance_level().await.unwrap(),
GPUPerformanceLevel::ProfilePeak
);
write(filename.as_path(), "nothing\n").await.expect("write");
assert!(get_gpu_performance_level().await.is_err());
}
#[tokio::test]
async fn test_set_gpu_performance_level() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_PERFORMANCE_LEVEL_SUFFIX);
set_gpu_performance_level(GPUPerformanceLevel::Auto)
.await
.expect("set");
assert_eq!(
read_to_string(filename.as_path()).await.unwrap().trim(),
"auto"
);
set_gpu_performance_level(GPUPerformanceLevel::Low)
.await
.expect("set");
assert_eq!(
read_to_string(filename.as_path()).await.unwrap().trim(),
"low"
);
set_gpu_performance_level(GPUPerformanceLevel::High)
.await
.expect("set");
assert_eq!(
read_to_string(filename.as_path()).await.unwrap().trim(),
"high"
);
set_gpu_performance_level(GPUPerformanceLevel::Manual)
.await
.expect("set");
assert_eq!(
read_to_string(filename.as_path()).await.unwrap().trim(),
"manual"
);
set_gpu_performance_level(GPUPerformanceLevel::ProfilePeak)
.await
.expect("set");
assert_eq!(
read_to_string(filename.as_path()).await.unwrap().trim(),
"profile_peak"
);
}
#[tokio::test]
async fn test_get_tdp_limit() {
let _h = testing::start();
setup().await.expect("setup");
let hwmon = path(GPU_HWMON_PREFIX);
assert!(get_tdp_limit().await.is_err());
write(hwmon.join("hwmon5").join(TDP_LIMIT1), "15000000\n")
.await
.expect("write");
assert_eq!(get_tdp_limit().await.unwrap(), 15);
}
#[tokio::test]
async fn test_set_tdp_limit() {
let _h = testing::start();
assert_eq!(
set_tdp_limit(2).await.unwrap_err().to_string(),
anyhow!("Invalid limit").to_string()
);
assert_eq!(
set_tdp_limit(20).await.unwrap_err().to_string(),
anyhow!("Invalid limit").to_string()
);
assert!(set_tdp_limit(10).await.is_err());
let hwmon = path(GPU_HWMON_PREFIX);
assert_eq!(
set_tdp_limit(10).await.unwrap_err().to_string(),
anyhow!("No such file or directory (os error 2)").to_string()
);
setup().await.expect("setup");
let hwmon = hwmon.join("hwmon5");
create_dir_all(hwmon.join(TDP_LIMIT1))
.await
.expect("create_dir_all");
create_dir_all(hwmon.join(TDP_LIMIT2))
.await
.expect("create_dir_all");
assert_eq!(
set_tdp_limit(10).await.unwrap_err().to_string(),
anyhow!("Is a directory (os error 21)").to_string()
);
remove_dir(hwmon.join(TDP_LIMIT1))
.await
.expect("remove_dir");
write(hwmon.join(TDP_LIMIT1), "0").await.expect("write");
assert!(set_tdp_limit(10).await.is_ok());
let power1_cap = read_to_string(hwmon.join(TDP_LIMIT1))
.await
.expect("power1_cap");
assert_eq!(power1_cap, "10000000");
remove_dir(hwmon.join(TDP_LIMIT2))
.await
.expect("remove_dir");
write(hwmon.join(TDP_LIMIT2), "0").await.expect("write");
assert!(set_tdp_limit(15).await.is_ok());
let power1_cap = read_to_string(hwmon.join(TDP_LIMIT1))
.await
.expect("power1_cap");
assert_eq!(power1_cap, "15000000");
let power2_cap = read_to_string(hwmon.join(TDP_LIMIT2))
.await
.expect("power2_cap");
assert_eq!(power2_cap, "15000000");
}
#[tokio::test]
async fn test_get_gpu_clocks() {
let _h = testing::start();
assert!(get_gpu_clocks().await.is_err());
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_CLOCKS_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
write(filename.as_path(), b"").await.expect("write");
assert_eq!(get_gpu_clocks().await.unwrap(), 0);
write_clocks(1600).await;
assert_eq!(get_gpu_clocks().await.unwrap(), 1600);
}
#[tokio::test]
async fn test_set_gpu_clocks() {
let _h = testing::start();
assert!(set_gpu_clocks(1600).await.is_err());
setup().await.expect("setup");
assert!(set_gpu_clocks(200).await.is_ok());
assert_eq!(read_clocks().await.unwrap(), format_clocks(200));
assert!(set_gpu_clocks(1600).await.is_ok());
assert_eq!(read_clocks().await.unwrap(), format_clocks(1600));
}
#[tokio::test]
async fn test_get_gpu_clocks_range() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_CLOCK_LEVELS_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
assert!(get_gpu_clocks_range().await.is_err());
write(filename.as_path(), &[] as &[u8; 0])
.await
.expect("write");
assert!(get_gpu_clocks_range().await.is_err());
let contents = "0: 200Mhz *
1: 1100Mhz
2: 1600Mhz";
write(filename.as_path(), contents).await.expect("write");
assert_eq!(get_gpu_clocks_range().await.unwrap(), (200, 1600));
let contents = "0: 1600Mhz *
1: 200Mhz
2: 1100Mhz";
write(filename.as_path(), contents).await.expect("write");
assert_eq!(get_gpu_clocks_range().await.unwrap(), (200, 1600));
}
#[test]
fn gpu_power_profile_roundtrip() {
enum_roundtrip!(GPUPowerProfile {
1: u32 = FullScreen,
3: u32 = Video,
4: u32 = VR,
5: u32 = Compute,
6: u32 = Custom,
8: u32 = Capped,
9: u32 = Uncapped,
"3d_full_screen": str = FullScreen,
"video": str = Video,
"vr": str = VR,
"compute": str = Compute,
"custom": str = Custom,
"capped": str = Capped,
"uncapped": str = Uncapped,
});
assert!(GPUPowerProfile::try_from(0).is_err());
assert!(GPUPowerProfile::try_from(2).is_err());
assert!(GPUPowerProfile::try_from(10).is_err());
assert!(GPUPowerProfile::from_str("fullscreen").is_err());
}
#[test]
fn cpu_governor_roundtrip() {
enum_roundtrip!(CPUScalingGovernor {
"conservative": str = Conservative,
"ondemand": str = OnDemand,
"userspace": str = UserSpace,
"powersave": str = PowerSave,
"performance": str = Performance,
"schedutil": str = SchedUtil,
});
assert!(CPUScalingGovernor::from_str("usersave").is_err());
}
#[test]
fn gpu_performance_level_roundtrip() {
enum_roundtrip!(GPUPerformanceLevel {
"auto": str = Auto,
"low": str = Low,
"high": str = High,
"manual": str = Manual,
"profile_peak": str = ProfilePeak,
});
assert!(GPUPerformanceLevel::from_str("peak_performance").is_err());
}
#[tokio::test]
async fn read_power_profiles() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_POWER_PROFILE_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
let contents = " 1 3D_FULL_SCREEN
3 VIDEO*
4 VR
5 COMPUTE
6 CUSTOM
8 CAPPED
9 UNCAPPED";
write(filename.as_path(), contents).await.expect("write");
fake_model(HardwareVariant::Unknown)
.await
.expect("fake_model");
let profiles = get_available_gpu_power_profiles().await.expect("get");
assert_eq!(
profiles,
&[
(
GPUPowerProfile::FullScreen as u32,
String::from("3D_FULL_SCREEN")
),
(GPUPowerProfile::Video as u32, String::from("VIDEO")),
(GPUPowerProfile::VR as u32, String::from("VR")),
(GPUPowerProfile::Compute as u32, String::from("COMPUTE")),
(GPUPowerProfile::Custom as u32, String::from("CUSTOM")),
(GPUPowerProfile::Capped as u32, String::from("CAPPED")),
(GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED"))
]
);
fake_model(HardwareVariant::Jupiter)
.await
.expect("fake_model");
let profiles = get_available_gpu_power_profiles().await.expect("get");
assert_eq!(
profiles,
&[
(GPUPowerProfile::Capped as u32, String::from("CAPPED")),
(GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED"))
]
);
}
#[tokio::test]
async fn read_unknown_power_profiles() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_POWER_PROFILE_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
let contents = " 1 3D_FULL_SCREEN
2 CGA
3 VIDEO*
4 VR
5 COMPUTE
6 CUSTOM
8 CAPPED
9 UNCAPPED";
write(filename.as_path(), contents).await.expect("write");
fake_model(HardwareVariant::Unknown)
.await
.expect("fake_model");
let profiles = get_available_gpu_power_profiles().await.expect("get");
assert_eq!(
profiles,
&[
(
GPUPowerProfile::FullScreen as u32,
String::from("3D_FULL_SCREEN")
),
(2, String::from("CGA")),
(GPUPowerProfile::Video as u32, String::from("VIDEO")),
(GPUPowerProfile::VR as u32, String::from("VR")),
(GPUPowerProfile::Compute as u32, String::from("COMPUTE")),
(GPUPowerProfile::Custom as u32, String::from("CUSTOM")),
(GPUPowerProfile::Capped as u32, String::from("CAPPED")),
(GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED"))
]
);
fake_model(HardwareVariant::Jupiter)
.await
.expect("fake_model");
let profiles = get_available_gpu_power_profiles().await.expect("get");
assert_eq!(
profiles,
&[
(GPUPowerProfile::Capped as u32, String::from("CAPPED")),
(GPUPowerProfile::Uncapped as u32, String::from("UNCAPPED"))
]
);
}
#[tokio::test]
async fn read_power_profile() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_POWER_PROFILE_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
let contents = " 1 3D_FULL_SCREEN
3 VIDEO*
4 VR
5 COMPUTE
6 CUSTOM
8 CAPPED
9 UNCAPPED";
write(filename.as_path(), contents).await.expect("write");
fake_model(HardwareVariant::Unknown)
.await
.expect("fake_model");
assert_eq!(
get_gpu_power_profile().await.expect("get"),
GPUPowerProfile::Video
);
fake_model(HardwareVariant::Jupiter)
.await
.expect("fake_model");
assert_eq!(
get_gpu_power_profile().await.expect("get"),
GPUPowerProfile::Video
);
}
#[tokio::test]
async fn read_no_power_profile() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_POWER_PROFILE_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
let contents = " 1 3D_FULL_SCREEN
3 VIDEO
4 VR
5 COMPUTE
6 CUSTOM
8 CAPPED
9 UNCAPPED";
write(filename.as_path(), contents).await.expect("write");
fake_model(HardwareVariant::Unknown)
.await
.expect("fake_model");
assert!(get_gpu_power_profile().await.is_err());
fake_model(HardwareVariant::Jupiter)
.await
.expect("fake_model");
assert!(get_gpu_power_profile().await.is_err());
}
#[tokio::test]
async fn read_unknown_power_profile() {
let _h = testing::start();
setup().await.expect("setup");
let base = find_hwmon().await.unwrap();
let filename = base.join(GPU_POWER_PROFILE_SUFFIX);
create_dir_all(filename.parent().unwrap())
.await
.expect("create_dir_all");
let contents = " 1 3D_FULL_SCREEN
2 CGA*
3 VIDEO
4 VR
5 COMPUTE
6 CUSTOM
8 CAPPED
9 UNCAPPED";
write(filename.as_path(), contents).await.expect("write");
fake_model(HardwareVariant::Unknown)
.await
.expect("fake_model");
assert!(get_gpu_power_profile().await.is_err());
fake_model(HardwareVariant::Jupiter)
.await
.expect("fake_model");
assert!(get_gpu_power_profile().await.is_err());
}
#[tokio::test]
async fn read_cpu_available_governors() {
let _h = testing::start();
let base = path(CPU_PREFIX).join(CPU0_NAME);
create_dir_all(&base).await.expect("create_dir_all");
let contents = "conservative ondemand userspace powersave performance schedutil";
write(base.join(CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX), contents)
.await
.expect("write");
assert_eq!(
get_available_cpu_scaling_governors().await.unwrap(),
vec![
CPUScalingGovernor::Conservative,
CPUScalingGovernor::OnDemand,
CPUScalingGovernor::UserSpace,
CPUScalingGovernor::PowerSave,
CPUScalingGovernor::Performance,
CPUScalingGovernor::SchedUtil
]
);
}
#[tokio::test]
async fn read_invalid_cpu_available_governors() {
let _h = testing::start();
let base = path(CPU_PREFIX).join(CPU0_NAME);
create_dir_all(&base).await.expect("create_dir_all");
let contents =
"conservative ondemand userspace rescascade powersave performance schedutil\n";
write(base.join(CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX), contents)
.await
.expect("write");
assert_eq!(
get_available_cpu_scaling_governors().await.unwrap(),
vec![
CPUScalingGovernor::Conservative,
CPUScalingGovernor::OnDemand,
CPUScalingGovernor::UserSpace,
CPUScalingGovernor::PowerSave,
CPUScalingGovernor::Performance,
CPUScalingGovernor::SchedUtil
]
);
}
#[tokio::test]
async fn read_cpu_governor() {
let _h = testing::start();
let base = path(CPU_PREFIX).join(CPU0_NAME);
create_dir_all(&base).await.expect("create_dir_all");
let contents = "ondemand\n";
write(base.join(CPU_SCALING_GOVERNOR_SUFFIX), contents)
.await
.expect("write");
assert_eq!(
get_cpu_scaling_governor().await.unwrap(),
CPUScalingGovernor::OnDemand
);
}
#[tokio::test]
async fn read_invalid_cpu_governor() {
let _h = testing::start();
let base = path(CPU_PREFIX).join(CPU0_NAME);
create_dir_all(&base).await.expect("create_dir_all");
let contents = "rescascade\n";
write(base.join(CPU_SCALING_GOVERNOR_SUFFIX), contents)
.await
.expect("write");
assert!(get_cpu_scaling_governor().await.is_err());
}
}