| // 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 |
| ); |
| } |
| } |