From 57e4b94ba29cf42d9544fc73fc65567f46b29125 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Tue, 25 Apr 2023 21:17:22 -0500 Subject: [PATCH 1/7] add doc source info --- docs/.gitignore | 1 + docs/source/conf.py | 39 +++++++++++++++++++++++++++++++++++++++ docs/source/index.rst | 23 +++++++++++++++++++++++ docs/source/reference.rst | 6 ++++++ 4 files changed, 69 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/reference.rst diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a007fea --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +build/* diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..37dafdb --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,39 @@ +import os +import sys + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +sys.path.insert(0, os.path.abspath(os.path.join("..", "..", "src"))) + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'amdgpu_stats' +copyright = '2023, Josh Lay' +author = 'Josh Lay' +release = '0.1.6' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_rtd_theme"] + +templates_path = ['_templates'] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..d3bb727 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,23 @@ +.. amdgpu_stats documentation master file, created by + sphinx-quickstart on Tue Apr 25 18:45:11 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to amdgpu_stats's documentation! +======================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + reference + + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/reference.rst b/docs/source/reference.rst new file mode 100644 index 0000000..cb02247 --- /dev/null +++ b/docs/source/reference.rst @@ -0,0 +1,6 @@ +``amdgpu_stats`` API reference +================================= + +.. automodule:: amdgpu_stats.utils + :members: + From 0c0db9d0208ae9cda6bcbf530672d59de4022749 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Tue, 25 Apr 2023 21:18:43 -0500 Subject: [PATCH 2/7] update sub-header, reflect module function --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bad533b..82d1960 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # amdgpu_stats -Simple TUI _(using [Textual](https://textual.textualize.io/))_ that shows AMD GPU statistics +A simple Python module/TUI _(using [Textual](https://textual.textualize.io/))_ that shows AMD GPU statistics ![Screenshot of main screen](https://raw.githubusercontent.com/joshlay/amdgpu_stats/master/screens/main.png "Main screen") From c37e2a4d90c486f8a4c3a26306c77958a9efe57e Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Tue, 25 Apr 2023 21:20:34 -0500 Subject: [PATCH 3/7] utils: move stat-fetch to funcs, modularity++ --- src/amdgpu_stats/utils.py | 174 ++++++++++++++++++++++++++++---------- 1 file changed, 128 insertions(+), 46 deletions(-) diff --git a/src/amdgpu_stats/utils.py b/src/amdgpu_stats/utils.py index c77aa73..199127b 100644 --- a/src/amdgpu_stats/utils.py +++ b/src/amdgpu_stats/utils.py @@ -1,17 +1,19 @@ """ utils.py -This module contains utility functions/variables used throughout 'amdgpu-stats' +This module contains utility functions/variables used throughout the 'amdgpu-stats' TUI Variables: - CARD: the identifier for the discovered AMD GPU, ie: `card0` / `card1` - hwmon_dir: the `hwmon` interface (dir) that provides stats for this card - SRC_FILES: dictionary of the known stats from the items in `hwmon_dir` - TEMP_FILES: dictionary of the *discovered* temperature nodes / stat files + - POWER_DOMAINS: tuple of supported power domains: `average`, `limit`, `cap`, and `default` + - CLOCK_DOMAINS: tuple of supported clock domains: `core`, `memory` """ from os import path import glob -from typing import Tuple, Optional +from typing import Tuple, Optional, Union from humanfriendly import format_size @@ -20,10 +22,10 @@ def find_card() -> Optional[Tuple[Optional[str], Optional[str]]]: ... looking for 'amdgpu' to find a card to monitor - Returns: - A tuple containing the 'cardN' name and hwmon directory for stats - If no AMD GPU found, this will be: (None, None) + + Returns: + tuple: ('cardN', '/hwmon/directory/with/stat/files') """ _card = None _hwmon_dir = None @@ -75,17 +77,22 @@ for temp_node_label_file in temp_node_labels: def read_stat(file: str) -> str: - """Given statistic file, `file`, return the contents""" + """Read statistic `file`, return the stripped contents + + Returns: + str: Statistics from `file`""" with open(file, "r", encoding="utf-8") as _fh: data = _fh.read() return data.strip() def format_frequency(frequency_hz: int) -> str: - """Takes a frequency (in Hz) and appends it with the appropriate suffix, ie: - - Hz - - MHz - - GHz""" + """ + Takes a frequency (in Hz) and normalizes it: `Hz`, `MHz`, or `GHz` + + Returns: + str: frequency string with the appropriate suffix applied + """ return ( format_size(frequency_hz, binary=False) .replace("B", "Hz") @@ -96,67 +103,142 @@ def format_frequency(frequency_hz: int) -> str: def get_power_stats() -> dict: """ Returns: - A dictionary of current GPU *power* related statistics. + dict: A dictionary of current GPU *power* related statistics. - {'limit': int, - 'average': int, - 'capability': int, - 'default': int} + Example: + `{'limit': int, 'average': int, 'capability': int, 'default': int}` """ - return {"limit": int(int(read_stat(SRC_FILES['pwr_limit'])) / 1000000), - "average": int(int(read_stat(SRC_FILES['pwr_average'])) / 1000000), - "capability": int(int(read_stat(SRC_FILES['pwr_cap'])) / 1000000), - "default": int(int(read_stat(SRC_FILES['pwr_default'])) / 1000000)} + return {"limit": get_gpu_power('limit'), + "average": get_gpu_power('average'), + "capability": get_gpu_power('cap'), + "default": get_gpu_power('default')} + + +# constant; supported power domains by 'get_gpu_power' func +# is concatenated with 'pwr_' to index SRC_FILES for the relevant data file +POWER_DOMAINS = ('limit', 'average', 'cap', 'default') +# defined outside/globally for efficiency -- it's called a lot in the TUI + + +def get_gpu_power(domain: str) -> int: + """ + Args: + domain (str): The GPU domain of interest regarding power + + Must be one of POWER_DOMAINS: + - limit: the effective limit placed on the card + - default: the default limit + - average: the average consumption + - cap: the board capability + + Returns: + int: The requested GPU power statistic by domain, in Watts + """ + if domain not in POWER_DOMAINS: + raise ValueError(f"Invalid power domain: '{domain}'. Must be one of: {POWER_DOMAINS}") + return int(int(read_stat(SRC_FILES['pwr_' + domain])) / 1000000) def get_core_stats() -> dict: """ Returns: - A dictionary of current GPU *core/memory* related statistics. + dict: A dictionary of current GPU *core/memory* related statistics. - {'sclk': int, - 'mclk': int, - 'voltage': float, - 'util_pct': int} + Clocks are in Hz, the `format_frequency` function may be used to normalize - Clocks are in Hz, `format_frequency` may be used to normalize + Example: + `{'sclk': int, 'mclk': int, 'voltage': float, 'util_pct': int}` """ - return {"sclk": int(read_stat(SRC_FILES['core_clock'])), - "mclk": int(read_stat(SRC_FILES['memory_clock'])), - "voltage": float( - f"{int(read_stat(SRC_FILES['core_voltage'])) / 1000:.2f}" - ), - "util_pct": int(read_stat(SRC_FILES['busy_pct']))} + return {"sclk": get_clock('core'), + "mclk": get_clock('memory'), + "voltage": get_voltage(), + "util_pct": get_gpu_usage()} + + +# constant; supported clock domains by 'get_clock' func +# is concatenated with 'clock_' to index SRC_FILES for the relevant data file +CLOCK_DOMAINS = ('core', 'memory') +# defined outside/globally for efficiency -- it's called a lot in the TUI + + +def get_clock(domain: str, format_freq: bool = False) -> Union[int, str]: + """ + Args: + domain (str): The GPU domain of interest regarding clock speed. + Must be one of CLOCK_DOMAINS + + format_freq (bool, optional): If True, a formatted string will be returned instead of an int. + Defaults to False. + + Returns: + Union[int, str]: The clock value for the specified domain. + If format_freq is True, a formatted string with Hz/MHz/GHz + will be returned instead of an int + """ + if domain not in CLOCK_DOMAINS: + raise ValueError(f"Invalid clock domain: '{domain}'. Must be one of: {CLOCK_DOMAINS}") + if format_freq: + return format_frequency(read_stat(SRC_FILES[domain + '_clock'])) + return int(read_stat(SRC_FILES[domain + '_clock'])) + + +def get_voltage() -> float: + """ + Returns: + float: The current GPU core voltage + """ + return round(int(read_stat(SRC_FILES['core_voltage'])) / 1000.0, 2) def get_fan_stats() -> dict: """ Returns: - A dictionary of current GPU *fan* related statistics. + dict: A dictionary of current GPU *fan* related statistics. - {'fan_rpm': int, - 'fan_rpm_target': int} - """ - return {"fan_rpm": int(read_stat(SRC_FILES['fan_rpm'])), - "fan_rpm_target": int(read_stat(SRC_FILES['fan_rpm_target']))} + Example: + `{'fan_rpm': int, 'fan_rpm_target': int}` + """ + return {"fan_rpm": get_fan_rpm(), + "fan_rpm_target": get_fan_target()} + + +def get_fan_rpm() -> int: + """ + Returns: + int: The current fan RPM + """ + return int(read_stat(SRC_FILES['fan_rpm'])) + + +def get_fan_target() -> int: + """ + Returns: + int: The current fan RPM + """ + return int(read_stat(SRC_FILES['fan_rpm_target'])) + + +def get_gpu_usage() -> int: + """ + Returns: + int: The current GPU usage/utilization as a percentage + """ + return int(read_stat(SRC_FILES['busy_pct'])) def get_temp_stats() -> dict: """ Returns: - A dictionary of current GPU *temperature* related statistics. + dict: A dictionary of current GPU *temperature* related statistics. - Keys/values are dynamically contructed based on discovered nodes + Example: + `{'name_temp_node_1': int, 'name_temp_node_2': int, 'name_temp_node_3': int}` - {'name_temp_node_1': int, - 'name_temp_node_2': int, - 'name_temp_node_3': int} + Dictionary keys (temp nodes) are dynamically contructed through discovery. - Driver provides this in millidegrees C + Driver provides temperatures in *millidegrees* C - Returned values are converted: (floor) divided by 1000 for *proper* C - - As integers for comparison + Returned values are converted to C, as integers for simple comparison """ temp_update = {} for temp_node, temp_file in TEMP_FILES.items(): From 94277e3476a904b608506eaddd128876845a8a1e Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Tue, 25 Apr 2023 21:20:56 -0500 Subject: [PATCH 4/7] bump to 0.1.6: more modularity/funcs --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e1eb08f..b07472c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "amdgpu-stats" -version = "0.1.5" -description = "A simple TUI (using Textual) that shows AMD GPU statistics" +version = "0.1.6" +description = "A simple module/TUI (using Textual) that provides AMD GPU statistics" authors = ["Josh Lay "] repository = "https://github.com/joshlay/amdgpu_stats" license = "MIT" From cd4c44069f43894e504ee5e022dd5c4db9a14c96 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Tue, 25 Apr 2023 21:41:16 -0500 Subject: [PATCH 5/7] docs: disable problematic sphinx conf.py linting --- docs/source/conf.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 37dafdb..c94fee8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,3 +1,11 @@ +""" +Configuration file for the Sphinx documentation builder. + +For the full list of built-in configuration values, see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" +# 'fix' linting; sphinx doesn't apply +# pylint: disable=invalid-name,redefined-builtin import os import sys @@ -9,11 +17,6 @@ import sys # sys.path.insert(0, os.path.abspath(os.path.join("..", "..", "src"))) -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information From 9c13b0de4cac4cca2593923f7f9f21572e8d4539 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Tue, 25 Apr 2023 21:45:02 -0500 Subject: [PATCH 6/7] utils: disable "line-too-long" for pylint --- src/amdgpu_stats/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/amdgpu_stats/utils.py b/src/amdgpu_stats/utils.py index 199127b..d26a546 100644 --- a/src/amdgpu_stats/utils.py +++ b/src/amdgpu_stats/utils.py @@ -11,6 +11,8 @@ Variables: - POWER_DOMAINS: tuple of supported power domains: `average`, `limit`, `cap`, and `default` - CLOCK_DOMAINS: tuple of supported clock domains: `core`, `memory` """ +# disable superfluous linting +# pylint: disable=line-too-long from os import path import glob from typing import Tuple, Optional, Union From adb00f25e98e1883c6b259d0c24bc662fde44085 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Tue, 25 Apr 2023 21:58:58 -0500 Subject: [PATCH 7/7] sphinx dep: make version amenable --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b07472c..fdbb7a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,10 @@ readme = "README.md" python = "^3.9" textual = ">=0.10" humanfriendly = ">=10.0" +sphinx = "^6.2" + +[tool.poetry.dev-dependencies] +sphinx-rtd-theme = ">=1.0.0" [tool.poetry.scripts] amdgpu-stats = "amdgpu_stats.interface:tui"