diff --git a/projects/start-cli/CHANGELOG.md b/projects/start-cli/CHANGELOG.md index bc1ecf82cc..c648dba1a9 100644 --- a/projects/start-cli/CHANGELOG.md +++ b/projects/start-cli/CHANGELOG.md @@ -13,6 +13,9 @@ or the CLI's externally observable behavior. ### Added +- **`server epp` shows or sets the CPU energy/performance preference.** The setting persists + across reboots and reports the values available on the server. + - **`s9pk pack` packs the package's `README.md`.** It sits beside `instructions.md` in the archive and is readable with `S9pk::readme()`. The point is what runs on the server: an AI assistant administering a service can now read the package's technical reference from the @@ -30,6 +33,9 @@ or the CLI's externally observable behavior. ### Changed +- **`server governor` replaces `server experimental governor`.** Scripts that set or inspect + the CPU governor need to use the direct `server` subcommand. + - **`server set-hostname` takes one required hostname, and `setup execute` no longer takes `--name`.** A StartOS server carries a single name — its `.local` hostname — where it used to carry a separate display label as well. Setting it diff --git a/projects/start-cli/man/start-cli-server-epp.1 b/projects/start-cli/man/start-cli-server-epp.1 new file mode 100644 index 0000000000..42d9d58c95 --- /dev/null +++ b/projects/start-cli/man/start-cli-server-epp.1 @@ -0,0 +1,19 @@ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.TH start-cli-server-epp 1 "epp " +.SH NAME +start\-cli\-server\-epp \- Show CPU energy performance preferences +.SH SYNOPSIS +\fBstart\-cli server epp\fR [\fB\-\-format\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fISET\fR] +.SH DESCRIPTION +Show CPU energy performance preferences +.SH OPTIONS +.TP +\fB\-\-format\fR + +.TP +\fB\-h\fR, \fB\-\-help\fR +Print help +.TP +[\fISET\fR] +CPU energy performance preference diff --git a/projects/start-cli/man/start-cli-server-experimental-governor.1 b/projects/start-cli/man/start-cli-server-experimental-governor.1 deleted file mode 100644 index b6ca2f04cc..0000000000 --- a/projects/start-cli/man/start-cli-server-experimental-governor.1 +++ /dev/null @@ -1,19 +0,0 @@ -.ie \n(.g .ds Aq \(aq -.el .ds Aq ' -.TH start-cli-server-experimental-governor 1 "governor " -.SH NAME -start\-cli\-server\-experimental\-governor \- Show CPU governors -.SH SYNOPSIS -\fBstart\-cli server experimental governor\fR [\fB\-\-format\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fISET\fR] -.SH DESCRIPTION -Show CPU governors -.SH OPTIONS -.TP -\fB\-\-format\fR - -.TP -\fB\-h\fR, \fB\-\-help\fR -Print help -.TP -[\fISET\fR] -CPU governor name diff --git a/projects/start-cli/man/start-cli-server-experimental.1 b/projects/start-cli/man/start-cli-server-experimental.1 index c0ac65e929..7495888c16 100644 --- a/projects/start-cli/man/start-cli-server-experimental.1 +++ b/projects/start-cli/man/start-cli-server-experimental.1 @@ -2,19 +2,16 @@ .el .ds Aq ' .TH start-cli-server-experimental 1 "experimental " .SH NAME -start\-cli\-server\-experimental \- Commands related to configuring experimental options such as zram and cpu governor +start\-cli\-server\-experimental \- Commands related to configuring experimental options .SH SYNOPSIS \fBstart\-cli server experimental\fR [\fB\-h\fR|\fB\-\-help\fR] <\fIsubcommands\fR> .SH DESCRIPTION -Commands related to configuring experimental options such as zram and cpu governor +Commands related to configuring experimental options .SH OPTIONS .TP \fB\-h\fR, \fB\-\-help\fR Print help .SH SUBCOMMANDS .TP -start\-cli\-server\-experimental\-governor(1) -Show CPU governors -.TP start\-cli\-server\-experimental\-zram(1) Enable ZRAM diff --git a/projects/start-cli/man/start-cli-server-governor.1 b/projects/start-cli/man/start-cli-server-governor.1 new file mode 100644 index 0000000000..3e2b2a7bdc --- /dev/null +++ b/projects/start-cli/man/start-cli-server-governor.1 @@ -0,0 +1,19 @@ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.TH start-cli-server-governor 1 "governor " +.SH NAME +start\-cli\-server\-governor \- Show CPU governors +.SH SYNOPSIS +\fBstart\-cli server governor\fR [\fB\-\-format\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fISET\fR] +.SH DESCRIPTION +Show CPU governors +.SH OPTIONS +.TP +\fB\-\-format\fR + +.TP +\fB\-h\fR, \fB\-\-help\fR +Print help +.TP +[\fISET\fR] +CPU governor name diff --git a/projects/start-cli/man/start-cli-server.1 b/projects/start-cli/man/start-cli-server.1 index 04d8e685bd..0f47b6d4b5 100644 --- a/projects/start-cli/man/start-cli-server.1 +++ b/projects/start-cli/man/start-cli-server.1 @@ -19,8 +19,14 @@ Remove system smtp server and credentials start\-cli\-server\-device\-info(1) Display device information .TP +start\-cli\-server\-epp(1) +Show CPU energy performance preferences +.TP start\-cli\-server\-experimental(1) -Commands related to configuring experimental options such as zram and cpu governor +Commands related to configuring experimental options +.TP +start\-cli\-server\-governor(1) +Show CPU governors .TP start\-cli\-server\-host(1) Commands for modifying the host for the system ui diff --git a/projects/start-os/CHANGELOG.md b/projects/start-os/CHANGELOG.md index ecc5973b02..bfa7c37540 100644 --- a/projects/start-os/CHANGELOG.md +++ b/projects/start-os/CHANGELOG.md @@ -44,6 +44,12 @@ file tracks notable changes since the move to the monorepo. one's name. See [Alternative Registries](https://docs.start9.com/start-os/alternative-registries.html). +- **`start-cli server epp` shows and sets the CPU's + energy/performance preference**, persisted across reboots the way the governor + is. On Librem Mini v2 systems without a saved preference, StartOS applies + `balance_power` when available. Without a saved preference, all other systems + retain their current value. + - **A service can permanently retire a network host or a port it no longer uses, and the port numbers it held become available again.** A service that reorganizes its interfaces across an update — renaming a host, dropping a @@ -159,6 +165,9 @@ file tracks notable changes since the move to the monorepo. its public address directly, or that is reached over StartTunnel, is unaffected. See [Public IP](https://docs.start9.com/start-os/public-ip.html). +- **`--format json` on `start-cli server governor` and + `start-cli ssh list` returns the result** it was asked for. + - **A downgrade to a version that cannot take over the service's data is refused before anything is downloaded or stopped**, with an explanation of what to do instead. diff --git a/projects/start-os/docs/src/cli-reference.md b/projects/start-os/docs/src/cli-reference.md index acd2a55e84..7438a3f192 100644 --- a/projects/start-os/docs/src/cli-reference.md +++ b/projects/start-os/docs/src/cli-reference.md @@ -147,12 +147,19 @@ Set the keyboard layout. Set the Echo IP service URLs used for external IP detection. -### `start-cli server experimental governor [SET]` +### `start-cli server governor [SET]` View or set the CPU governor (e.g., `performance`, `powersave`). - `--format` — Output format +### `start-cli server epp [SET]` + +View or set the CPU energy/performance preference. StartOS applies `balance_power` +on Librem Mini v2 systems when no preference is saved and the setting is available. + +- `--format` — Output format + ### `start-cli server experimental zram` Enable or disable ZRAM compressed swap. diff --git a/projects/start-os/web/ui/src/app/services/api/mock-patch.ts b/projects/start-os/web/ui/src/app/services/api/mock-patch.ts index a9b1241ac2..22b6ca56e1 100644 --- a/projects/start-os/web/ui/src/app/services/api/mock-patch.ts +++ b/projects/start-os/web/ui/src/app/services/api/mock-patch.ts @@ -310,6 +310,7 @@ export const mockPatchData: DataModel = { echoipUrls: ['https://ipconfig.me', 'https://ifconfig.co'], zram: true, governor: 'performance', + epp: 'balance_power', ram: 8 * 1024 * 1024 * 1024, devices: [], kiosk: true, diff --git a/shared-libs/crates/start-core/locales/i18n.yaml b/shared-libs/crates/start-core/locales/i18n.yaml index d785fd8ca0..1573ff8d80 100644 --- a/shared-libs/crates/start-core/locales/i18n.yaml +++ b/shared-libs/crates/start-core/locales/i18n.yaml @@ -333,6 +333,13 @@ setup.password-required: pl_PL: "Hasło jest wymagane do nowej konfiguracji" # system.rs +system.epp-not-available: + en_US: "Energy performance preference %{epp} not available" + de_DE: "Energie-Leistungs-Präferenz %{epp} nicht verfügbar" + es_ES: "La preferencia de energía/rendimiento %{epp} no está disponible" + fr_FR: "La préférence énergie/performance %{epp} n'est pas disponible" + pl_PL: "Preferencja energii/wydajności %{epp} nie jest dostępna" + system.governor-not-available: en_US: "Governor %{governor} not available" de_DE: "Governor %{governor} nicht verfügbar" @@ -1408,6 +1415,13 @@ init.error-running-script: fr_FR: "Erreur lors de l'exécution de %{script} : %{error}" pl_PL: "Błąd uruchamiania %{script}: %{error}" +init.cpu-epp-not-available: + en_US: 'CPU Energy Performance Preference "%{epp}" Not Available' + de_DE: 'CPU-Energie-Leistungs-Präferenz "%{epp}" nicht verfügbar' + es_ES: 'Preferencia de energía/rendimiento de CPU "%{epp}" no disponible' + fr_FR: 'Préférence énergie/performance CPU "%{epp}" non disponible' + pl_PL: 'Preferencja energii/wydajności CPU "%{epp}" niedostępna' + init.cpu-governor-not-available: en_US: 'CPU Governor "%{governor}" Not Available' de_DE: 'CPU-Governor "%{governor}" nicht verfügbar' @@ -1415,6 +1429,13 @@ init.cpu-governor-not-available: fr_FR: 'Gouverneur CPU "%{governor}" non disponible' pl_PL: 'Governor CPU "%{governor}" niedostępny' +init.setting-cpu-epp: + en_US: 'Setting CPU Energy Performance Preference to "%{epp}"' + de_DE: 'Setze CPU-Energie-Leistungs-Präferenz auf "%{epp}"' + es_ES: 'Configurando la preferencia de energía/rendimiento de CPU a "%{epp}"' + fr_FR: 'Configuration de la préférence énergie/performance CPU à "%{epp}"' + pl_PL: 'Ustawianie preferencji energii/wydajności CPU na "%{epp}"' + init.setting-cpu-governor: en_US: 'Setting CPU Governor to "%{governor}"' de_DE: 'Setze CPU-Governor auf "%{governor}"' @@ -3404,6 +3425,13 @@ help.arg.event-id: fr_FR: "Identifiant unique de l'événement" pl_PL: "Unikalny identyfikator zdarzenia" +help.arg.epp-name: + en_US: "CPU energy performance preference" + de_DE: "CPU-Energie-Leistungs-Präferenz" + es_ES: "Preferencia de energía/rendimiento de CPU" + fr_FR: "Préférence énergie/performance du CPU" + pl_PL: "Preferencja energii/wydajności CPU" + help.arg.except-actions: en_US: "Action IDs to exclude" de_DE: "Auszuschließende Aktions-IDs" @@ -5026,11 +5054,11 @@ about.command-set-country: pl_PL: "Polecenie ustawienia kraju" about.commands-experimental: - en_US: "Commands related to configuring experimental options such as zram and cpu governor" - de_DE: "Befehle zur Konfiguration experimenteller Optionen wie zram und CPU-Governor" - es_ES: "Comandos relacionados con la configuración de opciones experimentales como zram y gobernador de CPU" - fr_FR: "Commandes liées à la configuration d'options expérimentales comme zram et le gouverneur CPU" - pl_PL: "Polecenia konfiguracji opcji eksperymentalnych jak zram i regulator CPU" + en_US: "Commands related to configuring experimental options" + de_DE: "Befehle zur Konfiguration experimenteller Optionen" + es_ES: "Comandos relacionados con la configuración de opciones experimentales" + fr_FR: "Commandes liées à la configuration d'options expérimentales" + pl_PL: "Polecenia konfiguracji opcji eksperymentalnych" about.commands-host-address-domain: en_US: "Commands for managing host address domains" @@ -6567,6 +6595,13 @@ about.setup-status: fr_FR: "Afficher l'état actuel de l'assistant d'installation" pl_PL: "Wyświetl obecny stan kreatora konfiguracji" +about.show-cpu-epp: + en_US: "Show CPU energy performance preferences" + de_DE: "CPU-Energie-Leistungs-Präferenzen anzeigen" + es_ES: "Mostrar preferencias de energía/rendimiento de CPU" + fr_FR: "Afficher les préférences énergie/performance du CPU" + pl_PL: "Pokaż preferencje energii/wydajności CPU" + about.show-cpu-governors: en_US: "Show CPU governors" de_DE: "CPU-Governors anzeigen" diff --git a/shared-libs/crates/start-core/src/db/model/public.rs b/shared-libs/crates/start-core/src/db/model/public.rs index e3399bc94c..c015f8cb4b 100644 --- a/shared-libs/crates/start-core/src/db/model/public.rs +++ b/shared-libs/crates/start-core/src/db/model/public.rs @@ -28,7 +28,7 @@ use crate::net::vhost::{AlpnInfo, PassthroughInfo}; use crate::prelude::*; use crate::progress::FullProgress; use crate::system::{KeyboardOptions, SmtpValue}; -use crate::util::cpupower::Governor; +use crate::util::cpupower::{Epp, Governor}; use crate::util::lshw::LshwDevice; use crate::util::serde::MaybeUtf8String; use crate::version::{Current, VersionT}; @@ -146,6 +146,7 @@ impl Public { ntp_synced: false, zram: true, governor: None, + epp: None, smtp: None, echoip_urls: default_echoip_urls(), ram: 0, @@ -197,6 +198,8 @@ pub struct ServerInfo { #[serde(default)] pub zram: bool, pub governor: Option, + #[serde(default)] + pub epp: Option, pub smtp: Option, #[serde(default = "default_echoip_urls")] #[ts(type = "string[]")] diff --git a/shared-libs/crates/start-core/src/firmware.rs b/shared-libs/crates/start-core/src/firmware.rs index f053bc743a..b99847df33 100644 --- a/shared-libs/crates/start-core/src/firmware.rs +++ b/shared-libs/crates/start-core/src/firmware.rs @@ -49,9 +49,8 @@ pub fn display_firmware_update_result(result: RequiresReboot) { } } -#[instrument] -pub async fn check_for_firmware_update() -> Result, Error> { - let system_product_name = String::from_utf8( +pub(crate) async fn system_product_name() -> Result { + Ok(String::from_utf8( Command::new("dmidecode") .arg("-s") .arg("system-product-name") @@ -59,7 +58,12 @@ pub async fn check_for_firmware_update() -> Result, Error> { .await?, )? .trim() - .to_owned(); + .to_owned()) +} + +#[instrument] +pub async fn check_for_firmware_update() -> Result, Error> { + let system_product_name = system_product_name().await?; let bios_version = String::from_utf8( Command::new("dmidecode") .arg("-s") diff --git a/shared-libs/crates/start-core/src/init.rs b/shared-libs/crates/start-core/src/init.rs index bc91ee0b43..c2ac3ec782 100644 --- a/shared-libs/crates/start-core/src/init.rs +++ b/shared-libs/crates/start-core/src/init.rs @@ -314,25 +314,44 @@ pub async fn init( let server_info = db.peek().await.into_public().into_server_info(); set_governor.start(); let selected_governor = server_info.as_governor().de()?; - let governor = if let Some(governor) = &selected_governor { - if cpupower::get_available_governors() - .await? - .contains(governor) - { - Some(governor) - } else { + let available_governors = cpupower::get_available_governors() + .await + .log_err() + .unwrap_or_default(); + let governor = match &selected_governor { + Some(governor) if available_governors.contains(governor) => Some(governor), + Some(governor) => { tracing::warn!( "{}", t!("init.cpu-governor-not-available", governor = governor) ); None } - } else { - cpupower::get_preferred_governor().await? + None => cpupower::preferred_governor(&available_governors), }; if let Some(governor) = governor { tracing::info!("{}", t!("init.setting-cpu-governor", governor = governor)); - cpupower::set_governor(governor).await?; + cpupower::set_governor(governor).await.log_err(); + } + + let selected_epp = server_info.as_epp().de()?; + let system_product_name = if selected_epp.is_none() { + crate::firmware::system_product_name().await.log_err() + } else { + None + }; + let epp = cpupower::preferred_epp(selected_epp, system_product_name.as_deref()); + if let Some(epp) = &epp { + let available_epps = cpupower::get_available_epps() + .await + .log_err() + .unwrap_or_default(); + if available_epps.contains(epp) { + tracing::info!("{}", t!("init.setting-cpu-epp", epp = epp)); + cpupower::set_epp(epp).await.log_err(); + } else { + tracing::warn!("{}", t!("init.cpu-epp-not-available", epp = epp)); + } } set_governor.complete(); diff --git a/shared-libs/crates/start-core/src/lib.rs b/shared-libs/crates/start-core/src/lib.rs index 0d6f2e29ca..cee5b05402 100644 --- a/shared-libs/crates/start-core/src/lib.rs +++ b/shared-libs/crates/start-core/src/lib.rs @@ -286,6 +286,26 @@ pub fn server() -> ParentHandler { .with_call_remote::(), ), ) + .subcommand( + "governor", + from_fn_async(system::governor) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + system::display_governor_info(handle.params, result) + }) + .with_about("about.show-cpu-governors") + .with_call_remote::(), + ) + .subcommand( + "epp", + from_fn_async(system::epp) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + system::display_epp_info(handle.params, result) + }) + .with_about("about.show-cpu-epp") + .with_call_remote::(), + ) .subcommand( "experimental", system::experimental::().with_about("about.commands-experimental"), diff --git a/shared-libs/crates/start-core/src/ssh.rs b/shared-libs/crates/start-core/src/ssh.rs index 9910721b5d..f2b0f409db 100644 --- a/shared-libs/crates/start-core/src/ssh.rs +++ b/shared-libs/crates/start-core/src/ssh.rs @@ -196,7 +196,7 @@ fn display_all_ssh_keys( use prettytable::*; if let Some(format) = params.format { - return display_serializable(format, params); + return display_serializable(format, result); } let mut table = Table::new(); diff --git a/shared-libs/crates/start-core/src/system/mod.rs b/shared-libs/crates/start-core/src/system/mod.rs index 8669a066d8..076474584b 100644 --- a/shared-libs/crates/start-core/src/system/mod.rs +++ b/shared-libs/crates/start-core/src/system/mod.rs @@ -24,31 +24,22 @@ use crate::registry::device_info::DeviceInfo; use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::shutdown::Shutdown; use crate::util::Invoke; -use crate::util::cpupower::{Governor, get_available_governors, set_governor}; +use crate::util::cpupower::{ + Epp, Governor, current_epp, get_available_epps, get_available_governors, set_epp, set_governor, +}; use crate::util::io::{copy_file, open_file, write_file_atomic}; -use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable}; +use crate::util::serde::{WithIoFormat, display_serializable}; use crate::util::sync::Watch; use crate::{MAIN_DATA, PACKAGE_DATA}; pub fn experimental() -> ParentHandler { - ParentHandler::new() - .subcommand( - "zram", - from_fn_async(zram) - .no_display() - .with_about("about.enable-zram") - .with_call_remote::(), - ) - .subcommand( - "governor", - from_fn_async(governor) - .with_display_serializable() - .with_custom_display_fn(|handle, result| { - display_governor_info(handle.params, result) - }) - .with_about("about.show-cpu-governors") - .with_call_remote::(), - ) + ParentHandler::new().subcommand( + "zram", + from_fn_async(zram) + .no_display() + .with_about("about.enable-zram") + .with_call_remote::(), + ) } pub async fn enable_zram() -> Result<(), Error> { @@ -174,14 +165,14 @@ pub struct GovernorInfo { available: BTreeSet, } -fn display_governor_info( +pub(crate) fn display_governor_info( params: WithIoFormat, result: GovernorInfo, ) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { - return display_serializable(format, params); + return display_serializable(format, result); } let mut table = Table::new(); @@ -1460,3 +1451,69 @@ pub async fn test_get_mem_info() { pub async fn test_get_disk_usage() { println!("{:?}", get_disk_info().await.unwrap()) } + +#[derive(Debug, Deserialize, Serialize)] +pub struct EppInfo { + current: Option, + available: BTreeSet, +} + +pub(crate) fn display_epp_info( + params: WithIoFormat, + result: EppInfo, +) -> Result<(), Error> { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, result); + } + + let mut table = Table::new(); + table.add_row(row![bc -> "ENERGY PERFORMANCE PREFERENCES"]); + for entry in result.available { + if Some(&entry) == result.current.as_ref() { + table.add_row(row![g -> format!("* {entry} (current)")]); + } else { + table.add_row(row![entry]); + } + } + table.print_tty(false)?; + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[group(skip)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct EppParams { + #[arg(help = "help.arg.epp-name")] + set: Option, +} + +/// `current` is read from the hardware rather than from the database, which +/// holds only an explicit override. +pub async fn epp(ctx: RpcContext, EppParams { set, .. }: EppParams) -> Result { + let available = get_available_epps().await?; + if let Some(set) = set { + if !available.contains(&set) { + return Err(Error::new( + eyre!("{}", t!("system.epp-not-available", epp = set.to_string())), + ErrorKind::InvalidRequest, + )); + } + set_epp(&set).await?; + ctx.db + .mutate(|d| { + d.as_public_mut() + .as_server_info_mut() + .as_epp_mut() + .ser(&Some(set)) + }) + .await + .result?; + } + Ok(EppInfo { + current: current_epp().await?, + available, + }) +} diff --git a/shared-libs/crates/start-core/src/util/cpupower.rs b/shared-libs/crates/start-core/src/util/cpupower.rs index fbd0a61884..b80e138c42 100644 --- a/shared-libs/crates/start-core/src/util/cpupower.rs +++ b/shared-libs/crates/start-core/src/util/cpupower.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; use imbl::OrdMap; use tokio::process::Command; @@ -7,6 +8,7 @@ use ts_rs::TS; use crate::prelude::*; use crate::util::Invoke; +use crate::util::io::maybe_read_file_to_string; pub const GOVERNOR_HEIRARCHY: &[Governor] = &[ Governor(Cow::Borrowed("ondemand")), @@ -131,14 +133,10 @@ pub async fn current_governor() -> Result, Error> { )) } -pub async fn get_preferred_governor() -> Result, Error> { - let governors = get_available_governors().await?; - for governor in GOVERNOR_HEIRARCHY { - if governors.contains(governor) { - return Ok(Some(governor)); - } - } - Ok(None) +pub fn preferred_governor(available: &BTreeSet) -> Option<&'static Governor> { + GOVERNOR_HEIRARCHY + .iter() + .find(|governor| available.contains(*governor)) } pub async fn set_governor(governor: &Governor) -> Result<(), Error> { @@ -150,3 +148,126 @@ pub async fn set_governor(governor: &Governor) -> Result<(), Error> { .await?; Ok(()) } + +const CPU_ROOT: &str = "/sys/devices/system/cpu"; + +/// Selects how aggressively an EPP-capable CPU pursues performance. +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, TS, +)] +#[ts(export, type = "string")] +pub struct Epp(Cow<'static, str>); + +const LIBREM_MINI_V2: &str = "librem_mini_v2"; +const LIBREM_MINI_V2_EPP: Epp = Epp(Cow::Borrowed("balance_power")); + +impl std::str::FromStr for Epp { + type Err = std::convert::Infallible; + fn from_str(s: &str) -> Result { + Ok(Self(s.to_owned().into())) + } +} +impl std::fmt::Display for Epp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} +impl std::ops::Deref for Epp { + type Target = str; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} +impl std::borrow::Borrow for Epp { + fn borrow(&self) -> &str { + &**self + } +} + +fn epp_path(cpu: &Path) -> PathBuf { + cpu.join("cpufreq/energy_performance_preference") +} + +async fn epp_paths() -> Result, Error> { + let mut dir = match tokio::fs::read_dir(CPU_ROOT).await { + Ok(dir) => dir, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(e) => return Err(e).with_kind(ErrorKind::Filesystem), + }; + let mut paths = Vec::new(); + while let Some(entry) = dir.next_entry().await.with_kind(ErrorKind::Filesystem)? { + let path = epp_path(&entry.path()); + if tokio::fs::try_exists(&path).await.unwrap_or(false) { + paths.push(path); + } + } + paths.sort(); + Ok(paths) +} + +pub async fn get_available_epps() -> Result, Error> { + let path = Path::new(CPU_ROOT).join("cpu0/cpufreq/energy_performance_available_preferences"); + Ok(maybe_read_file_to_string(path) + .await? + .into_iter() + .flat_map(|raw| { + raw.split_ascii_whitespace() + .map(|e| Epp(Cow::Owned(e.to_owned()))) + .collect::>() + }) + .collect()) +} + +pub async fn current_epp() -> Result, Error> { + Ok( + maybe_read_file_to_string(epp_path(&Path::new(CPU_ROOT).join("cpu0"))) + .await? + .map(|raw| Epp(Cow::Owned(raw.trim().to_owned()))), + ) +} + +pub(crate) fn preferred_epp( + selected: Option, + system_product_name: Option<&str>, +) -> Option { + selected.or_else(|| { + (system_product_name == Some(LIBREM_MINI_V2)).then(|| LIBREM_MINI_V2_EPP.clone()) + }) +} + +pub async fn set_epp(epp: &Epp) -> Result<(), Error> { + for path in epp_paths().await? { + tokio::fs::write(&path, &*epp.0) + .await + .with_ctx(|_| (ErrorKind::CpuSettings, path.display().to_string()))?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn librem_mini_v2_defaults_to_balance_power() { + assert_eq!( + preferred_epp(None, Some("librem_mini_v2")), + Some(Epp(Cow::Borrowed("balance_power"))) + ); + } + + #[test] + fn selected_epp_overrides_the_librem_mini_v2_default() { + let selected = Epp(Cow::Borrowed("performance")); + assert_eq!( + preferred_epp(Some(selected.clone()), Some("librem_mini_v2")), + Some(selected) + ); + } + + #[test] + fn other_products_have_no_default_epp() { + assert_eq!(preferred_epp(None, Some("other")), None); + assert_eq!(preferred_epp(None, None), None); + } +} diff --git a/shared-libs/ts-modules/start-core/lib/osBindings/Epp.ts b/shared-libs/ts-modules/start-core/lib/osBindings/Epp.ts new file mode 100644 index 0000000000..9c6b00003b --- /dev/null +++ b/shared-libs/ts-modules/start-core/lib/osBindings/Epp.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Selects how aggressively an EPP-capable CPU pursues performance. + */ +export type Epp = string diff --git a/shared-libs/ts-modules/start-core/lib/osBindings/ServerInfo.ts b/shared-libs/ts-modules/start-core/lib/osBindings/ServerInfo.ts index 370bf5c166..39327893e2 100644 --- a/shared-libs/ts-modules/start-core/lib/osBindings/ServerInfo.ts +++ b/shared-libs/ts-modules/start-core/lib/osBindings/ServerInfo.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Epp } from './Epp' import type { Governor } from './Governor' import type { KeyboardOptions } from './KeyboardOptions' import type { LshwDevice } from './LshwDevice' @@ -22,6 +23,7 @@ export type ServerInfo = { ntpSynced: boolean zram: boolean governor: Governor | null + epp: Epp | null smtp: SmtpValue | null echoipUrls: string[] ram: number diff --git a/shared-libs/ts-modules/start-core/lib/osBindings/index.ts b/shared-libs/ts-modules/start-core/lib/osBindings/index.ts index 4de6775b48..10568f0ec8 100644 --- a/shared-libs/ts-modules/start-core/lib/osBindings/index.ts +++ b/shared-libs/ts-modules/start-core/lib/osBindings/index.ts @@ -107,6 +107,7 @@ export { EchoParams } from './EchoParams' export { EditSignerParams } from './EditSignerParams' export { EffectsRunActionParams } from './EffectsRunActionParams' export { EncryptedWire } from './EncryptedWire' +export { Epp } from './Epp' export { ErrorData } from './ErrorData' export { EventId } from './EventId' export { ExportActionParams } from './ExportActionParams'