v0.1.6: Improve modularity, move stats to functions
v0.1.6: Improve modularity, move stats to functions
This commit is contained in:
commit
665ca6a161
7 changed files with 209 additions and 49 deletions
|
@ -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")
|
||||
|
||||
|
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
build/*
|
42
docs/source/conf.py
Normal file
42
docs/source/conf.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
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
|
||||
|
||||
# -- 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")))
|
||||
|
||||
# -- 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']
|
23
docs/source/index.rst
Normal file
23
docs/source/index.rst
Normal file
|
@ -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`
|
6
docs/source/reference.rst
Normal file
6
docs/source/reference.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
``amdgpu_stats`` API reference
|
||||
=================================
|
||||
|
||||
.. automodule:: amdgpu_stats.utils
|
||||
:members:
|
||||
|
|
@ -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 <pypi@jlay.io>"]
|
||||
repository = "https://github.com/joshlay/amdgpu_stats"
|
||||
license = "MIT"
|
||||
|
@ -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"
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
"""
|
||||
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`
|
||||
"""
|
||||
# disable superfluous linting
|
||||
# pylint: disable=line-too-long
|
||||
from os import path
|
||||
import glob
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, Union
|
||||
from humanfriendly import format_size
|
||||
|
||||
|
||||
|
@ -20,10 +24,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 +79,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 +105,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}
|
||||
Example:
|
||||
`{'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']))}
|
||||
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():
|
||||
|
|
Reference in a new issue