Skip to content

Commit

Permalink
tuned-ppd: Make it possible to change profiles using Fn keys
Browse files Browse the repository at this point in the history
This applies only to some Thinkpad laptops, where function
keys can be used to change the ACPI platform profile. When
the new  option `thinkpad_function_keys` is enabled, the
platform profile is watched for changes using inotify.
  • Loading branch information
zacikpa committed Nov 29, 2024
1 parent 33cd370 commit 9fa2e74
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 1 deletion.
13 changes: 13 additions & 0 deletions tuned/ppd/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
BATTERY_SECTION = "battery"
DEFAULT_PROFILE_OPTION = "default"
BATTERY_DETECTION_OPTION = "battery_detection"
THINKPAD_FUNCTION_KEYS_OPTION = "thinkpad_function_keys"


class ProfileMap:
Expand Down Expand Up @@ -66,6 +67,16 @@ def ppd_to_tuned(self):
"""
return self._ppd_to_tuned

@property
def thinkpad_function_keys(self):
"""
Whether to react to changes of ACPI platform profile
done via function keys (e.g., Fn-L) on newer Thinkpad
machines. Experimental feature.
"""
return self._thinkpad_function_keys


def load_from_file(self, config_file):
"""
Loads the configuration from the provided file.
Expand Down Expand Up @@ -116,3 +127,5 @@ def load_from_file(self, config_file):
raise TunedException("Unknown PPD profiles in the battery section: " + ", ".join(unknown_battery_profiles))

self._ppd_to_tuned = ProfileMap(profile_dict_ac, profile_dict_dc)

self._thinkpad_function_keys = cfg.getboolean(MAIN_SECTION, THINKPAD_FUNCTION_KEYS_OPTION, fallback=False)
72 changes: 71 additions & 1 deletion tuned/ppd/controller.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from tuned import exports, logs
from tuned.utils.commands import commands
from tuned.consts import PPD_CONFIG_FILE, PPD_BASE_PROFILE_FILE, PPD_API_COMPATIBILITY
from tuned.ppd.config import PPDConfig, PPD_PERFORMANCE, PPD_POWER_SAVER
from tuned.ppd.config import PPDConfig, PPD_PERFORMANCE, PPD_BALANCED, PPD_POWER_SAVER

from enum import StrEnum
import pyinotify
import threading
import dbus
import os
import time

log = logs.get()

Expand All @@ -19,6 +21,14 @@
UPOWER_DBUS_PATH = "/org/freedesktop/UPower"
UPOWER_DBUS_INTERFACE = "org.freedesktop.UPower"

PLATFORM_PROFILE_PATH = "/sys/firmware/acpi/platform_profile"
PLATFORM_PROFILE_MAPPING = {
"low-power": PPD_POWER_SAVER,
"balanced": PPD_BALANCED,
"performance": PPD_PERFORMANCE
}


class PerformanceDegraded(StrEnum):
"""
Possible reasons for performance degradation.
Expand All @@ -43,6 +53,51 @@ def process_IN_MODIFY(self, event):
self._controller.check_performance_degraded()


class PlatformProfileEventHandler(pyinotify.ProcessEvent):
"""
Event handler for switching PPD profiles based on the
ACPI platform profile
This handler should only invoke a PPD profile change if the
change of the file at PLATFORM_PROFILE_PATH comes from within
the kernel (e.g., when the user presses Fn-L on a Thinkpad laptop).
This is currently detected as the file being modified without
being opened before.
"""
CLOSE_MODIFY_BUFFER = 0.1

def __init__(self, controller):
super(PlatformProfileEventHandler, self).__init__()
self._controller = controller
self._file_open = False
self._last_close = 0

def process_IN_OPEN(self, event):
if event.pathname != PLATFORM_PROFILE_PATH:
return
self._file_open = True
self._last_close = 0

def process_IN_CLOSE_WRITE(self, event):
if event.pathname != PLATFORM_PROFILE_PATH:
return
self._file_open = False
self._last_close = time.time()

def process_IN_CLOSE_NOWRITE(self, event):
if event.pathname != PLATFORM_PROFILE_PATH:
return
self._file_open = False

def process_IN_MODIFY(self, event):
if event.pathname != PLATFORM_PROFILE_PATH or self._file_open or self._last_close + self.CLOSE_MODIFY_BUFFER > time.time():
# Do not invoke a profile change if a modify event comes:
# 1. when the file is open,
# 2. directly after the file is closed (the events may sometimes come in the wrong order).
return
self._controller.check_platform_profile()


class ProfileHold(object):
"""
Class holding information about a single profile hold,
Expand Down Expand Up @@ -163,6 +218,7 @@ def __init__(self, bus, tuned_interface):
self._tuned_interface = tuned_interface
self._cmd = commands()
self._terminate = threading.Event()
self._platform_profile_supported = os.path.isfile(PLATFORM_PROFILE_PATH)
self._no_turbo_supported = os.path.isfile(NO_TURBO_PATH)
self._lap_mode_supported = os.path.isfile(LAP_MODE_PATH)
self.initialize()
Expand Down Expand Up @@ -202,6 +258,16 @@ def check_performance_degraded(self):
self._performance_degraded = performance_degraded
exports.property_changed("PerformanceDegraded", performance_degraded)

def check_platform_profile(self):
"""
Sets the active PPD profile based on the content of the ACPI platform profile.
"""
platform_profile = self._cmd.read_file(PLATFORM_PROFILE_PATH).strip()
if platform_profile not in PLATFORM_PROFILE_MAPPING:
return
log.debug("Platform profile changed: %s" % platform_profile)
self.set_active_profile(PLATFORM_PROFILE_MAPPING[platform_profile])

def _load_base_profile(self):
"""
Loads and returns the saved PPD base profile.
Expand Down Expand Up @@ -259,6 +325,10 @@ def run(self):
watches |= watch_manager.add_watch(path=os.path.dirname(LAP_MODE_PATH),
mask=pyinotify.IN_MODIFY,
proc_fun=PerformanceDegradedEventHandler(LAP_MODE_PATH, self))
if self._platform_profile_supported and self._config.thinkpad_function_keys:
watches |= watch_manager.add_watch(path=os.path.dirname(PLATFORM_PROFILE_PATH),
mask=pyinotify.IN_OPEN | pyinotify.IN_MODIFY | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CLOSE_NOWRITE,
proc_fun=PlatformProfileEventHandler(self))
if watches:
notifier.start()
while not self._cmd.wait(self._terminate, 1):
Expand Down

0 comments on commit 9fa2e74

Please sign in to comment.