blob: f30b2a9224528ca5c39edf9bece75ee7c449c888 [file] [log] [blame] [edit]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// APIs to adjust the Quality of Service (QoS) expected for a thread or a
// process. QoS definitions map to performance characteristics.
pub mod cgroups;
mod mmap;
mod proc;
mod sched_attr;
mod storage;
#[cfg(test)]
mod test_utils;
use std::fmt::Display;
use std::io;
use std::path::Path;
pub use cgroups::CgroupContext;
pub use cgroups::CpuCgroup;
pub use cgroups::CpusetCgroup;
use proc::load_process_timestamp;
use proc::load_thread_timestamp;
use proc::ThreadChecker;
pub use sched_attr::compute_uclamp_min;
use sched_attr::SchedAttrContext;
use sched_attr::UCLAMP_BOOSTED_MIN;
pub use sched_attr::UCLAMP_MAX;
use storage::restorable::RestorableProcessMap;
use storage::simple::SimpleProcessMap;
use storage::ProcessContext;
use storage::ProcessMap;
use storage::ThreadMap;
pub type Result<T> = std::result::Result<T, Error>;
pub const NUM_PROCESS_STATES: usize = ProcessState::Background as usize + 1;
pub const NUM_THREAD_STATES: usize = ThreadState::UrgentBurstyServer as usize + 1;
/// Errors from schedqos crate.
#[derive(Debug)]
pub enum Error {
Config(&'static str, &'static str),
Cgroup(&'static str, io::Error),
SchedAttr(io::Error),
LatencySensitive(io::Error),
Proc(proc::Error),
Storage(storage::restorable::Error),
ProcessNotFound,
ProcessNotRegistered,
ThreadNotFound,
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Config(_, _) => None,
Self::Cgroup(_, e) => Some(e),
Self::SchedAttr(e) => Some(e),
Self::LatencySensitive(e) => Some(e),
Self::Proc(e) => Some(e),
Self::Storage(e) => Some(e),
Self::ProcessNotFound => None,
Self::ProcessNotRegistered => None,
Self::ThreadNotFound => None,
}
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Config(category, e) => f.write_fmt(format_args!("config: {category}: {e}")),
Self::Cgroup(name, e) => f.write_fmt(format_args!("cgroup: {name}: {e}")),
Self::SchedAttr(e) => f.write_fmt(format_args!("sched_setattr(2): {e}")),
Self::LatencySensitive(e) => f.write_fmt(format_args!("latency sensitive file: {e}")),
Self::Proc(e) => f.write_fmt(format_args!("procfs: {e}")),
Self::Storage(e) => f.write_fmt(format_args!("storage: {e}")),
Self::ProcessNotFound => f.write_str("process not found"),
Self::ProcessNotRegistered => f.write_str("process not registered"),
Self::ThreadNotFound => f.write_str("thread not found"),
}
}
}
impl From<proc::Error> for Error {
fn from(e: proc::Error) -> Self {
Self::Proc(e)
}
}
/// Scheduler QoS states of a process.
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ProcessState {
Normal = 0,
Background = 1,
}
impl TryFrom<u8> for ProcessState {
type Error = ();
fn try_from(v: u8) -> std::result::Result<Self, Self::Error> {
match v {
0 => Ok(Self::Normal),
1 => Ok(Self::Background),
_ => Err(()),
}
}
}
/// Scheduler QoS states of a thread.
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ThreadState {
UrgentBursty = 0,
Urgent = 1,
Balanced = 2,
Eco = 3,
Utility = 4,
Background = 5,
UrgentBurstyServer = 6,
}
impl TryFrom<u8> for ThreadState {
type Error = ();
fn try_from(v: u8) -> std::result::Result<Self, Self::Error> {
match v {
0 => Ok(Self::UrgentBursty),
1 => Ok(Self::Urgent),
2 => Ok(Self::Balanced),
3 => Ok(Self::Eco),
4 => Ok(Self::Utility),
5 => Ok(Self::Background),
6 => Ok(Self::UrgentBurstyServer),
_ => Err(()),
}
}
}
/// Config of each process/thread QoS state.
#[derive(Debug)]
pub struct Config {
/// Config for cgroups
pub cgroup_context: CgroupContext,
/// ProcessStateConfig for each process QoS state
pub process_configs: [ProcessStateConfig; NUM_PROCESS_STATES],
/// ThreadStateConfig for each thread QoS state
pub thread_configs: [ThreadStateConfig; NUM_THREAD_STATES],
}
impl Config {
pub const fn default_process_config() -> [ProcessStateConfig; NUM_PROCESS_STATES] {
[
// ProcessState::Normal
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Normal,
allow_rt: true,
allow_all_cores: true,
},
// Process:State::Background
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Background,
allow_rt: false,
allow_all_cores: false,
},
]
}
pub const fn default_thread_config() -> [ThreadStateConfig; NUM_THREAD_STATES] {
[
// ThreadState::UrgentBursty
ThreadStateConfig {
rt_priority: Some(8),
nice: -8,
uclamp_min: UCLAMP_BOOSTED_MIN,
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
},
// ThreadState::Urgent
ThreadStateConfig {
nice: -8,
uclamp_min: UCLAMP_BOOSTED_MIN,
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
..ThreadStateConfig::default()
},
// ThreadState::Balanced
ThreadStateConfig {
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
..ThreadStateConfig::default()
},
// ThreadState::Eco
ThreadStateConfig {
cpuset_cgroup: CpusetCgroup::Efficient,
..ThreadStateConfig::default()
},
// ThreadState::Utility
ThreadStateConfig {
nice: 1,
cpuset_cgroup: CpusetCgroup::Efficient,
..ThreadStateConfig::default()
},
// ThreadState::Background
ThreadStateConfig {
nice: 10,
cpuset_cgroup: CpusetCgroup::Efficient,
..ThreadStateConfig::default()
},
// ThreadState::UrgentBurstyServer
ThreadStateConfig {
rt_priority: Some(12),
nice: -12,
uclamp_min: UCLAMP_BOOSTED_MIN,
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
},
]
}
}
/// Detailed scheduler settings for a process QoS state.
#[derive(Clone, Debug)]
pub struct ProcessStateConfig {
/// The cpu cgroup
pub cpu_cgroup: CpuCgroup,
/// If RT is not allowed, threads with [ThreadStateConfig::rt_priority] use SCHED_OTHER.
pub allow_rt: bool,
/// If all core is not allowed, move all threads to the efficient cpuset cgroup.
pub allow_all_cores: bool,
}
/// Detailed scheduler settings for a thread QoS state.
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ThreadStateConfig {
/// The priority in RT (SCHED_FIFO). If this is None, it uses SCHED_OTHER instead.
pub rt_priority: Option<u32>,
/// The nice value
pub nice: i32,
/// sched_attr.sched_util_min
///
/// This must be smaller than or equal to 1024.
pub uclamp_min: u32,
/// The cpuset cgroup
pub cpuset_cgroup: CpusetCgroup,
/// Whether the thread is latency sensitive or not.
///
/// On systems that use EAS, EAS will try to pack workloads onto non-idle
/// cpus first as long as there is capacity. However, if an idle cpu was
/// chosen it would reduce the latency.
pub latency_sensitive: bool,
}
impl ThreadStateConfig {
fn validate(&self) -> std::result::Result<(), &'static str> {
if self.uclamp_min > UCLAMP_MAX {
return Err("uclamp_min is too big");
}
Ok(())
}
const fn default() -> Self {
ThreadStateConfig {
rt_priority: None,
nice: 0,
uclamp_min: 0,
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: false,
}
}
}
/// Wrap u32 PID with [ProcessId].
///
/// Using u32 for both process id and thread id is confusing in this library.
/// This is to prevent unexpected typo by the explicit typing.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ProcessId(u32);
impl From<u32> for ProcessId {
fn from(pid: u32) -> Self {
ProcessId(pid)
}
}
/// Wrap u32 TID with [ThreadId].
///
/// See [ProcessId] for the reason.
///
/// [std::thread::ThreadId] is not our use case because it is only for thread in
/// the running process and not expected for threads of other processes.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ThreadId(u32);
impl From<u32> for ThreadId {
fn from(tid: u32) -> Self {
ThreadId(tid)
}
}
pub struct ProcessKey {
process_id: ProcessId,
timestamp: u64,
}
pub type SimpleSchedQosContext = SchedQosContext<SimpleProcessMap>;
pub type RestorableSchedQosContext = SchedQosContext<RestorableProcessMap>;
pub struct SchedQosContext<PM: ProcessMap> {
config: Config,
sched_attr_context: SchedAttrContext,
process_map: PM,
}
impl SimpleSchedQosContext {
pub fn new_simple(config: Config) -> Result<Self> {
Self::new(config, SimpleProcessMap::new())
}
}
impl RestorableSchedQosContext {
pub fn new_file(config: Config, path: &Path) -> Result<Self> {
let storage = RestorableProcessMap::new(path).map_err(Error::Storage)?;
Self::new(config, storage)
}
pub fn load_from_file(config: Config, path: &Path) -> Result<Self> {
let storage = RestorableProcessMap::load(path).map_err(Error::Storage)?;
Self::new(config, storage)
}
}
impl<PM: ProcessMap> SchedQosContext<PM> {
fn new(config: Config, process_map: PM) -> Result<Self> {
for thread_config in &config.thread_configs {
thread_config
.validate()
.map_err(|e| Error::Config("thread validation", e))?;
}
Ok(Self {
config,
sched_attr_context: SchedAttrContext::new().map_err(Error::SchedAttr)?,
process_map,
})
}
pub fn set_process_state(
&mut self,
process_id: ProcessId,
process_state: ProcessState,
) -> Result<Option<ProcessKey>> {
let process_config = &self.config.process_configs[process_state as usize];
let timestamp = match load_process_timestamp(process_id) {
Err(proc::Error::NotFound) => {
self.process_map.remove_process(process_id, None);
self.process_map.compact();
return Err(Error::ProcessNotFound);
}
other => other?,
};
self.config
.cgroup_context
.set_cpu_cgroup(process_id, process_config.cpu_cgroup)
.map_err(|e| Error::Cgroup(process_config.cpu_cgroup.name(), e))?;
let Some(mut process) =
self.process_map
.insert_or_update(process_id, timestamp, process_state)
else {
// If process_id is the same but timestamp is different, the process is treated as a
// new process and clears old thread contexts inside.
self.process_map.compact();
return Ok(Some(ProcessKey {
process_id,
timestamp,
}));
};
// Cache the last error while updating thread settings. This will be returned as
// this method's error if process setting update succeeds. Errors while updating
// thread settings do not stop other setting updates.
let mut result = Ok(None);
// Only apply process state thread restrictions to managed threads. Although we
// could theoretically try to apply the restrictions to unmanaged threads as well,
// defining coherent state transitions and properly restoring state later would be
// overly complicated.
process.thread_map().retain_threads(|thread_id, thread| {
// If the thread is dead, remove the thread from the map.
match load_thread_timestamp(process_id, *thread_id) {
Ok(starttime) if starttime == thread.timestamp => {}
Ok(_) => return false,
Err(e) => {
if !matches!(e, proc::Error::NotFound) {
result = Err(Error::Proc(e));
}
return false;
}
}
let thread_config = &self.config.thread_configs[thread.state as usize];
if thread_config.rt_priority.is_some() {
match self.sched_attr_context.set_thread_sched_attr(
*thread_id,
thread_config,
process_config.allow_rt,
) {
Ok(_) => {}
Err(e) if e.raw_os_error() == Some(libc::ESRCH) => {
// Ignore the error. There is rare cases that the thread die after the
// timestamp check above.
return false;
}
Err(e) => {
result = Err(Error::SchedAttr(e));
}
}
}
if thread_config.cpuset_cgroup != CpusetCgroup::Efficient {
let cpuset_cgroup = if process_config.allow_all_cores {
thread_config.cpuset_cgroup
} else {
CpusetCgroup::Efficient
};
match self
.config
.cgroup_context
.set_cpuset_cgroup(*thread_id, cpuset_cgroup)
{
Ok(_) => {}
Err(e) if e.raw_os_error() == Some(libc::ESRCH) => {
// Ignore the error. There is rare cases that the thread die after the
// timestamp check above.
return false;
}
Err(e) => {
result = Err(Error::Cgroup(cpuset_cgroup.name(), e));
}
}
}
true
});
drop(process);
self.process_map.compact();
result
}
/// Stop managing QoS state associated with the given [ProcessKey].
pub fn remove_process(&mut self, process_key: ProcessKey) {
self.process_map
.remove_process(process_key.process_id, Some(process_key.timestamp));
self.process_map.compact();
}
pub fn set_thread_state(
&mut self,
process_id: ProcessId,
thread_id: ThreadId,
thread_state: ThreadState,
) -> Result<()> {
let Some(mut process) = self.process_map.get_process(process_id) else {
return Err(Error::ProcessNotRegistered);
};
let process_state = process.state();
let timestamp = match load_thread_timestamp(process_id, thread_id) {
Err(proc::Error::NotFound) => {
process.thread_map().remove_thread(thread_id);
drop(process);
self.process_map.compact();
return Err(Error::ThreadNotFound);
}
other => other?,
};
// Validate process timestamp after getting thread timestamp. This guarantees that the
// thread timestamp above is of the thread which belongs to the registered process.
if load_process_timestamp(process_id).ok() != Some(process.timestamp()) {
drop(process);
self.process_map.remove_process(process_id, None);
self.process_map.compact();
return Err(Error::ThreadNotFound);
};
let mut thread_checker = ThreadChecker::new(process_id);
process
.thread_map()
.insert_or_update(thread_id, timestamp, thread_state, |thread_id| {
thread_checker.thread_exists(*thread_id)
});
drop(process);
self.process_map.compact();
let process_config = &self.config.process_configs[process_state as usize];
let thread_config = &self.config.thread_configs[thread_state as usize];
self.sched_attr_context
.set_thread_sched_attr(thread_id, thread_config, process_config.allow_rt)
.map_err(Error::SchedAttr)?;
let cpuset_cgroup = if process_config.allow_all_cores {
thread_config.cpuset_cgroup
} else {
CpusetCgroup::Efficient
};
self.config
.cgroup_context
.set_cpuset_cgroup(thread_id, cpuset_cgroup)
.map_err(|e| Error::Cgroup(cpuset_cgroup.name(), e))?;
// Apply latency sensitive. Latency_sensitive will prefer idle cores.
// This is a patch not yet in upstream(http://crrev/c/2981472)
let latency_sensitive_file = format!(
"/proc/{}/task/{}/latency_sensitive",
process_id.0, thread_id.0
);
if std::path::Path::new(&latency_sensitive_file).exists() {
let value = if thread_config.latency_sensitive {
b"1"
} else {
b"0"
};
std::fs::write(&latency_sensitive_file, value).map_err(Error::LatencySensitive)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
use crate::test_utils::*;
#[test]
fn test_process_state_conversion() {
for state in [ProcessState::Normal, ProcessState::Background] {
assert_eq!(state, ProcessState::try_from(state as u8).unwrap());
}
assert!(ProcessState::try_from(NUM_PROCESS_STATES as u8).is_err());
}
#[test]
fn test_thread_state_conversion() {
for state in [
ThreadState::UrgentBursty,
ThreadState::Urgent,
ThreadState::Balanced,
ThreadState::Eco,
ThreadState::Utility,
ThreadState::Background,
ThreadState::UrgentBurstyServer,
] {
assert_eq!(state, ThreadState::try_from(state as u8).unwrap());
}
assert!(ThreadState::try_from(NUM_THREAD_STATES as u8).is_err());
}
#[test]
fn test_set_process_state() {
let (cgroup_context, mut cgroup_files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: [
// ProcessState::Normal
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Normal,
allow_rt: true,
allow_all_cores: true,
},
// Process:State::Background
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Background,
allow_rt: false,
allow_all_cores: false,
},
],
thread_configs: Config::default_thread_config(),
})
.unwrap();
let process_id = ProcessId(std::process::id());
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
assert_eq!(
read_number(&mut cgroup_files.cpu_normal),
Some(process_id.0)
);
ctx.set_process_state(process_id, ProcessState::Background)
.unwrap();
assert_eq!(
read_number(&mut cgroup_files.cpu_background),
Some(process_id.0)
);
}
#[test]
fn test_set_process_state_change_threads() {
let (cgroup_context, mut cgroup_files) = create_fake_cgroup_context_pair();
let sched_ctx = SchedAttrContext::new().unwrap();
let thread_state_rt_all = ThreadState::try_from(0).unwrap();
let thread_state_all = ThreadState::try_from(1).unwrap();
let thread_state_efficient = ThreadState::try_from(2).unwrap();
let thread_config_rt_all = ThreadStateConfig {
rt_priority: Some(8),
cpuset_cgroup: CpusetCgroup::All,
..ThreadStateConfig::default()
};
let thread_config_all = ThreadStateConfig {
rt_priority: None,
cpuset_cgroup: CpusetCgroup::All,
..ThreadStateConfig::default()
};
let thread_config_efficient = ThreadStateConfig {
rt_priority: None,
cpuset_cgroup: CpusetCgroup::Efficient,
..ThreadStateConfig::default()
};
let mut thread_configs = Config::default_thread_config();
thread_configs[thread_state_rt_all as usize] = thread_config_rt_all.clone();
thread_configs[thread_state_all as usize] = thread_config_all.clone();
thread_configs[thread_state_efficient as usize] = thread_config_efficient.clone();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: [
// ProcessState::Normal
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Normal,
allow_rt: true,
allow_all_cores: true,
},
// Process:State::Background
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Background,
allow_rt: false,
allow_all_cores: false,
},
],
thread_configs,
})
.unwrap();
let process_id = ProcessId(std::process::id());
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
let (thread_id1, _thread1) = spawn_thread_for_test();
let (thread_id2, _thread2) = spawn_thread_for_test();
let (thread_id3, _thread3) = spawn_thread_for_test();
let (thread_id4, _thread4) = spawn_thread_for_test();
let (thread_id5, _thread5) = spawn_thread_for_test();
let (thread_id6, _thread6) = spawn_thread_for_test();
let (thread_id_unmanaged, _thread_unmanaged) = spawn_thread_for_test();
let sched_attr_unmanaged = SchedAttrChecker::new(thread_id_unmanaged);
ctx.set_thread_state(process_id, thread_id1, thread_state_rt_all)
.unwrap();
ctx.set_thread_state(process_id, thread_id2, thread_state_rt_all)
.unwrap();
ctx.set_thread_state(process_id, thread_id3, thread_state_all)
.unwrap();
ctx.set_thread_state(process_id, thread_id4, thread_state_all)
.unwrap();
ctx.set_thread_state(process_id, thread_id5, thread_state_efficient)
.unwrap();
drain_file(&mut cgroup_files.cpu_normal);
drain_file(&mut cgroup_files.cpu_background);
drain_file(&mut cgroup_files.cpuset_all);
drain_file(&mut cgroup_files.cpuset_efficient);
ctx.set_process_state(process_id, ProcessState::Background)
.unwrap();
assert_eq!(read_number(&mut cgroup_files.cpu_normal), None);
assert_eq!(
read_number(&mut cgroup_files.cpu_background),
Some(process_id.0)
);
assert_eq!(read_number(&mut cgroup_files.cpuset_all), None);
assert_eq!(
read_numbers(&mut cgroup_files.cpuset_efficient).collect::<HashSet<_>>(),
HashSet::from([thread_id1.0, thread_id2.0, thread_id3.0, thread_id4.0])
);
assert_sched_attr(&sched_ctx, thread_id1, &thread_config_rt_all, false);
assert_sched_attr(&sched_ctx, thread_id2, &thread_config_rt_all, false);
assert_sched_attr(&sched_ctx, thread_id3, &thread_config_all, false);
assert_sched_attr(&sched_ctx, thread_id4, &thread_config_all, false);
assert_sched_attr(&sched_ctx, thread_id5, &thread_config_efficient, false);
assert_sched_attr(&sched_ctx, thread_id6, &thread_config_efficient, false);
assert!(!sched_attr_unmanaged.is_changed());
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
assert_eq!(
read_number(&mut cgroup_files.cpu_normal),
Some(process_id.0)
);
assert_eq!(read_number(&mut cgroup_files.cpu_background), None);
assert_eq!(
read_numbers(&mut cgroup_files.cpuset_all).collect::<HashSet<_>>(),
HashSet::from([thread_id1.0, thread_id2.0, thread_id3.0, thread_id4.0])
);
assert_eq!(read_number(&mut cgroup_files.cpuset_efficient), None);
assert_sched_attr(&sched_ctx, thread_id1, &thread_config_rt_all, true);
assert_sched_attr(&sched_ctx, thread_id2, &thread_config_rt_all, true);
assert_sched_attr(&sched_ctx, thread_id3, &thread_config_all, true);
assert_sched_attr(&sched_ctx, thread_id4, &thread_config_all, true);
assert_sched_attr(&sched_ctx, thread_id5, &thread_config_efficient, true);
assert_sched_attr(&sched_ctx, thread_id6, &thread_config_efficient, true);
assert!(!sched_attr_unmanaged.is_changed());
}
#[test]
fn test_set_process_state_invalid_process() {
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
let (process_id, _, process) = fork_process_for_test();
drop(process);
assert!(matches!(
ctx.set_process_state(process_id, ProcessState::Normal)
.err()
.unwrap(),
Error::ProcessNotFound
));
}
#[test]
fn test_set_process_state_for_new_process() {
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
let process_id = ProcessId(std::process::id());
// First set_process_state() creates a new process context.
let process_key = ctx
.set_process_state(process_id, ProcessState::Normal)
.unwrap();
assert!(process_key.is_some());
let process_key = process_key.unwrap();
assert_eq!(process_key.process_id, process_id);
assert_eq!(ctx.process_map.len(), 1);
let process_key = ctx
.set_process_state(process_id, ProcessState::Background)
.unwrap();
assert!(process_key.is_none());
let process_key = ctx
.set_process_state(process_id, ProcessState::Normal)
.unwrap();
assert!(process_key.is_none());
let (process_id, _, _process) = fork_process_for_test();
let process_key = ctx
.set_process_state(process_id, ProcessState::Normal)
.unwrap();
assert!(process_key.is_some());
let process_key = process_key.unwrap();
assert_eq!(process_key.process_id, process_id);
assert_eq!(ctx.process_map.len(), 2);
}
#[test]
fn test_set_process_state_with_different_timestamp() {
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
let process_id = ProcessId(std::process::id());
ctx.process_map
.insert_or_update(process_id, 0, ProcessState::Normal);
let (thread_id, _thread) = spawn_thread_for_test();
ctx.process_map
.get_process(process_id)
.unwrap()
.thread_map()
.insert_or_update(
thread_id,
load_thread_timestamp(process_id, thread_id).unwrap(),
ThreadState::UrgentBursty,
|_| true,
);
// set_process_state() with different timestamp is treated as a new process context.
let process_key = ctx
.set_process_state(process_id, ProcessState::Normal)
.unwrap();
assert!(process_key.is_some());
let process_key = process_key.unwrap();
assert_eq!(process_key.process_id, process_id);
assert_eq!(ctx.process_map.len(), 1);
assert_eq!(
ctx.process_map
.get_process(process_id)
.unwrap()
.thread_map()
.len(),
0
);
}
#[test]
fn test_set_process_state_compact() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("states");
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_file(
Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
},
&file_path,
)
.unwrap();
let process_id = ProcessId(std::process::id());
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
let (thread_id1, dead_thread1) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id1, ThreadState::Urgent)
.unwrap();
let (thread_id2, _thread2) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id2, ThreadState::Utility)
.unwrap();
let mut process_ctx = ctx.process_map.get_process(process_id).unwrap();
assert_eq!(process_ctx.thread_map().len(), 2);
assert_eq!(ctx.process_map.n_cells(), 3);
drop(dead_thread1);
wait_for_thread_removed(process_id, thread_id1);
ctx.set_process_state(process_id, ProcessState::Background)
.unwrap();
let mut process_ctx = ctx.process_map.get_process(process_id).unwrap();
assert_eq!(process_ctx.thread_map().len(), 1);
assert_eq!(ctx.process_map.n_cells(), 2);
}
#[test]
fn test_remove_process() {
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
let mut processes = Vec::new();
for _ in 0..3 {
let (process_id, _, process) = fork_process_for_test();
processes.push(process);
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
}
let (process_id, _, process) = fork_process_for_test();
let process_key = ctx
.set_process_state(process_id, ProcessState::Normal)
.unwrap()
.unwrap();
assert_eq!(ctx.process_map.len(), 4);
// Process is not cloneable. This is for testing purpose.
let cloned_process_key = ProcessKey {
process_id,
timestamp: process_key.timestamp,
};
drop(process);
ctx.remove_process(process_key);
assert_eq!(ctx.process_map.len(), 3);
// If the process is removed, remove_process() is no-op.
ctx.remove_process(cloned_process_key);
assert_eq!(ctx.process_map.len(), 3);
}
#[test]
fn test_remove_process_compact() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("states");
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_file(
Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
},
&file_path,
)
.unwrap();
let (process_id, thread_id, process) = fork_process_for_test();
let process_key = ctx
.set_process_state(process_id, ProcessState::Normal)
.unwrap()
.unwrap();
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.unwrap();
assert_eq!(ctx.process_map.n_cells(), 2);
drop(process);
ctx.remove_process(process_key);
assert_eq!(ctx.process_map.n_cells(), 0);
}
#[test]
fn test_set_thread_state() {
let process_id = ProcessId(std::process::id());
let (cgroup_context, mut cgroup_files) = create_fake_cgroup_context_pair();
let thread_configs = [
// ThreadState::UrgentBursty
ThreadStateConfig {
rt_priority: Some(8),
nice: -8,
uclamp_min: UCLAMP_BOOSTED_MIN,
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
},
// ThreadState::Urgent
ThreadStateConfig {
nice: -8,
uclamp_min: UCLAMP_BOOSTED_MIN,
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
..ThreadStateConfig::default()
},
// ThreadState::Balanced
ThreadStateConfig {
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
..ThreadStateConfig::default()
},
// ThreadState::Eco
ThreadStateConfig {
cpuset_cgroup: CpusetCgroup::Efficient,
..ThreadStateConfig::default()
},
// ThreadState::Utility
ThreadStateConfig {
nice: 1,
cpuset_cgroup: CpusetCgroup::Efficient,
..ThreadStateConfig::default()
},
// ThreadState::Background
ThreadStateConfig {
nice: 10,
cpuset_cgroup: CpusetCgroup::Efficient,
..ThreadStateConfig::default()
},
// ThreadState::UrgentBurstyServer
ThreadStateConfig {
rt_priority: Some(12),
nice: -12,
uclamp_min: UCLAMP_BOOSTED_MIN,
cpuset_cgroup: CpusetCgroup::All,
latency_sensitive: true,
},
];
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: [
// ProcessState::Normal
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Normal,
allow_rt: true,
allow_all_cores: true,
},
// Process:State::Background
ProcessStateConfig {
cpu_cgroup: CpuCgroup::Background,
allow_rt: false,
allow_all_cores: false,
},
],
thread_configs: thread_configs.clone(),
})
.unwrap();
// set_process_state() is required before set_thread_state().
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
let sched_ctx = SchedAttrContext::new().unwrap();
for state in [
ThreadState::UrgentBursty,
ThreadState::Urgent,
ThreadState::Balanced,
ThreadState::Eco,
ThreadState::Utility,
ThreadState::Background,
ThreadState::UrgentBurstyServer,
] {
let (thread_id, _thread) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id, state).unwrap();
let thread_config = &thread_configs[state as usize];
match thread_config.cpuset_cgroup {
CpusetCgroup::All => {
assert_eq!(read_number(&mut cgroup_files.cpuset_all), Some(thread_id.0))
}
CpusetCgroup::Efficient => {
assert_eq!(
read_number(&mut cgroup_files.cpuset_efficient),
Some(thread_id.0)
)
}
}
assert_sched_attr(&sched_ctx, thread_id, thread_config, true);
}
ctx.set_process_state(process_id, ProcessState::Background)
.unwrap();
drain_file(&mut cgroup_files.cpuset_all);
drain_file(&mut cgroup_files.cpuset_efficient);
for state in [
ThreadState::UrgentBursty,
ThreadState::Urgent,
ThreadState::Balanced,
ThreadState::Eco,
ThreadState::Utility,
ThreadState::Background,
ThreadState::UrgentBurstyServer,
] {
let (thread_id, _thread) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id, state).unwrap();
assert_eq!(
read_number(&mut cgroup_files.cpuset_efficient),
Some(thread_id.0)
);
let thread_config = &thread_configs[state as usize];
assert_sched_attr(&sched_ctx, thread_id, thread_config, false);
}
}
#[test]
fn test_set_thread_state_without_process() {
let process_id = ProcessId(std::process::id());
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
let (thread_id, _thread) = spawn_thread_for_test();
assert!(matches!(
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.err()
.unwrap(),
Error::ProcessNotRegistered
));
}
#[test]
fn test_set_thread_state_invalid_thread() {
let process_id = ProcessId(std::process::id());
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
let (_, child_process_thread_id, _process) = fork_process_for_test();
let (thread_id, thread) = spawn_thread_for_test();
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
// The thread does not in the process.
assert!(matches!(
ctx.set_thread_state(process_id, child_process_thread_id, ThreadState::Balanced)
.err()
.unwrap(),
Error::ThreadNotFound
));
// The thread is dead.
drop(thread);
assert!(wait_for_thread_removed(process_id, thread_id));
assert!(matches!(
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.err()
.unwrap(),
Error::ThreadNotFound
));
let (thread_id, thread) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.unwrap();
// The thread is dead after registered.
drop(thread);
assert!(wait_for_thread_removed(process_id, thread_id));
assert!(matches!(
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.err()
.unwrap(),
Error::ThreadNotFound
));
}
#[test]
fn test_set_thread_state_with_different_timestamp_process() {
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
let process_id = ProcessId(std::process::id());
ctx.process_map
.insert_or_update(process_id, 0, ProcessState::Normal);
let (thread_id, _thread) = spawn_thread_for_test();
// The timestamp of process does not match.
assert!(matches!(
ctx.set_thread_state(process_id, thread_id, ThreadState::UrgentBursty)
.err()
.unwrap(),
Error::ThreadNotFound
));
}
#[test]
fn test_set_thread_state_gc() {
let process_id = ProcessId(std::process::id());
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_simple(Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
})
.unwrap();
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
let (thread_id, _thread1) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.unwrap();
let mut process_ctx = ctx.process_map.get_process(process_id).unwrap();
assert_eq!(process_ctx.thread_map().len(), 1);
for _ in 0..10 {
let (thread_id, thread) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.unwrap();
drop(thread);
wait_for_thread_removed(process_id, thread_id);
}
let (thread_id, _thread2) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.unwrap();
let mut process_ctx = ctx.process_map.get_process(process_id).unwrap();
assert_eq!(process_ctx.thread_map().len(), 2);
let (thread_id, _thread3) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id, ThreadState::Balanced)
.unwrap();
let mut process_ctx = ctx.process_map.get_process(process_id).unwrap();
assert_eq!(process_ctx.thread_map().len(), 3);
}
#[test]
fn test_set_thread_state_compact() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("states");
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_file(
Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
},
&file_path,
)
.unwrap();
let process_id = ProcessId(std::process::id());
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
let (thread_id1, dead_thread1) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id1, ThreadState::Urgent)
.unwrap();
let (thread_id2, _thread2) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id2, ThreadState::Utility)
.unwrap();
let mut process_ctx = ctx.process_map.get_process(process_id).unwrap();
assert_eq!(process_ctx.thread_map().len(), 2);
assert_eq!(ctx.process_map.n_cells(), 3);
drop(dead_thread1);
wait_for_thread_removed(process_id, thread_id1);
let (thread_id3, _thread3) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id3, ThreadState::Background)
.unwrap();
let mut process_ctx = ctx.process_map.get_process(process_id).unwrap();
assert_eq!(process_ctx.thread_map().len(), 2);
assert_eq!(ctx.process_map.n_cells(), 3);
}
#[test]
fn test_restart() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("states");
let (cgroup_context, _files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::new_file(
Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
},
&file_path,
)
.unwrap();
let process_id = ProcessId(std::process::id());
ctx.set_process_state(process_id, ProcessState::Normal)
.unwrap();
let (thread_id1, _thread1) = spawn_thread_for_test();
ctx.set_thread_state(process_id, thread_id1, ThreadState::UrgentBursty)
.unwrap();
let (process_id2, thread_id2, _process) = fork_process_for_test();
ctx.set_process_state(process_id2, ProcessState::Background)
.unwrap()
.unwrap();
ctx.set_thread_state(process_id2, thread_id2, ThreadState::Background)
.unwrap();
let (cgroup_context, mut files) = create_fake_cgroup_context_pair();
let mut ctx = SchedQosContext::load_from_file(
Config {
cgroup_context,
process_configs: Config::default_process_config(),
thread_configs: Config::default_thread_config(),
},
&file_path,
)
.unwrap();
ctx.set_process_state(process_id, ProcessState::Background)
.unwrap();
assert_eq!(
read_number(&mut files.cpuset_efficient).unwrap(),
thread_id1.0
);
assert!(read_number(&mut files.cpuset_efficient).is_none());
ctx.set_thread_state(process_id2, thread_id2, ThreadState::UrgentBursty)
.unwrap();
assert_eq!(
read_number(&mut files.cpuset_efficient).unwrap(),
thread_id2.0
);
}
}