diff --git a/cbutil/cpu_frequency.py b/cbutil/cpu_frequency.py new file mode 100644 index 0000000000000000000000000000000000000000..2a628c079c21fa30b5bf4a43a7f71a0ab1c9d775 --- /dev/null +++ b/cbutil/cpu_frequency.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python + +import subprocess +import re +import logging +import argparse + +logger = logging.getLogger(__file__) + +# tuples with filepath and conversion_factor to Hz +FREQUENCY_FILES = [ + ("/sys/devices/system/cpu/cpu0/cpufreq/bios_limit", 1e3), # KHZ + ("/sys/devices/system/cpu/cpu0/cpufreq/base_frequency", 1e3) # KHZ +] + +# tuples with command, regex pattern and conversion_factor to Hz +FREQUENCY_COMMANDS = [ + ("likwid-powermeter -i", r"Base clock:\s*([+-]?\d+(?:\.\d+)?)", 1e6) # MHZ +] + + +def read_frequency_from_file(file_path: str) -> float: + """ + Read the frequency value from a file. + + Args: + file_path: The path to the file. + + Returns: + The frequency value as a float. + + Raises: + FileNotFoundError: If the file does not exist. + ValueError: If the file contains an invalid number. + + """ + try: + with open(file_path, "r") as file_handle: + return float(file_handle.read().strip()) + except FileNotFoundError as e: + raise FileNotFoundError(f"File {file_path} not found") from e + except ValueError as e: + raise ValueError(f"Invalid number in file {file_path}") from e + + +def run_command(command: str) -> str: + """ + Runs a shell command and returns its output. + + Args: + command: The shell command to run. + + Returns: + The output of the command as a string. + + Raises: + ValueError: If there is an error running the shell command. + """ + try: + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True + ) + return result.stdout.strip() + + except subprocess.CalledProcessError as e: + raise ValueError(f"Error running command '{command}':\n{e.stderr}") from e + + +def filter_output(pattern: str, output: str) -> str: + """ + Filters the output of a command using a regex pattern. + + Args: + pattern: The regex pattern to search for. + output: The output of the command as a string. + + Returns: + The first match for the regex pattern in the output. + + Raises: + LookupError: If no match is found for the regex pattern. + """ + pattern = re.compile(pattern) + match = pattern.search(output) + if match: + return match.groups()[0] + else: + raise LookupError(f"No match found for pattern '{pattern}' in command output:\n{output}") + + +def run_command_and_grep_output(command: str, grep_pattern: str) -> str: + """ + Runs a shell command and greps the output for a regex pattern. + + Args: + command: The shell command to run. + grep_pattern: The regex pattern to search for in the command output. + + Returns: + The first match for the regex pattern in the command output. + + Raises: + LookupError: If no match is found for the regex pattern. + ValueError: If there is an error running the shell command. + + """ + + try: + output = run_command(command) + return filter_output(grep_pattern, output) + except subprocess.CalledProcessError as e: + raise ValueError(f"Error running command '{command}':\n{e.stderr}") from e + + +def get_cpu_base_frequency(frequency_files=FREQUENCY_FILES, frequency_commands=FREQUENCY_COMMANDS) -> float: + """ + Searches for the base CPU frequency in a list of files and returns it in GHz. + + Args: + frequency_files (list of tuples): A list with tuples of frequency file paths and conversion factors. + frequency_commands (list of tuples): A list with tuples of commands, regex patterns, and conversion factors. + + Returns: + The base CPU frequency in Hz as a float. + + Raises: + ValueError: If the base CPU frequency cannot be determined. + """ + + for frequency_file, conversion_factor in frequency_files: + try: + frequency = read_frequency_from_file(frequency_file) * float(conversion_factor) + logger.info(f"Found CPU frequency in {frequency_file}: {frequency:.2f} Hz") + return frequency + except (FileNotFoundError, ValueError) as e: + logger.debug(f"Error reading CPU frequency from {frequency_file}: {e}") + + for command, grep_pattern, conversion_factor in frequency_commands: + try: + frequency = float(run_command_and_grep_output(command, grep_pattern)) * float(conversion_factor) + logger.info(f"Found CPU frequency in {command} {grep_pattern}: {frequency:.2f} Hz") + return frequency + except (LookupError, ValueError) as e: + logger.debug(f"Error reading CPU frequency from {command} with {grep_pattern}: {e}") + + raise ValueError("Unable to determine base CPU frequency") + + +def main(): + parser = argparse.ArgumentParser(description="Get the base CPU clock frequency in GHz") + parser.add_argument("-v", "--verbose", action="store_true", help="increase output verbosity") + args = parser.parse_args() + + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + + frequency = get_cpu_base_frequency() / 1e9 + print(f"{frequency:.2f}") + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 0b81b7340c70e918be36129df335d0305d303ac0..053944d769664d8e69f434b04ab2e0b3884bccec 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,10 @@ setup(name="cb-util", "cbutil.postprocessing", "dashboards", "plotting"]), + entry_points={ + 'console_scripts': + 'get_frequency = cbutil.cpu_frequency:main' + }, install_requires=[ "python-dotenv", "influxdb",