blob: 7a01b2678142615e42fbc014df7d863d3fb92ff4 [file] [log] [blame]
// 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 std::arch::x86_64::{CpuidResult, __cpuid, __cpuid_count};
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::sync::Mutex;
use anyhow::{bail, Context, Result};
use glob::glob;
use libchromeos::sys::info;
use once_cell::sync::Lazy;
use crate::common;
use crate::cpu_utils;
#[cfg(feature = "chromeos")]
use featured::CheckFeature; // Trait CheckFeature is for is_feature_enabled_blocking
#[cfg(feature = "chromeos")]
use once_cell::sync::OnceCell;
const MEDIA_MIN_ECORE_NUM: u32 = 4;
static MEDIA_DYNAMIC_CGROUP_ACTIVE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
// List of sysfs, resourced updates during media dynamic cgroup.
const CGROUP_CPUSET_ALL: [&str; 4] = [
"sys/fs/cgroup/cpuset/chrome/urgent/cpus",
"sys/fs/cgroup/cpuset/chrome/non-urgent/cpus",
"sys/fs/cgroup/cpuset/chrome/cpus",
"sys/fs/cgroup/cpuset/user_space/media/cpus",
];
// List of sysfs, which has no constraint (i.e allowed to use all cpus) at boot.
const CGROUP_CPUSET_NO_LIMIT: [&str; 3] = [
"sys/fs/cgroup/cpuset/chrome/urgent/cpus",
"sys/fs/cgroup/cpuset/chrome/cpus",
"sys/fs/cgroup/cpuset/user_space/media/cpus",
];
// ChromeOS limits non-urgent chrome tasks to use only power efficient cores at boot.
const CGROUP_CPUSET_NONURGENT: &str = "sys/fs/cgroup/cpuset/chrome/non-urgent/cpus";
const SCHEDULER_NONURGENT_PATH: &str = "run/chromeos-config/v1/scheduler-tune/cpuset-nonurgent";
// Protects Feature access with a mutex.
#[cfg(feature = "chromeos")]
struct FeatureWrapper {
feature: Mutex<featured::Feature>,
}
// FeatureWrapper is thread safe because its content is protected by a Mutex.
#[cfg(feature = "chromeos")]
unsafe impl Send for FeatureWrapper {}
#[cfg(feature = "chromeos")]
unsafe impl Sync for FeatureWrapper {}
#[cfg(feature = "chromeos")]
static CGROUP_FEATURE: OnceCell<FeatureWrapper> = OnceCell::new();
#[derive(PartialEq, Eq)]
pub enum MediaDynamicCgroupAction {
Start,
Stop,
}
pub fn init() -> Result<()> {
#[cfg(feature = "chromeos")]
{
const FEATURE_MEDIA_DYNAMIC_CGROUP: &str = "CrOSLateBootMediaDynamicCgroup";
let feature = featured::Feature::new(FEATURE_MEDIA_DYNAMIC_CGROUP, false)?;
if CGROUP_FEATURE
.set(FeatureWrapper {
feature: Mutex::new(feature),
})
.is_err()
{
bail!("Failed to set CGROUP_FEATURE");
}
}
Ok(())
}
fn is_dynamic_cgroup_enabled() -> Result<bool> {
#[cfg(feature = "chromeos")]
{
let feature_wrapper = CGROUP_FEATURE
.get()
.context("CGROUP_FEATURE is not initialized")?;
if let Ok(feature) = feature_wrapper.feature.lock() {
Ok(featured::PlatformFeatures::get()?.is_feature_enabled_blocking(&feature))
} else {
bail!("Failed to lock CGROUP_FEATURE");
}
}
#[cfg(not(feature = "chromeos"))]
Ok(false)
}
fn write_cpusets(root: &Path, cpus: &str) -> Result<()> {
for sysfs_path in CGROUP_CPUSET_ALL.iter() {
std::fs::write(root.join(sysfs_path), cpus).with_context(|| {
format!(
"Error writing to path: {}, new value: {}",
root.join(sysfs_path).display(),
cpus
)
})?;
}
Ok(())
}
fn get_scheduler_tune_cpuset_nonurgent(root: &Path) -> Result<Option<String>> {
let scheduler_tune_path = root.join(SCHEDULER_NONURGENT_PATH);
if !scheduler_tune_path.exists() {
return Ok(None);
}
Ok(Some(std::fs::read_to_string(scheduler_tune_path)?))
}
fn write_default_nonurgent_cpusets(root: &Path) -> Result<()> {
let cpuset_path = root.join(CGROUP_CPUSET_NONURGENT);
match get_scheduler_tune_cpuset_nonurgent(root) {
Ok(Some(cpusets)) => {
std::fs::write(cpuset_path, cpusets)?;
}
Ok(None) => {
std::fs::write(cpuset_path, cpu_utils::get_little_cores(root)?)?;
}
Err(e) => {
std::fs::write(cpuset_path, cpu_utils::get_cpuset_all_cpus(root)?)?;
bail!("Failed to get scheduler-tune cpuset-nonurgent, {}", e);
}
}
Ok(())
}
// Write cpuset/*/cpus values according to the default values in ui-pre-start [1].
// [1]: https://source.corp.google.com/chromeos_public/src/platform2/login_manager/init/scripts/ui-pre-start;rcl=5505d08e00b5c3973df4eab239142d4d2f2d0e4f;l=160
fn write_default_cpusets(root: &Path) -> Result<()> {
// non-urgent cpuset
write_default_nonurgent_cpusets(root)?;
// Other cpusets
let all_cpus = cpu_utils::get_cpuset_all_cpus(root)?;
for cpus in CGROUP_CPUSET_NO_LIMIT {
let cpus_path = root.join(cpus);
std::fs::write(cpus_path, all_cpus.as_bytes())?;
}
Ok(())
}
fn is_intel_platform_with_extended_features() -> Result<bool> {
// cpuid with EAX=0: Highest Function Parameter and Manufacturer ID.
// This returns the CPU's manufacturer ID string.
// The largest value that EAX can be set to before calling CPUID is returned in EAX.
// https://en.wikipedia.org/wiki/CPUID.
const CPUID_EAX_FOR_HFP_MID: u32 = 0;
// Intel processor manufacture ID is "GenuineIntel".
const CPUID_GENUINE_INTEL_EBX: u32 = 0x756e6547;
const CPUID_GENUINE_INTEL_ECX: u32 = 0x6c65746e;
const CPUID_GENUINE_INTEL_EDX: u32 = 0x49656e69;
// cpuid with EAX=7 and ECX=0: Extended Features.
const CPUID_EAX_EXT_FEATURE: u32 = 7;
// Check manufacturer ID is "GenuineIntel" and the highest function supports extended features.
match unsafe { __cpuid(CPUID_EAX_FOR_HFP_MID) } {
CpuidResult {
eax,
ebx: CPUID_GENUINE_INTEL_EBX,
ecx: CPUID_GENUINE_INTEL_ECX,
edx: CPUID_GENUINE_INTEL_EDX,
} if eax >= CPUID_EAX_EXT_FEATURE => Ok(true),
_ => Ok(false),
}
}
// Check Intel hybrid platform.
fn is_intel_hybrid_platform() -> Result<bool> {
if !is_intel_platform_with_extended_features()? {
return Ok(false);
}
// cpuid with EAX=7 and ECX=0: Extended Features.
// 15th bit in EDX tells platform has hybrid architecture or not.
const CPUID_EAX_EXT_FEATURE: u32 = 7;
const CPUID_ECX_EXT_FEATURE: u32 = 0;
const CPUID_EDX_HYBRID_SHIFT: u32 = 15;
// Read hybrid information.
let hybrid_info = unsafe { __cpuid_count(CPUID_EAX_EXT_FEATURE, CPUID_ECX_EXT_FEATURE) };
// Check system has Intel hybrid architecture.
Ok(hybrid_info.edx & (1 << CPUID_EDX_HYBRID_SHIFT) > 0)
}
// Return Intel hybrid platform total number of core and ecore.
fn get_intel_hybrid_core_num(root: &Path) -> Result<(u32, u32)> {
let sysfs_path = root
.join("sys/devices/system/cpu/cpufreq/policy*/cpuinfo_max_freq")
.to_str()
.context("Failed to construct cpuinfo_max_freq glob string")?
.to_owned();
// Frequency of P-core.
// Intel platform policy0(cpu0) would be always P-core.
let pcore_freq = common::read_file_to_u64(
root.join("sys/devices/system/cpu/cpufreq/policy0/cpuinfo_max_freq"),
)? as u32;
let core_freq_vec = glob(&sysfs_path)?
.map(|core_freq_path| Ok(common::read_file_to_u64(core_freq_path?)? as u32))
.collect::<Result<Vec<u32>>>()?;
// Total number of available cpus
let total_core_num = core_freq_vec.len() as u32;
// Toal number of E-core
let total_ecore_num = core_freq_vec
.iter()
.filter(|core_freq| core_freq < &&pcore_freq)
.count() as u32;
Ok((total_core_num, total_ecore_num))
}
// Return cpulist (cpus) for Media Dynamic Cgroup feature.
fn get_media_dynamic_cgroup_cpuset_cpus(root: &Path) -> Result<String> {
let (total_cpu_num, ecore_cpu_num) = get_intel_hybrid_core_num(root)?;
// Set cpuset to first 4 E-Core CPUs.
// e.g. Intel ADL-P-282, cpuset_head=4, cpuset_tail=7
let cpuset_head = total_cpu_num - ecore_cpu_num;
let cpuset_tail = cpuset_head + MEDIA_MIN_ECORE_NUM - 1;
// Compose new cpuset for media dynamic cgroup.
Ok((cpuset_head).to_string() + "-" + &(cpuset_tail.to_string()))
}
// In order to use media dynamic cgroup, followings are required.
// Feature has to be turned on.
// Intel Hybird platform + number of e-core > 4
fn platform_feature_media_dynamic_cgroup_enabled(root: &Path) -> Result<bool> {
if !is_intel_hybrid_platform()? {
return Ok(false);
}
let (_total_cpu_num, ecore_cpu_num) =
get_intel_hybrid_core_num(root).context("Failed to get core numbers")?;
Ok(ecore_cpu_num > MEDIA_MIN_ECORE_NUM)
}
// Extracts the loadavg parsing function for unittest.
fn parse_loadavg_1min<R: BufRead>(reader: R) -> Result<f64> {
let first_line = reader.lines().next().context("No content in buffer")??;
let error_context =
|| -> String { format!("Couldn't parse /proc/loadavg content: \"{}\"", first_line) };
first_line
.split_ascii_whitespace()
.next()
.with_context(error_context)?
.parse()
.with_context(error_context)
}
// Returns the load of the last 1 minute in the /proc/loadavg.
// Example /proc/loadavg content: "0.08 0.06 0.07 1/532 5515".
// See also loadavg in https://www.kernel.org/doc/html/latest/filesystems/proc.html
fn get_loadavg_1min() -> Result<f64> {
parse_loadavg_1min(BufReader::new(std::fs::File::open("/proc/loadavg")?))
}
fn media_dynamic_cgroup_impl(action: MediaDynamicCgroupAction, root: &Path) -> Result<()> {
let (new_active, recent_system_load) = if action == MediaDynamicCgroupAction::Start {
const MEDIA_MAX_SYSTEM_LOAD: f64 = (MEDIA_MIN_ECORE_NUM + 1) as f64;
let system_load = get_loadavg_1min()?;
(system_load < MEDIA_MAX_SYSTEM_LOAD, system_load)
} else {
(false, f64::NAN)
};
if let Ok(mut active) = MEDIA_DYNAMIC_CGROUP_ACTIVE.lock() {
if *active != new_active {
if new_active {
info!(
"system load is reasonable: {}, so start media dynamic cgroup",
recent_system_load
);
// Write platform cpuset to media dynamic cgroup cpuset.
let media_cpus = get_media_dynamic_cgroup_cpuset_cpus(root)?;
write_cpusets(root, &media_cpus).context("Failed to update dynamic cgropu cpus")?;
} else {
if !recent_system_load.is_nan() {
info!(
"system load is high: {}, stop media dynamic cgroup",
recent_system_load
);
} else {
info!("stop media dynamic cgroup");
}
write_default_cpusets(root).context("Failed to restore cpuset")?;
}
*active = new_active;
}
} else {
bail!("Failed to lock MEDIA_DYNAMIC_CGROUP_ACTIVE");
}
Ok(())
}
pub fn media_dynamic_cgroup(action: MediaDynamicCgroupAction) -> Result<()> {
// Check whether CrOS supports media dynamic cgroup for power saving.
if is_dynamic_cgroup_enabled()? {
let root = Path::new("/");
// Check whether platform supports media dynamic cgroup.
if platform_feature_media_dynamic_cgroup_enabled(root)? {
media_dynamic_cgroup_impl(action, root)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::test_utils::tests::*;
use std::path::PathBuf;
use tempfile::TempDir;
use super::*;
#[test]
fn test_power_is_intel_hybrid_system() -> Result<()> {
// cpuid with EAX=0: Highest Function Parameter and Manufacturer ID.
// This returns the CPU's manufacturer ID string.
// The largest value that EAX can be set to before calling CPUID is returned in EAX.
// https://en.wikipedia.org/wiki/CPUID.
const CPUID_EAX_FOR_HFP_MID: u32 = 0;
// Intel processor manufacture ID is "GenuineIntel".
const CPUID_GENUINE_INTEL_EBX: u32 = 0x756e6547;
const CPUID_GENUINE_INTEL_ECX: u32 = 0x6c65746e;
const CPUID_GENUINE_INTEL_EDX: u32 = 0x49656e69;
const CPUID_EAX_EXT_FEATURE: u32 = 7;
// Check system has Intel platform i.e "GenuineIntel" and the highest function.
let (intel_platform, highest_feature) = match unsafe { __cpuid(CPUID_EAX_FOR_HFP_MID) } {
CpuidResult {
eax,
ebx: CPUID_GENUINE_INTEL_EBX,
ecx: CPUID_GENUINE_INTEL_ECX,
edx: CPUID_GENUINE_INTEL_EDX,
} => {
println!("Intel platform with highest function: {}", eax);
(true, eax)
}
_ => (false, 0),
};
let intel_hybrid_platform = is_intel_hybrid_platform()?;
// If system is not Intel platform, hybrid should be false.
if !intel_platform {
assert!(!intel_hybrid_platform);
}
// If system is Intel platform but if the highest function is less than 7
// hybrid should be false.
if intel_platform && highest_feature < CPUID_EAX_EXT_FEATURE {
assert!(!intel_hybrid_platform);
}
println!(
"Does platform support Intel hybrid feature? {}",
intel_hybrid_platform
);
Ok(())
}
#[test]
fn test_power_platform_feature_media_dynamic_cgroup_enabled() -> Result<()> {
let platform_media_dynamic_cgroup =
platform_feature_media_dynamic_cgroup_enabled(&PathBuf::from("/"));
assert!(platform_media_dynamic_cgroup.is_ok());
println!(
"Does platform support media dynamic cgroup? {}",
platform_media_dynamic_cgroup.unwrap()
);
Ok(())
}
#[test]
fn test_power_get_intel_hybrid_core_num() -> Result<()> {
let root = TempDir::new().unwrap();
// Create fake sysfs ../cpufreq/policy*/cpuinfo_max_freq.
// Start with platform with 4 ISO cores.
for cpu in 0..4 {
// Create fake sysfs ../cpufreq/policy*/cpufino_max_freq.
let max_freq_path = root.path().join(format!(
"sys/devices/system/cpu/cpufreq/policy{}/cpuinfo_max_freq",
cpu
));
test_create_parent_dir(&max_freq_path);
std::fs::write(max_freq_path, "6000")?;
}
// Check (total_core_num, total_ecore_num).
let core_num = get_intel_hybrid_core_num(root.path()).unwrap();
assert_eq!(core_num, (4, 0));
// Add fake 8 e-cores sysfs.
for cpu in 4..12 {
// Create fake sysfs ../cpufreq/policy*/cpufino_max_freq.
let max_freq_path = root.path().join(format!(
"sys/devices/system/cpu/cpufreq/policy{}/cpuinfo_max_freq",
cpu
));
test_create_parent_dir(&max_freq_path);
std::fs::write(max_freq_path, "4000")?;
}
// Check (total_core_num, total_ecore_num).
let core_num = get_intel_hybrid_core_num(root.path()).unwrap();
assert_eq!(core_num, (12, 8));
Ok(())
}
fn test_write_cpusets(root: &Path, cpus_content: &str) {
for cpus in CGROUP_CPUSET_ALL.iter() {
let cpuset_cpus = root.join(cpus);
test_create_parent_dir(&cpuset_cpus);
std::fs::write(cpuset_cpus, cpus_content).unwrap();
}
}
fn test_write_scheduler_tune_cpuset_nonurgent(root: &Path, nonurgent_cpus: &str) {
let scheduler_tune_nonurgent_path =
root.join("run/chromeos-config/v1/scheduler-tune/cpuset-nonurgent");
test_create_parent_dir(&scheduler_tune_nonurgent_path);
std::fs::write(scheduler_tune_nonurgent_path, nonurgent_cpus).unwrap();
}
fn test_check_file_content(path: &Path, content: &str) {
let file_content = std::fs::read_to_string(path).unwrap();
assert_eq!(file_content, content);
}
fn test_write_cpu_capacity(root: &Path, cpu_num: u32, capacity: u32) {
let cpu_cap_path = root.join(format!("sys/bus/cpu/devices/cpu{}/cpu_capacity", cpu_num));
test_create_parent_dir(&cpu_cap_path);
std::fs::write(cpu_cap_path, capacity.to_string()).unwrap();
}
#[test]
fn test_write_default_cpusets_with_scheduler_tune() -> Result<()> {
// Setup.
let root = TempDir::new().unwrap();
test_write_cpuset_root_cpus(root.path(), "0-7");
test_write_cpusets(root.path(), "0-1"); // Init cpus.
test_write_scheduler_tune_cpuset_nonurgent(root.path(), "0-5");
// Call function to test.
write_default_cpusets(root.path())?;
// Check result.
for cpuset_path in CGROUP_CPUSET_NO_LIMIT.iter() {
let path = root.path().join(cpuset_path);
test_check_file_content(&path, "0-7");
}
test_check_file_content(&root.path().join(SCHEDULER_NONURGENT_PATH), "0-5");
Ok(())
}
#[test]
fn test_write_default_cpusets_without_little_cores() -> Result<()> {
// Setup.
let root = TempDir::new().unwrap();
test_write_cpuset_root_cpus(root.path(), "0-7");
test_write_cpusets(root.path(), "0-1"); // Init cpus.
test_write_ui_use_flags(root.path(), "");
// Call function to test.
write_default_cpusets(root.path())?;
// Check result.
for cpuset_path in CGROUP_CPUSET_NO_LIMIT.iter() {
let path = root.path().join(cpuset_path);
test_check_file_content(&path, "0-7");
}
test_check_file_content(&root.path().join(CGROUP_CPUSET_NONURGENT), "0-7");
Ok(())
}
#[test]
fn test_write_default_cpusets_with_little_cores_capacity() -> Result<()> {
// Setup.
let root = TempDir::new().unwrap();
test_write_cpuset_root_cpus(root.path(), "0-7");
test_write_cpusets(root.path(), "0-1"); // Init cpus.
test_write_ui_use_flags(root.path(), "big_little");
for i in 0..6 {
test_write_cpu_capacity(root.path(), i, 512);
}
for i in 6..8 {
test_write_cpu_capacity(root.path(), i, 1024);
}
// Call function to test.
write_default_cpusets(root.path())?;
// Check result.
for cpuset_path in CGROUP_CPUSET_NO_LIMIT.iter() {
let path = root.path().join(cpuset_path);
test_check_file_content(&path, "0-7");
}
// In the sysfs, the content would be converted to "0-5". But there is no auto conversion
// in the test temp files.
test_check_file_content(&root.path().join(CGROUP_CPUSET_NONURGENT), "0,1,2,3,4,5");
Ok(())
}
#[test]
fn test_write_default_cpusets_with_little_cores_max_freq() -> Result<()> {
// Setup.
let root = TempDir::new().unwrap();
test_write_cpuset_root_cpus(root.path(), "0-11");
test_write_cpusets(root.path(), "0-1"); // Init cpus.
test_write_ui_use_flags(root.path(), "big_little");
for i in 0..8 {
test_write_cpu_max_freq(root.path(), i, 1800000);
}
for i in 8..12 {
test_write_cpu_max_freq(root.path(), i, 2400000);
}
// Call function to test.
write_default_cpusets(root.path())?;
// Check result.
for cpuset_path in CGROUP_CPUSET_NO_LIMIT.iter() {
let path = root.path().join(cpuset_path);
test_check_file_content(&path, "0-11");
}
test_check_file_content(
&root.path().join(CGROUP_CPUSET_NONURGENT),
"0,1,2,3,4,5,6,7",
);
Ok(())
}
#[test]
fn test_parse_loadavg_1min() {
assert_eq!(
parse_loadavg_1min("0.08 0.06 0.07 1/532 5515\n".to_string().as_bytes()).unwrap(),
0.08
);
}
}