Merge pull request #843 from CachyOS/feature/scx-loader-config

scx_loader: introduce configuration
This commit is contained in:
Tejun Heo 2024-10-23 23:00:24 +00:00 committed by GitHub
commit 386ae20ee7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 434 additions and 35 deletions

27
Cargo.lock generated
View File

@ -1763,6 +1763,7 @@ dependencies = [
"serde",
"sysinfo",
"tokio",
"toml",
"zbus",
"zvariant",
]
@ -1967,6 +1968,15 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
@ -2263,11 +2273,26 @@ dependencies = [
"syn",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@ -2276,6 +2301,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]

View File

@ -16,5 +16,6 @@ nix = { features = ["process", "signal"], default-features = false, version = "0
serde = { version = "1.0", features = ["derive"] }
sysinfo = "0.31.4"
tokio = { version = "1.39", features = ["macros", "sync", "rt-multi-thread", "process"] }
toml = "0.8.19"
zbus = { version = "4", features = ["tokio"], default-features = false }
zvariant = "4.2"

View File

@ -0,0 +1,84 @@
# scx_loader Configuration File
The `scx_loader` can be configured using a TOML file. This file allows you to customize the default scheduler mode, specify custom flags for each supported scheduler and mode, and set a default scheduler to start on boot.
## Configuration File Location
`scx_loader` looks for the configuration file in the following paths (in order):
1. `/etc/scx_loader/config.toml`
2. `/etc/scx_loader.toml`
If no configuration file is found at any of these paths, `scx_loader` will use the built-in default configuration.
## Configuration Structure
The configuration file has the following structure:
```toml
default_sched = "scx_bpfland"
default_mode = "Auto"
[scheds.scx_bpfland]
auto_mode = []
gaming_mode = ["-k", "-m", "performance"]
lowlatency_mode = ["--lowlatency"]
powersave_mode = ["-m", "powersave"]
[scheds.scx_rusty]
auto_mode = []
gaming_mode = []
lowlatency_mode = []
powersave_mode = []
[scheds.scx_lavd]
auto_mode = []
gaming_mode = ["--performance"]
lowlatency_mode = ["--performance"]
powersave_mode = ["--powersave"]
```
**`default_sched`:**
* This field specifies the scheduler that will be started automatically when `scx_loader` starts (e.g., on boot).
* It should be set to the name of a supported scheduler (e.g., `"scx_bpfland"`, `"scx_rusty"`, `"scx_lavd"`).
* If this field is not present or is set to an empty string, no scheduler will be started automatically.
**`default_mode`:**
* This field specifies the default scheduler mode that will be used when starting a scheduler without explicitly specifying a mode.
* Possible values are: `"Auto"`, `"Gaming"`, `"LowLatency"`, `"PowerSave"`.
* If this field is not present, it defaults to `"Auto"`.
**`[scheds.scx_name]`:**
* This section defines the custom flags for a specific scheduler. Replace `scx_name` with the actual name of the scheduler (e.g., `scx_bpfland`, `scx_rusty`, `scx_lavd`).
**`auto_mode`, `gaming_mode`, `lowlatency_mode`, `powersave_mode`:**
* These fields specify the flags to be used for each scheduler mode.
* Each field is an array of strings, where each string represents a flag.
* If a field is not present or is an empty array, the default flags for that mode will be used.
## Example Configuration
The example configuration above shows how to set custom flags for different schedulers and modes, and how to configure `scx_bpfland` to start automatically on boot.
* For `scx_bpfland`:
* Gaming mode: `-k -m performance`
* Low Latency mode: `--lowlatency`
* Power Save mode: `-m powersave`
* For `scx_rusty`:
* No custom flags are defined, so the default flags for each mode will be used.
* For `scx_lavd`:
* Gaming mode: `--performance`
* Low Latency mode: `--performance`
* Power Save mode: `--powersave`
## Fallback Behavior
If a specific flag is not defined in the configuration file, `scx_loader` will fall back to the default flags defined in the code.
## Missing Required Fields
If the `default_mode` field is missing, it will default to `"Auto"`. If a `[scheds.scx_name]` section is missing, or if specific mode flags are missing within that section, the default flags for the corresponding scheduler and mode will be used. If `default_sched` is missing or empty, no scheduler will be started automatically.

View File

@ -0,0 +1,271 @@
// SPDX-License-Identifier: GPL-2.0
//
// Copyright (c) 2024 Vladislav Nepogodin <vnepogodin@cachyos.org>
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2.
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use anyhow::Result;
use serde::Deserialize;
use crate::get_name_from_scx;
use crate::SchedMode;
use crate::SupportedSched;
#[derive(Debug, PartialEq, Default, Deserialize)]
#[serde(default)]
pub struct Config {
pub default_sched: Option<SupportedSched>,
pub default_mode: Option<SchedMode>,
pub scheds: HashMap<String, Sched>,
}
#[derive(Debug, PartialEq, Default, Deserialize)]
pub struct Sched {
pub auto_mode: Option<Vec<String>>,
pub gaming_mode: Option<Vec<String>>,
pub lowlatency_mode: Option<Vec<String>>,
pub powersave_mode: Option<Vec<String>>,
}
/// Initialize config from first found config path, overwise fallback to default config
pub fn init_config() -> Result<Config> {
if let Ok(config_path) = get_config_path() {
parse_config_file(&config_path)
} else {
Ok(get_default_config())
}
}
pub fn parse_config_file(filepath: &str) -> Result<Config> {
let file_content = fs::read_to_string(filepath)?;
parse_config_content(&file_content)
}
pub fn get_config_path() -> Result<String> {
// Search in system directories
let check_paths = [
"/etc/scx_loader/config.toml".to_owned(),
"/etc/scx_loader.toml".to_owned(),
];
for check_path in check_paths {
if !Path::new(&check_path).exists() {
continue;
}
// we found config path
return Ok(check_path);
}
anyhow::bail!("Failed to find config!");
}
fn parse_config_content(file_content: &str) -> Result<Config> {
if file_content.is_empty() {
anyhow::bail!("The config file is empty!")
}
let config: Config = toml::from_str(file_content)?;
Ok(config)
}
pub fn get_default_config() -> Config {
Config {
default_sched: None,
default_mode: Some(SchedMode::Auto),
scheds: HashMap::from([
(
"scx_bpfland".to_string(),
get_default_sched_for_config(&SupportedSched::Bpfland),
),
(
"scx_rusty".to_string(),
get_default_sched_for_config(&SupportedSched::Rusty),
),
(
"scx_lavd".to_string(),
get_default_sched_for_config(&SupportedSched::Lavd),
),
]),
}
}
/// Get the scx flags for the given sched mode
pub fn get_scx_flags_for_mode(
config: &Config,
scx_sched: &SupportedSched,
sched_mode: SchedMode,
) -> Vec<String> {
if let Some(sched_config) = config.scheds.get(get_name_from_scx(scx_sched)) {
let scx_flags = extract_scx_flags_from_config(sched_config, &sched_mode);
// try to exact flags from config, otherwise fallback to hardcoded default
scx_flags.unwrap_or({
get_default_scx_flags_for_mode(scx_sched, sched_mode)
.into_iter()
.map(String::from)
.collect()
})
} else {
get_default_scx_flags_for_mode(scx_sched, sched_mode)
.into_iter()
.map(String::from)
.collect()
}
}
/// Extract the scx flags from config
fn extract_scx_flags_from_config(
sched_config: &Sched,
sched_mode: &SchedMode,
) -> Option<Vec<String>> {
match sched_mode {
SchedMode::Gaming => sched_config.gaming_mode.clone(),
SchedMode::LowLatency => sched_config.lowlatency_mode.clone(),
SchedMode::PowerSave => sched_config.powersave_mode.clone(),
SchedMode::Auto => sched_config.auto_mode.clone(),
}
}
/// Get Sched object for configuration object
fn get_default_sched_for_config(scx_sched: &SupportedSched) -> Sched {
Sched {
auto_mode: Some(
get_default_scx_flags_for_mode(scx_sched, SchedMode::Auto)
.into_iter()
.map(String::from)
.collect(),
),
gaming_mode: Some(
get_default_scx_flags_for_mode(scx_sched, SchedMode::Gaming)
.into_iter()
.map(String::from)
.collect(),
),
lowlatency_mode: Some(
get_default_scx_flags_for_mode(scx_sched, SchedMode::LowLatency)
.into_iter()
.map(String::from)
.collect(),
),
powersave_mode: Some(
get_default_scx_flags_for_mode(scx_sched, SchedMode::PowerSave)
.into_iter()
.map(String::from)
.collect(),
),
}
}
/// Get the default scx flags for the given sched mode
fn get_default_scx_flags_for_mode(scx_sched: &SupportedSched, sched_mode: SchedMode) -> Vec<&str> {
match scx_sched {
SupportedSched::Bpfland => match sched_mode {
SchedMode::Gaming => vec!["-k", "-m", "performance"],
SchedMode::LowLatency => vec!["--lowlatency"],
SchedMode::PowerSave => vec!["-m", "powersave"],
SchedMode::Auto => vec![],
},
SupportedSched::Lavd => match sched_mode {
SchedMode::Gaming | SchedMode::LowLatency => vec!["--performance"],
SchedMode::PowerSave => vec!["--powersave"],
// NOTE: potentially adding --auto in future
SchedMode::Auto => vec![],
},
// scx_rusty doesn't support any of these modes
SupportedSched::Rusty => vec![],
}
}
#[cfg(test)]
mod tests {
use crate::config::*;
#[test]
fn test_default_config() {
let config_str = r#"
default_mode = "Auto"
[scheds.scx_bpfland]
auto_mode = []
gaming_mode = ["-k", "-m", "performance"]
lowlatency_mode = ["--lowlatency"]
powersave_mode = ["-m", "powersave"]
[scheds.scx_rusty]
auto_mode = []
gaming_mode = []
lowlatency_mode = []
powersave_mode = []
[scheds.scx_lavd]
auto_mode = []
gaming_mode = ["--performance"]
lowlatency_mode = ["--performance"]
powersave_mode = ["--powersave"]
"#;
let parsed_config = parse_config_content(config_str).expect("Failed to parse config");
let expected_config = get_default_config();
assert_eq!(parsed_config, expected_config);
}
#[test]
fn test_simple_fallback_config_flags() {
let config_str = r#"
default_mode = "Auto"
"#;
let parsed_config = parse_config_content(config_str).expect("Failed to parse config");
let bpfland_flags =
get_scx_flags_for_mode(&parsed_config, &SupportedSched::Bpfland, SchedMode::Gaming);
let expected_flags =
get_default_scx_flags_for_mode(&SupportedSched::Bpfland, SchedMode::Gaming);
assert_eq!(
bpfland_flags
.iter()
.map(|x| x.as_str())
.collect::<Vec<&str>>(),
expected_flags
);
}
#[test]
fn test_sched_fallback_config_flags() {
let config_str = r#"
default_mode = "Auto"
[scheds.scx_lavd]
auto_mode = ["--help"]
"#;
let parsed_config = parse_config_content(config_str).expect("Failed to parse config");
let lavd_flags =
get_scx_flags_for_mode(&parsed_config, &SupportedSched::Lavd, SchedMode::Gaming);
let expected_flags =
get_default_scx_flags_for_mode(&SupportedSched::Lavd, SchedMode::Gaming);
assert_eq!(
lavd_flags.iter().map(|x| x.as_str()).collect::<Vec<&str>>(),
expected_flags
);
let lavd_flags =
get_scx_flags_for_mode(&parsed_config, &SupportedSched::Lavd, SchedMode::Auto);
assert_eq!(
lavd_flags.iter().map(|x| x.as_str()).collect::<Vec<&str>>(),
vec!["--help"]
);
}
#[test]
fn test_empty_config() {
let config_str = "";
let result = parse_config_content(config_str);
assert!(result.is_err());
}
}

View File

@ -5,6 +5,7 @@
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2.
mod config;
mod logger;
use std::process::Stdio;
@ -29,27 +30,40 @@ use zbus::Connection;
use zvariant::Type;
use zvariant::Value;
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum SupportedSched {
#[serde(rename = "scx_bpfland")]
Bpfland,
#[serde(rename = "scx_rusty")]
Rusty,
#[serde(rename = "scx_lavd")]
Lavd,
}
#[derive(Debug, PartialEq)]
enum ScxMessage {
/// Quit the scx_loader
Quit,
/// Stop the scheduler, if any
StopSched,
/// Start the scheduler with the given mode
StartSched((SupportedSched, SchedMode)),
/// Start the scheduler with the given scx arguments
StartSchedArgs((SupportedSched, Vec<String>)),
/// Switch to another scheduler with the given mode
SwitchSched((SupportedSched, SchedMode)),
/// Switch to another scheduler with the given scx arguments
SwitchSchedArgs((SupportedSched, Vec<String>)),
}
#[derive(Debug, PartialEq)]
enum RunnerMessage {
/// Switch to another scheduler with the given scx arguments
Switch((SupportedSched, Vec<String>)),
/// Start the scheduler with the given scx arguments
Start((SupportedSched, Vec<String>)),
/// Stop the scheduler, if any
Stop,
}
@ -84,7 +98,7 @@ impl ScxLoader {
#[zbus(property)]
async fn current_scheduler(&self) -> String {
if let Some(current_scx) = &self.current_scx {
let current_scx = get_name_from_scx(&current_scx).into();
let current_scx = get_name_from_scx(current_scx).into();
log::info!("called {current_scx:?}");
return current_scx;
}
@ -181,7 +195,7 @@ impl ScxLoader {
async fn stop_scheduler(&mut self) -> zbus::fdo::Result<()> {
if let Some(current_scx) = &self.current_scx {
let scx_name = get_name_from_scx(&current_scx);
let scx_name = get_name_from_scx(current_scx);
log::info!("stopping {scx_name:?}..");
let _ = self.channel.send(ScxMessage::StopSched);
@ -192,6 +206,18 @@ impl ScxLoader {
}
}
#[zbus::proxy(
interface = "org.scx.Loader",
default_service = "org.scx.Loader",
default_path = "/org/scx/Loader"
)]
pub trait LoaderClient {
/// Method for switching to the specified scheduler with the given mode.
/// This method will stop the currently running scheduler (if any) and
/// then start the new scheduler.
fn switch_scheduler(&self, scx_name: &str, sched_mode: SchedMode) -> zbus::Result<()>;
}
// Monitors CPU utilization and enables scx_lavd when utilization of any CPUs is > 90%
async fn monitor_cpu_util() -> Result<()> {
let mut system = System::new_all();
@ -264,6 +290,9 @@ async fn main() -> Result<()> {
let args = Args::parse();
// initialize the config
let config = config::init_config().context("Failed to initialize config")?;
// If --auto is passed, start scx_loader as a standard background process
// that swaps schedulers out automatically
// based on CPU utilization without registering a dbus interface.
@ -300,13 +329,28 @@ async fn main() -> Result<()> {
connection.request_name("org.scx.Loader").await?;
// if user set default scheduler, then start it
if let Some(default_sched) = &config.default_sched {
log::info!("Starting default scheduler: {default_sched:?}");
let default_mode = config.default_mode.clone().unwrap_or(SchedMode::Auto);
let loader_client = LoaderClientProxy::new(&connection).await?;
loader_client
.switch_scheduler(get_name_from_scx(default_sched), default_mode)
.await?;
}
// run worker/receiver loop
worker_loop(rx).await?;
worker_loop(config, rx).await?;
Ok(())
}
async fn worker_loop(mut receiver: UnboundedReceiver<ScxMessage>) -> Result<()> {
async fn worker_loop(
config: config::Config,
mut receiver: UnboundedReceiver<ScxMessage>,
) -> Result<()> {
// setup channel for scheduler runner
let (runner_tx, runner_rx) = tokio::sync::mpsc::channel::<RunnerMessage>(1);
@ -344,10 +388,7 @@ async fn worker_loop(mut receiver: UnboundedReceiver<ScxMessage>) -> Result<()>
log::info!("Got event to start scheduler!");
// get scheduler args for the mode
let args: Vec<_> = get_scx_flags_for_mode(&scx_sched, sched_mode)
.into_iter()
.map(String::from)
.collect();
let args = config::get_scx_flags_for_mode(&config, &scx_sched, sched_mode);
// send message with scheduler and asociated args to the runner
runner_tx
@ -366,10 +407,7 @@ async fn worker_loop(mut receiver: UnboundedReceiver<ScxMessage>) -> Result<()>
log::info!("Got event to switch scheduler!");
// get scheduler args for the mode
let args: Vec<_> = get_scx_flags_for_mode(&scx_sched, sched_mode)
.into_iter()
.map(String::from)
.collect();
let args = config::get_scx_flags_for_mode(&config, &scx_sched, sched_mode);
// send message with scheduler and asociated args to the runner
runner_tx
@ -522,25 +560,3 @@ fn get_name_from_scx(supported_sched: &SupportedSched) -> &'static str {
SupportedSched::Lavd => "scx_lavd",
}
}
/// Get the scx flags for the given sched mode
fn get_scx_flags_for_mode(scx_sched: &SupportedSched, sched_mode: SchedMode) -> Vec<&str> {
match scx_sched {
SupportedSched::Bpfland => match sched_mode {
SchedMode::Gaming => vec!["-c", "0", "-k", "-m", "performance"],
SchedMode::LowLatency => vec!["--lowlatency"],
SchedMode::PowerSave => vec!["-m", "powersave"],
SchedMode::Auto => vec![],
},
SupportedSched::Lavd => match sched_mode {
SchedMode::Gaming | SchedMode::LowLatency => vec!["--performance"],
SchedMode::PowerSave => vec!["--powersave"],
// NOTE: potentially adding --auto in future
SchedMode::Auto => vec![],
},
// scx_rusty doesn't support any of these modes
SupportedSched::Rusty => match sched_mode {
_ => vec![],
},
}
}