1
0
Fork 0

tuned-ppd: Keep track of active and base profile

Instead of querying TuneD each time we want to determine the active
TuneD profile, remember its value. Also watch for signals from TuneD,
updating the value when the TuneD profile changes (resolves #689).

The daemon now also keeps track of the "base" PPD profile, which is
restored when all profile holds are released or when tuned-ppd is
restarted. For the latter purpose, this profile is also saved in a file.

Direct access via two dictionaries (one for AC, one for DC)
was clumsy, this commit replaces it with a new class - ProfileMap.
This commit is contained in:
Pavol Žáčik 2024-08-08 13:33:41 +02:00
parent 79547f093f
commit d5bd033422
No known key found for this signature in database
GPG key ID: B99527D5E6487A92
5 changed files with 100 additions and 51 deletions

View file

@ -178,6 +178,7 @@ install: install-dirs
echo -n > $(DESTDIR)$(TUNED_CFG_DIR)/active_profile
echo -n > $(DESTDIR)$(TUNED_CFG_DIR)/profile_mode
echo -n > $(DESTDIR)$(TUNED_CFG_DIR)/post_loaded_profile
echo -n > $(DESTDIR)$(TUNED_CFG_DIR)/ppd_base_profile
install -Dpm 0644 bootcmdline $(DESTDIR)$(TUNED_CFG_DIR)/bootcmdline
install -Dpm 0644 modules.conf $(DESTDIR)$(SYSCONFDIR)/modprobe.d/tuned.conf

View file

@ -504,6 +504,7 @@ fi
%config(noreplace) %verify(not size mtime md5) %{_sysconfdir}/tuned/active_profile
%config(noreplace) %verify(not size mtime md5) %{_sysconfdir}/tuned/profile_mode
%config(noreplace) %verify(not size mtime md5) %{_sysconfdir}/tuned/post_loaded_profile
%config(noreplace) %verify(not size mtime md5) %{_sysconfdir}/tuned/ppd_base_profile
%config(noreplace) %{_sysconfdir}/tuned/tuned-main.conf
%config(noreplace) %verify(not size mtime md5) %{_sysconfdir}/tuned/bootcmdline
%verify(not size mtime md5) %{_sysconfdir}/modprobe.d/tuned.conf

View file

@ -102,6 +102,7 @@ PPD_DBUS_BUS = PPD_NAMESPACE
PPD_DBUS_OBJECT = "/net/hadess/PowerProfiles"
PPD_DBUS_INTERFACE = PPD_DBUS_BUS
PPD_CONFIG_FILE = "/etc/tuned/ppd.conf"
PPD_BASE_PROFILE_FILE = "/etc/tuned/ppd_base_profile"
# After adding new option to tuned-main.conf add here its name with CFG_ prefix
# and eventually default value with CFG_DEF_ prefix (default is None)

View file

@ -12,8 +12,23 @@ DEFAULT_PROFILE_OPTION = "default"
BATTERY_DETECTION_OPTION = "battery_detection"
class ProfileMap:
def __init__(self, ac_map, dc_map):
self._ac_map = ac_map
self._dc_map = dc_map
def get(self, profile, on_battery):
profile_map = self._dc_map if on_battery else self._ac_map
return profile_map[profile]
def keys(self, on_battery):
profile_map = self._dc_map if on_battery else self._ac_map
return profile_map.keys()
class PPDConfig:
def __init__(self, config_file):
def __init__(self, config_file, tuned_interface):
self._tuned_interface = tuned_interface
self.load_from_file(config_file)
@property
@ -32,10 +47,6 @@ class PPDConfig:
def tuned_to_ppd(self):
return self._tuned_to_ppd
@property
def ppd_to_tuned_battery(self):
return self._ppd_to_tuned_battery
def load_from_file(self, config_file):
cfg = ConfigParser()
@ -48,38 +59,41 @@ class PPDConfig:
if PROFILES_SECTION not in cfg:
raise TunedException("Missing profiles section in the configuration file '%s'" % config_file)
self._ppd_to_tuned = dict(cfg[PROFILES_SECTION])
profile_dict_ac = dict(cfg[PROFILES_SECTION])
if not all(isinstance(mapped_profile, str) for mapped_profile in self._ppd_to_tuned.values()):
raise TunedException("Invalid profile mapping in the configuration file '%s'" % config_file)
if len(set(self._ppd_to_tuned.values())) != len(self._ppd_to_tuned):
raise TunedException("Duplicate profile mapping in the configuration file '%s'" % config_file)
self._tuned_to_ppd = {v: k for k, v in self._ppd_to_tuned.items()}
if PPD_POWER_SAVER not in self._ppd_to_tuned:
if PPD_POWER_SAVER not in profile_dict_ac:
raise TunedException("Missing power-saver profile in the configuration file '%s'" % config_file)
if PPD_PERFORMANCE not in self._ppd_to_tuned:
if PPD_PERFORMANCE not in profile_dict_ac:
raise TunedException("Missing performance profile in the configuration file '%s'" % config_file)
if MAIN_SECTION not in cfg or DEFAULT_PROFILE_OPTION not in cfg[MAIN_SECTION]:
raise TunedException("Missing default profile in the configuration file '%s'" % config_file)
self._default_profile = cfg[MAIN_SECTION][DEFAULT_PROFILE_OPTION]
if self._default_profile not in self._ppd_to_tuned:
raise TunedException("Unknown default profile '%s'" % self._default_profile)
if self._default_profile not in profile_dict_ac:
raise TunedException("Default profile '%s' missing in the profile mapping" % self._default_profile)
if BATTERY_DETECTION_OPTION not in cfg[MAIN_SECTION]:
raise TunedException("Missing battery detection option in the configuration file '%s'" % config_file)
self._ppd_to_tuned_battery = self._ppd_to_tuned
self._battery_detection = cfg.getboolean(MAIN_SECTION, BATTERY_DETECTION_OPTION)
if self._battery_detection:
if BATTERY_SECTION not in cfg:
raise TunedException("Missing battery section in the configuration file '%s'" % config_file)
for k, _v in dict(cfg[PROFILES_SECTION]).items():
if k in cfg[BATTERY_SECTION].keys():
self._tuned_to_ppd = self._tuned_to_ppd | {cfg[BATTERY_SECTION][k]:k}
for k, v in dict(cfg[BATTERY_SECTION]).items():
if k in cfg[PROFILES_SECTION].keys():
self._ppd_to_tuned_battery = self._ppd_to_tuned_battery | {k:v}
self._battery_detection = cfg.getboolean(MAIN_SECTION, BATTERY_DETECTION_OPTION, fallback=BATTERY_SECTION in cfg)
if self._battery_detection and BATTERY_SECTION not in cfg:
raise TunedException("Missing battery section in the configuration file '%s'" % config_file)
profile_dict_dc = profile_dict_ac | dict(cfg[BATTERY_SECTION]) if self._battery_detection else profile_dict_ac
# Make sure all of the TuneD profiles specified in the configuration file actually exist
unknown_tuned_profiles = (set(profile_dict_ac.values()) | set(profile_dict_dc.values())) - set(self._tuned_interface.profiles())
if unknown_tuned_profiles:
raise TunedException("Unknown TuneD profiles in the configuration file: " + ", ".join(unknown_tuned_profiles))
# Make sure there are no PPD profiles appearing in the battery section which are not defined before
unknown_battery_profiles = set(profile_dict_dc.keys()) - set(profile_dict_ac.keys())
if unknown_battery_profiles:
raise TunedException("Unknown PPD profiles in the battery section: " + ", ".join(unknown_battery_profiles))
# Make sure the profile mapping is injective so it can be reverted
if len(set(profile_dict_ac.values())) != len(profile_dict_ac) or len(set(profile_dict_dc.values())) != len(profile_dict_dc):
raise TunedException("Duplicate profile mapping in the configuration file '%s'" % config_file)
self._ppd_to_tuned = ProfileMap(profile_dict_ac, profile_dict_dc)
self._tuned_to_ppd = ProfileMap({v: k for k, v in profile_dict_ac.items()}, {v: k for k, v in profile_dict_dc.items()})

View file

@ -1,6 +1,6 @@
from tuned import exports, logs
from tuned.utils.commands import commands
from tuned.consts import PPD_CONFIG_FILE
from tuned.consts import PPD_CONFIG_FILE, PPD_BASE_PROFILE_FILE
from tuned.ppd.config import PPDConfig, PPD_PERFORMANCE, PPD_POWER_SAVER
from enum import StrEnum
import threading
@ -105,13 +105,30 @@ class Controller(exports.interfaces.ExportableInterface):
self._terminate = threading.Event()
self._battery_handler = None
self._on_battery = False
self._tuned_interface.connect_to_signal("profile_changed", self._tuned_profile_changed)
self.initialize()
def _upower_changed(self, interface, changed, invalidated):
self._on_battery = bool(self._upower_properties.Get(UPOWER_DBUS_INTERFACE, "OnBattery"))
log.info("Battery status changed: " + ("DC (battery)" if self._on_battery else "AC (charging)"))
tuned_profile = self._config.ppd_to_tuned_battery[self._base_profile] if self._on_battery else self._config.ppd_to_tuned[self._base_profile]
self.switch_profile(tuned_profile)
self.switch_profile(self._active_profile)
def _tuned_profile_changed(self, tuned_profile, result, errstr):
if not result:
return
self._profile_holds.clear()
try:
ppd_profile = self._config.tuned_to_ppd.get(tuned_profile, self._on_battery)
except KeyError:
ppd_profile = UNKNOWN_PROFILE
log.warning("TuneD profile changed to an unknown profile '%s'" % tuned_profile)
if self._active_profile != ppd_profile:
log.info("Profile changed to '%s'" % ppd_profile)
self._active_profile = ppd_profile
exports.property_changed("ActiveProfile", self._active_profile)
if ppd_profile != UNKNOWN_PROFILE:
self._base_profile = ppd_profile
self._save_base_profile(ppd_profile)
def _setup_battery_signaling(self):
self._on_battery = False
@ -140,14 +157,31 @@ class Controller(exports.interfaces.ExportableInterface):
self._performance_degraded = performance_degraded
exports.property_changed("PerformanceDegraded", performance_degraded)
def _load_base_profile(self):
return self._cmd.read_file(PPD_BASE_PROFILE_FILE, no_error=True).strip() or None
def _save_base_profile(self, profile):
self._cmd.write_to_file(PPD_BASE_PROFILE_FILE, profile + "\n")
def _set_tuned_profile(self, tuned_profile):
active_tuned_profile = self._tuned_interface.active_profile()
if active_tuned_profile == tuned_profile:
return True
log.info("Setting TuneD profile to '%s'" % tuned_profile)
ok, error_msg = self._tuned_interface.switch_profile(tuned_profile)
if not ok:
log.error(str(error_msg))
return bool(ok)
def initialize(self):
self._active_profile = None
self._profile_holds = ProfileHoldManager(self)
self._performance_degraded = PerformanceDegraded.NONE
self._config = PPDConfig(PPD_CONFIG_FILE)
self._config = PPDConfig(PPD_CONFIG_FILE, self._tuned_interface)
self._setup_battery_signaling()
active_profile = self.active_profile()
self._base_profile = active_profile if active_profile != UNKNOWN_PROFILE else self._config.default_profile
self._base_profile = self._load_base_profile() or self._config.default_profile
self.switch_profile(self._base_profile)
self._save_base_profile(self._base_profile)
def run(self):
exports.start()
@ -167,16 +201,12 @@ class Controller(exports.interfaces.ExportableInterface):
self._terminate.set()
def switch_profile(self, profile):
if self.active_profile() == profile:
return
tuned_profile = self._config.ppd_to_tuned_battery[profile] if self._on_battery else self._config.ppd_to_tuned[profile]
log.info("Switching to profile '%s'" % tuned_profile)
self._tuned_interface.switch_profile(tuned_profile)
exports.property_changed("ActiveProfile", profile)
def active_profile(self):
tuned_profile = self._tuned_interface.active_profile()
return self._config.tuned_to_ppd.get(tuned_profile, UNKNOWN_PROFILE)
if not self._set_tuned_profile(self._config.ppd_to_tuned.get(profile, self._on_battery)):
return False
if self._active_profile != profile:
exports.property_changed("ActiveProfile", profile)
self._active_profile = profile
return True
@exports.export("sss", "u")
def HoldProfile(self, profile, reason, app_id, caller):
@ -198,21 +228,23 @@ class Controller(exports.interfaces.ExportableInterface):
@exports.property_setter("ActiveProfile")
def set_active_profile(self, profile):
if profile not in self._config.ppd_to_tuned:
if profile not in self._config.ppd_to_tuned.keys(self._on_battery):
raise dbus.exceptions.DBusException("Invalid profile '%s'" % profile)
log.debug("Setting base profile to %s" % profile)
self._base_profile = profile
self._profile_holds.clear()
self.switch_profile(profile)
if not self.switch_profile(profile):
raise dbus.exceptions.DBusException("Error setting profile %s'" % profile)
self._base_profile = profile
self._save_base_profile(profile)
@exports.property_getter("ActiveProfile")
def get_active_profile(self):
return self.active_profile()
return self._active_profile
@exports.property_getter("Profiles")
def get_profiles(self):
return dbus.Array(
[{"Profile": profile, "Driver": DRIVER} for profile in self._config.ppd_to_tuned.keys()],
[{"Profile": profile, "Driver": DRIVER} for profile in self._config.ppd_to_tuned.keys(self._on_battery)],
signature="a{sv}",
)