From 908062253e5a7afbfdae1b4ed1bfe75e2490a139 Mon Sep 17 00:00:00 2001 From: "John B. Wyatt IV" Date: Thu, 10 Apr 2025 13:50:54 -0400 Subject: [PATCH] Add cpu_power cmd Resolves: RHEL-61750 Signed-off-by: John B. Wyatt IV --- ...mon-cpu-and-nics-determination-code-.patch | 162 ++++++++++ ...Add-idle_state-control-functionality.patch | 290 ++++++++++++++++++ 0003-tuna-utils-A-few-tweaks.patch | 54 ++++ tuna.spec | 12 +- 4 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 0001-tuna-extract-common-cpu-and-nics-determination-code-.patch create mode 100644 0002-tuna-Add-idle_state-control-functionality.patch create mode 100644 0003-tuna-utils-A-few-tweaks.patch diff --git a/0001-tuna-extract-common-cpu-and-nics-determination-code-.patch b/0001-tuna-extract-common-cpu-and-nics-determination-code-.patch new file mode 100644 index 0000000..8b9bf75 --- /dev/null +++ b/0001-tuna-extract-common-cpu-and-nics-determination-code-.patch @@ -0,0 +1,162 @@ +From e6c4991d6d628b5ab0e1767f3f44beba55e31c55 Mon Sep 17 00:00:00 2001 +From: "John B. Wyatt IV" +Date: Wed, 9 Apr 2025 15:31:32 -0400 +Subject: [PATCH 1/3] tuna: extract common cpu and nics determination code into + a utils.py file + +Extracting the code allows these previously local (global to the file) +variables and functions to be used in other files of tuna. Reducing +the number of globals makes the code cleaner and reduces the size of +tuna-cmd.py. + +Included a suggestion by Crystal to move a function from the latter +patch into utils.py and make it dependent on get_nr_cpus() (v2). + +Included a request by John Kacur to place the SPDX message at the top of +the file (v4). + +Suggested-by: Crystal Wood + +Signed-off-by: John B. Wyatt IV +Signed-off-by: John B. Wyatt IV +- minor spelling error fix +Signed-off-by: John Kacur +--- + tuna-cmd.py | 34 +++++++--------------------------- + tuna/utils.py | 28 ++++++++++++++++++++++++++++ + 2 files changed, 35 insertions(+), 27 deletions(-) + create mode 100644 tuna/utils.py + +diff --git a/tuna-cmd.py b/tuna-cmd.py +index f37e286..d0323f5 100755 +--- a/tuna-cmd.py ++++ b/tuna-cmd.py +@@ -21,7 +21,7 @@ from functools import reduce + import tuna.new_eth as ethtool + import tuna.tuna_sched as tuna_sched + import procfs +-from tuna import tuna, sysfs ++from tuna import tuna, sysfs, utils + import logging + import time + import shutil +@@ -76,7 +76,6 @@ except: + + # FIXME: ETOOMANYGLOBALS, we need a class! + +-nr_cpus = None + ps = None + irqs = None + +@@ -233,25 +232,6 @@ def gen_parser(): + return parser + + +-def get_nr_cpus(): +- """ Get all cpus including disabled cpus """ +- global nr_cpus +- if nr_cpus: +- return nr_cpus +- nr_cpus = os.sysconf('SC_NPROCESSORS_CONF') +- return nr_cpus +- +-nics = None +- +- +-def get_nics(): +- global nics +- if nics: +- return nics +- nics = ethtool.get_active_devices() +- return nics +- +- + def thread_help(tid): + global ps + if not ps: +@@ -277,7 +257,7 @@ def save(cpu_list, thread_list, filename): + if (cpu_list and not set(kt.affinity).intersection(set(cpu_list))) or \ + (thread_list and kt.pid not in thread_list): + del kthreads[name] +- tuna.generate_rtgroups(filename, kthreads, get_nr_cpus()) ++ tuna.generate_rtgroups(filename, kthreads, utils.get_nr_cpus()) + + + def ps_show_header(has_ctxt_switch_info, cgroups=False): +@@ -328,7 +308,7 @@ def format_affinity(affinity): + if len(affinity) <= 4: + return ",".join(str(a) for a in affinity) + +- return ",".join(str(hex(a)) for a in procfs.hexbitmask(affinity, get_nr_cpus())) ++ return ",".join(str(hex(a)) for a in procfs.hexbitmask(affinity, utils.get_nr_cpus())) + + def ps_show_thread(pid, affect_children, ps, has_ctxt_switch_info, sock_inodes, + sock_inode_re, cgroups, columns=None, compact=True): +@@ -351,7 +331,7 @@ def ps_show_thread(pid, affect_children, ps, has_ctxt_switch_info, sock_inodes, + irqs = procfs.interrupts() + users = irqs[tuna.irq_thread_number(cmd)]["users"] + for u in users: +- if u in get_nics(): ++ if u in utils.get_nics(): + users[users.index(u)] = "%s(%s)" % ( + u, ethtool.get_module(u)) + users = ",".join(users) +@@ -486,7 +466,7 @@ def do_ps(thread_list, cpu_list, irq_list, show_uthreads, show_kthreads, + + + def find_drivers_by_users(users): +- nics = get_nics() ++ nics = utils.get_nics() + drivers = [] + for u in users: + try: +@@ -689,10 +669,10 @@ def main(): + apply_config(args.profilename) + + elif args.command in ['include', 'I']: +- tuna.include_cpus(args.cpu_list, get_nr_cpus()) ++ tuna.include_cpus(args.cpu_list, utils.get_nr_cpus()) + + elif args.command in ['isolate', 'i']: +- tuna.isolate_cpus(args.cpu_list, get_nr_cpus()) ++ tuna.isolate_cpus(args.cpu_list, utils.get_nr_cpus()) + + elif args.command in ['run', 'r']: + +diff --git a/tuna/utils.py b/tuna/utils.py +new file mode 100644 +index 0000000..f55432d +--- /dev/null ++++ b/tuna/utils.py +@@ -0,0 +1,28 @@ ++# SPDX-License-Identifier: GPL-2.0-only ++# Copyright (C) 2024 John B. Wyatt IV ++ ++import os ++ ++import tuna.new_eth as ethtool ++ ++# Collect a few globals and functions so they can be reused in other modules ++nr_cpus = None ++nics = None ++ ++def get_nr_cpus(): ++ """ Get all cpus including disabled cpus """ ++ global nr_cpus ++ if nr_cpus != None: ++ return nr_cpus ++ nr_cpus = os.sysconf('SC_NPROCESSORS_CONF') ++ return nr_cpus ++ ++def get_all_cpu_list(): ++ return list(range(get_nr_cpus())) ++ ++def get_nics(): ++ global nics ++ if nics != None: ++ return nics ++ nics = ethtool.get_active_devices() ++ return nics +-- +2.49.0 + diff --git a/0002-tuna-Add-idle_state-control-functionality.patch b/0002-tuna-Add-idle_state-control-functionality.patch new file mode 100644 index 0000000..e9b5a9d --- /dev/null +++ b/0002-tuna-Add-idle_state-control-functionality.patch @@ -0,0 +1,290 @@ +From 9885caebbdc699e96ac18b1a603ad4790fbbe22d Mon Sep 17 00:00:00 2001 +From: "John B. Wyatt IV" +Date: Wed, 9 Apr 2025 15:31:33 -0400 +Subject: [PATCH 2/3] tuna: Add idle_state control functionality + +Allows Tuna to control cpu idle-state functionality on the system, +including querying, enabling, disabling of cpu idle-states to control +power usage or to test functionality. + +This requires cpupower, a utility in the Linux kernel repository and +the cpupower Python bindings added in Linux 6.12 to control cpu +idle-states. + +This patch revision includes text snippet & Python suggestions by Crystal +Wood (v2-4) and small Python suggestions & code snippets by John Kacur +(v3-5). + +Suggested-by: John Kacur + +Signed-off-by: John B. Wyatt IV +Signed-off-by: John B. Wyatt IV +Signed-off-by: John Kacur +--- + tuna-cmd.py | 30 +++++++- + tuna/cpupower.py | 177 +++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 204 insertions(+), 3 deletions(-) + create mode 100755 tuna/cpupower.py + +diff --git a/tuna-cmd.py b/tuna-cmd.py +index d0323f5..4997eaa 100755 +--- a/tuna-cmd.py ++++ b/tuna-cmd.py +@@ -25,6 +25,7 @@ from tuna import tuna, sysfs, utils + import logging + import time + import shutil ++import tuna.cpupower as cpw + + def get_loglevel(level): + if level.isdigit() and int(level) in range(0,5): +@@ -115,8 +116,12 @@ def gen_parser(): + "disable_perf": dict(action='store_true', help="Explicitly disable usage of perf in GUI for process view"), + "refresh": dict(default=2500, metavar='MSEC', type=int, help="Refresh the GUI every MSEC milliseconds"), + "priority": dict(default=(None, None), metavar="POLICY:RTPRIO", type=tuna.get_policy_and_rtprio, help="Set thread scheduler tunables: POLICY and RTPRIO"), +- "background": dict(action='store_true', help="Run command as background task") +- } ++ "background": dict(action='store_true', help="Run command as background task"), ++ "idle_info": dict(dest='idle_info', action='store_const', const=True, help='Print general idle information for the selected CPUs, including index values for IDLE-STATE.'), ++ "idle_state_disabled_status": dict(dest='idle_state_disabled_status', metavar='IDLE-STATE', type=int, help='Print whether IDLE-STATE is enabled on the selected CPUs.'), ++ "disable_idle_state": dict(dest='disable_idle_state', metavar='IDLE-STATE', type=int, help='Disable IDLE-STATE on the selected CPUs.'), ++ "enable_idle_state": dict(dest='enable_idle_state', metavar='IDLE-STATE', type=int, help='Enable IDLE-STATE on the selected CPUs.') ++ } + + parser = HelpMessageParser(description="tuna - Application Tuning Program") + +@@ -127,6 +132,9 @@ def gen_parser(): + + subparser = parser.add_subparsers(dest='command') + ++ idle_set = subparser.add_parser('cpu_power', ++ description='Manage CPU idle state disabling (requires libcpupower and it\'s Python bindings)', ++ help='Set all idle states on a given CPU-LIST.') + isolate = subparser.add_parser('isolate', description="Move all allowed threads and IRQs away from CPU-LIST", + help="Move all allowed threads and IRQs away from CPU-LIST") + include = subparser.add_parser('include', description="Allow all threads to run on CPU-LIST", +@@ -146,7 +154,6 @@ def gen_parser(): + show_threads = subparser.add_parser('show_threads', description='Show thread list', help='Show thread list') + show_irqs = subparser.add_parser('show_irqs', description='Show IRQ list', help='Show IRQ list') + show_configs = subparser.add_parser('show_configs', description='List preloaded profiles', help='List preloaded profiles') +- + what_is = subparser.add_parser('what_is', description='Provides help about selected entities', help='Provides help about selected entities') + gui = subparser.add_parser('gui', description="Start the GUI", help="Start the GUI") + +@@ -218,6 +225,13 @@ def gen_parser(): + show_irqs_group.add_argument('-S', '--sockets', **MODS['sockets']) + show_irqs.add_argument('-q', '--irqs', **MODS['irqs']) + ++ idle_set_group = idle_set.add_mutually_exclusive_group(required=True) ++ idle_set_group.add_argument('-i', '--idle-info', **MODS['idle_info']) ++ idle_set_group.add_argument('-s', '--status', **MODS['idle_state_disabled_status']) ++ idle_set_group.add_argument('-d', '--disable', **MODS['disable_idle_state']) ++ idle_set_group.add_argument('-e', '--enable', **MODS['enable_idle_state']) ++ idle_set.add_argument('-c', '--cpus', **MODS['cpus']) ++ + what_is.add_argument('thread_list', **POS['thread_list']) + + gui.add_argument('-d', '--disable_perf', **MODS['disable_perf']) +@@ -647,6 +661,16 @@ def main(): + print("Valid log levels: NOTSET, DEBUG, INFO, WARNING, ERROR") + print("Log levels may be specified numerically (0-4)\n") + ++ if args.command == 'cpu_power': ++ if not cpw.have_cpupower: ++ print(f"Error: libcpupower bindings are not detected; please install libcpupower bindings from at least kernel {cpw.cpupower_required_kernel}.", file=sys.stderr) ++ sys.exit(1) ++ ++ my_cpupower = cpw.Cpupower(args.cpu_list) ++ ret = my_cpupower.idle_set_handler(args) ++ if ret > 0: ++ sys.exit(ret) ++ + if 'irq_list' in vars(args): + ps = procfs.pidstats() + if tuna.has_threaded_irqs(ps): +diff --git a/tuna/cpupower.py b/tuna/cpupower.py +new file mode 100755 +index 0000000..ec04b90 +--- /dev/null ++++ b/tuna/cpupower.py +@@ -0,0 +1,177 @@ ++# SPDX-License-Identifier: GPL-2.0-only ++# Copyright (C) 2025 John B. Wyatt IV ++ ++import sys ++from typing import List ++from tuna import utils ++ ++cpupower_required_kernel = "6.12" ++have_cpupower = None ++ ++try: ++ import raw_pylibcpupower as lcpw ++ lcpw.cpufreq_get_available_frequencies(0) ++ have_cpupower = True ++except ImportError: ++ lcpw = None ++ have_cpupower = False ++ ++if have_cpupower: ++ class Cpupower: ++ """The Cpupower class allows you to query and change the power states of the ++ cpu. ++ ++ You may query or change the cpus all at once or a list of the cpus provided to the constructor's cpulist argument. ++ ++ The bindings must be detected on the $PYTHONPATH variable. ++ ++ You must use have_cpupower variable to determine if the bindings were ++ detected in your code.""" ++ ++ LCPW_ERROR_TWO_CASE = 1 # enum for common error messages ++ LCPW_ERROR_THREE_CASE = 2 ++ ++ def __init__(self, cpu_list=None): ++ if cpu_list and not cpu_list == []: ++ self.__cpu_list = cpu_list ++ else: ++ self.__cpu_list = utils.get_all_cpu_list() ++ ++ def handle_common_lcpw_errors(self, e, error_type, idle_name): ++ match e: ++ case 0: ++ pass ++ case -1: ++ print(f"Idlestate {idle_name} not available", file=sys.stderr) ++ case -2: ++ print("Disabling is not supported by the kernel", file=sys.stderr) ++ case -3: ++ if error_type == Cpupower.LCPW_ERROR_THREE_CASE: ++ print("No write access to disable/enable C-states: try using sudo", file=sys.stderr) ++ else: ++ print(f"Not documented: {e}", file=sys.stderr) ++ case _: ++ print(f"Not documented: {e}", file=sys.stderr) ++ ++ def get_idle_states(self, cpu): ++ """ ++ Get the c-states of a cpu. ++ ++ You can capture the return values with: ++ states_list, states_amt = get_idle_states() ++ ++ Returns ++ List[String]: list of cstates ++ Int: amt of cstates ++ """ ++ ret = [] ++ for cstate in range(lcpw.cpuidle_state_count(cpu)): ++ ret.append(lcpw.cpuidle_state_name(cpu,cstate)) ++ return ret, lcpw.cpuidle_state_count(cpu) ++ ++ def get_idle_info(self, cpu): ++ idle_states, idle_states_amt = self.get_idle_states(cpu) ++ idle_states_list = [] ++ for idle_state, idle_state_name in enumerate(idle_states): ++ idle_states_list.append( ++ { ++ "CPU ID": cpu, ++ "Idle State Name": idle_state_name, ++ "Flags/Description": lcpw.cpuidle_state_desc(cpu, idle_state), ++ "Latency": lcpw.cpuidle_state_latency(cpu, idle_state), ++ "Usage": lcpw.cpuidle_state_usage(cpu, idle_state), ++ "Duration": lcpw.cpuidle_state_time(cpu, idle_state) ++ } ++ ) ++ idle_info = { ++ "CPUidle-driver": lcpw.cpuidle_get_driver(), ++ "CPUidle-governor": lcpw.cpuidle_get_governor(), ++ "idle-states-count": idle_states_amt, ++ "available-idle-states": idle_states, ++ "cpu-states": idle_states_list ++ } ++ return idle_info ++ ++ def print_idle_info(self, cpu_list): ++ for cpu in cpu_list: ++ idle_info = self.get_idle_info(cpu) ++ print( ++f"""CPUidle driver: {idle_info["CPUidle-driver"]} ++CPUidle governor: {idle_info["CPUidle-governor"]} ++analyzing CPU {cpu} ++ ++Number of idle states: {idle_info["idle-states-count"]} ++Available idle states: {idle_info["available-idle-states"]}""") ++ for state in idle_info["cpu-states"]: ++ print( ++f"""{state["Idle State Name"]} ++Flags/Description: {state["Flags/Description"]} ++Latency: {state["Latency"]} ++Usage: {state["Usage"]} ++Duration: {state["Duration"]}""") ++ ++ def idle_set_handler(self, args) -> int: ++ if args.idle_state_disabled_status is not None: ++ cstate_index = args.idle_state_disabled_status ++ cstate_list, cstate_amt = self.get_idle_states(self.__cpu_list[0]) # Assuming all cpus have the same idle state ++ if cstate_index < 0 or cstate_index >= cstate_amt: ++ print(f"Invalid idle state range. Total for this cpu is {cstate_amt}", file=sys.stderr) ++ return 1 ++ cstate_name = cstate_list[cstate_index] ++ ret = self.is_disabled_idle_state(cstate_index) ++ for i,e in enumerate(ret): ++ if e == 1: ++ print(f"CPU: {self.__cpu_list[i]} Idle state \"{cstate_name}\" is disabled.") ++ elif e == 0: ++ print(f"CPU: {self.__cpu_list[i]} Idle state \"{cstate_name}\" is enabled.") ++ else: ++ self.handle_common_lcpw_errors(e, self.LCPW_ERROR_TWO_CASE, cstate_name) ++ elif args.idle_info is not None: ++ self.print_idle_info(self.__cpu_list) ++ return 0 ++ elif args.disable_idle_state is not None: ++ cstate_index = args.disable_idle_state ++ cstate_list, cstate_amt = self.get_idle_states(self.__cpu_list[0]) # Assuming all cpus have the same idle state ++ if cstate_index < 0 or cstate_index >= cstate_amt: ++ print(f"Invalid idle state range. Total for this cpu is {cstate_amt}") ++ return 1 ++ cstate_name = cstate_list[cstate_index] ++ ret = self.disable_idle_state(cstate_index, 1) ++ for e in ret: ++ self.handle_common_lcpw_errors(e, self.LCPW_ERROR_THREE_CASE, cstate_name) ++ elif args.enable_idle_state is not None: ++ cstate_index = args.enable_idle_state ++ cstate_list, cstate_amt = self.get_idle_states(self.__cpu_list[0]) # Assuming all cpus have the same idle state ++ if cstate_index < 0 or cstate_index >= cstate_amt: ++ print(f"Invalid idle state range. Total for this cpu is {cstate_amt}") ++ return 1 ++ cstate_name = cstate_list[cstate_index] ++ ret = self.disable_idle_state(cstate_index, 0) ++ for e in ret: ++ self.handle_common_lcpw_errors(e, self.LCPW_ERROR_THREE_CASE, cstate_name) ++ return 0 ++ ++ def disable_idle_state(self, state, disabled) -> List[int]: ++ """ ++ Disable or enable an idle state using the object's stored list of cpus. ++ ++ Args: ++ state (int): The cpu idle state index to disable or enable as an int starting from 0. ++ disabled (int): set to 1 to disable or 0 to enable. Less than 0 is an error. ++ """ ++ ret = [] ++ for cpu in self.__cpu_list: ++ ret.append(lcpw.cpuidle_state_disable(cpu, state, disabled)) ++ return ret ++ ++ def is_disabled_idle_state(self, state) -> List[int]: ++ """ ++ Query the idle state. ++ ++ Args: ++ state: The cpu idle state. 1 is disabled, 0 is enabled. Less than 0 is an error. ++ """ ++ ret = [] ++ for cpu in self.__cpu_list: ++ ret.append(lcpw.cpuidle_is_state_disabled(cpu, state)) ++ return ret +-- +2.49.0 + diff --git a/0003-tuna-utils-A-few-tweaks.patch b/0003-tuna-utils-A-few-tweaks.patch new file mode 100644 index 0000000..935e0f9 --- /dev/null +++ b/0003-tuna-utils-A-few-tweaks.patch @@ -0,0 +1,54 @@ +From 59475dfad6c040654307dd7ee3427437b72a13ed Mon Sep 17 00:00:00 2001 +From: John Kacur +Date: Wed, 9 Apr 2025 18:20:13 -0400 +Subject: [PATCH 3/3] tuna: utils: A few tweaks + +- Change copyright to 2025 +- Use is not None instead of != +- Add a few document strings + +Signed-off-by: John Kacur +--- + tuna/utils.py | 10 ++++++---- + 1 file changed, 6 insertions(+), 4 deletions(-) + +diff --git a/tuna/utils.py b/tuna/utils.py +index f55432d..75900bd 100644 +--- a/tuna/utils.py ++++ b/tuna/utils.py +@@ -1,8 +1,8 @@ + # SPDX-License-Identifier: GPL-2.0-only +-# Copyright (C) 2024 John B. Wyatt IV ++# Copyright (C) 2025 John B. Wyatt IV ++""" Module to return cpus and nics """ + + import os +- + import tuna.new_eth as ethtool + + # Collect a few globals and functions so they can be reused in other modules +@@ -12,17 +12,19 @@ nics = None + def get_nr_cpus(): + """ Get all cpus including disabled cpus """ + global nr_cpus +- if nr_cpus != None: ++ if nr_cpus is not None: + return nr_cpus + nr_cpus = os.sysconf('SC_NPROCESSORS_CONF') + return nr_cpus + + def get_all_cpu_list(): ++ """ Return a list of all cpus """ + return list(range(get_nr_cpus())) + + def get_nics(): ++ """ Return a list of network devices """ + global nics +- if nics != None: ++ if nics is not None: + return nics + nics = ethtool.get_active_devices() + return nics +-- +2.49.0 + diff --git a/tuna.spec b/tuna.spec index 7e06c89..586f7e6 100644 --- a/tuna.spec +++ b/tuna.spec @@ -2,7 +2,7 @@ Name: tuna Version: 0.19 -Release: 9%{?dist} +Release: 10%{?dist} License: GPL-2.0-only AND LGPL-2.1-only Summary: Application tuning GUI & command line utility Source: https://www.kernel.org/pub/software/utils/%{name}/%{name}-%{version}.tar.xz @@ -18,6 +18,9 @@ Requires: python3-linux-procfs >= 0.7.3 Patch1: 0001-Add-SPDX-license-identifiers.patch Patch2: 0002-tuna-Remove-spec-file-from-git.patch Patch3: tuna-Don-t-start-the-gui-if-a-display-is-not-availab.patch +Patch4: 0001-tuna-extract-common-cpu-and-nics-determination-code-.patch +Patch5: 0002-tuna-Add-idle_state-control-functionality.patch +Patch6: 0003-tuna-utils-A-few-tweaks.patch %description Provides interface for changing scheduler and IRQ tunables, at whole CPU and at @@ -33,6 +36,9 @@ installed. %patch 1 -p1 %patch 2 -p1 %patch 3 -p1 +%patch 4 -p1 +%patch 5 -p1 +%patch 6 -p1 %build %py3_build @@ -74,6 +80,10 @@ done %{_datadir}/polkit-1/actions/org.tuna.policy %changelog +* Thu Apr 10 2025 John B. Wyatt IV - 0.19-10 +- Add cpu_power cmd + Resolves: RHEL-61750 + * Tue Oct 29 2024 Troy Dawson - 0.19-9 - Bump release for October 2024 mass rebuild: Resolves: RHEL-64018