From 2064a3071425a41695519014efea5c8ac41eff78 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 2 Jun 2023 20:38:11 -0500 Subject: [PATCH 01/10] bindings: log scrolling {page,}up/down + vim --- src/amdgpu_stats/tui.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/amdgpu_stats/tui.py b/src/amdgpu_stats/tui.py index 0d5df63..037b108 100644 --- a/src/amdgpu_stats/tui.py +++ b/src/amdgpu_stats/tui.py @@ -119,6 +119,7 @@ class GPUStatsWidget(Static): show_cursor=True, name='stats_table', classes='stat_table') + self.tabbed_container = TabbedContent() def on_mount(self) -> None: @@ -191,6 +192,10 @@ class app(App): # pylint: disable=invalid-name Binding("c", "custom_dark", "Colors"), Binding("t", "custom_tab", "Tab switch"), Binding("s", "custom_screenshot", "Screenshot"), + Binding("up,k", "custom_logscroll('up')", "Scroll Logs", ), + Binding("down,j", "custom_logscroll('down')", "Scroll Logs"), + Binding("pageup", "custom_logscroll('pageup')", "", show=False), + Binding("pagedown", "custom_logscroll('pagedown')", "", show=False), Binding("q", "quit", "Quit") ] @@ -212,7 +217,21 @@ class app(App): # pylint: disable=invalid-name self.app.dark = not self.app.dark self.update_log(f"[bold]Dark side: [italic]{self.app.dark}") - def action_custom_screenshot(self, screen_dir: str = '/tmp') -> None: + async def action_custom_logscroll(self, direction: str) -> None: + """Action that handles scrolling of the logging widget + + 'j', 'k', 'Up'/'Down' arrows handle line-by-line + Page Up/Down do... pages""" + if direction == "pageup": + self.stats_widget.text_log.scroll_page_up(animate=True, speed=None, duration=0.175) + elif direction == "up": + self.stats_widget.text_log.scroll_up(animate=False) + elif direction == "pagedown": + self.stats_widget.text_log.scroll_page_down(animate=True, speed=None, duration=0.175) + elif direction == "down": + self.stats_widget.text_log.scroll_down(animate=False) + + async def action_custom_screenshot(self, screen_dir: str = '/tmp') -> None: """Action that fires when the user presses 's' for a screenshot""" # construct the screenshot elements: name (w/ ISO timestamp) + path screen_name = ('amdgpu_stats_' + From 2e0fe97e9e865fd963ced011b43bdcef75fe1b95 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 2 Jun 2023 20:39:14 -0500 Subject: [PATCH 02/10] style: scroll top/bottom borders to "inner" --- src/amdgpu_stats/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/amdgpu_stats/style.css b/src/amdgpu_stats/style.css index 9102d5d..efc665d 100644 --- a/src/amdgpu_stats/style.css +++ b/src/amdgpu_stats/style.css @@ -70,6 +70,6 @@ Footer { } ScrollBar { - border-top: solid $panel-lighten-3; - border-bottom: solid $panel-lighten-3; + border-top: inner $panel-lighten-3; + border-bottom: inner $panel-lighten-3; } From 85939dc9809589788817360910d1adad9a493a82 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 2 Jun 2023 20:39:41 -0500 Subject: [PATCH 03/10] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c0eb160..c34f486 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "amdgpu-stats" -version = "0.1.17" +version = "0.1.18" description = "A module/TUI for AMD GPU statistics" authors = ["Josh Lay "] repository = "https://github.com/joshlay/amdgpu_stats" From 5243106c6b21fd60bc91b0c7e3fb26ff8d1a977b Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 2 Jun 2023 20:47:18 -0500 Subject: [PATCH 04/10] badge: pylint --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index aa39b3d..82f0d84 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # amdgpu_stats +![pylint](https://github.com/joshlay/amdgpu_stats/actions/workflows/pylint.yml/badge.svg) A Python module/TUI for AMD GPU statistics From e5a68827207f9b8f6c01061905879bb05ada148c Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 2 Jun 2023 23:30:47 -0500 Subject: [PATCH 05/10] table/logging: improve/shorten output. style outside of key --- src/amdgpu_stats/tui.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/amdgpu_stats/tui.py b/src/amdgpu_stats/tui.py index 037b108..c3d4a14 100644 --- a/src/amdgpu_stats/tui.py +++ b/src/amdgpu_stats/tui.py @@ -16,6 +16,7 @@ Functions: # pylint: disable=line-too-long from datetime import datetime from typing import Optional +from yaml import dump from rich.text import Text from textual import work @@ -23,13 +24,13 @@ from textual.binding import Binding from textual.app import App, ComposeResult from textual.containers import Container from textual.widgets import ( - Header, - Footer, - Static, - TextLog, DataTable, + Footer, + Header, + Static, TabbedContent, TabPane, + TextLog, ) from .utils import ( @@ -58,7 +59,6 @@ class Notification(Static): '''Fires when notification is clicked, removes the widget''' self.remove() - class GPUStatsWidget(Static): """The main stats widget.""" @@ -75,9 +75,9 @@ class GPUStatsWidget(Static): "Utilization": "", "Voltage": "", "Power": "", - "[italic]Limit": "", - "[italic]Default": "", - "[italic]Capability": "", + "Limit": "", + "Default": "", + "Capability": "", "Fan RPM": "", "Edge temp": "", "Junction temp": "", @@ -90,9 +90,9 @@ class GPUStatsWidget(Static): "Utilization": f'{get_gpu_usage(card=card)}%', "Voltage": f'{get_voltage(card=card)}V', "Power": f'{get_power_stats(card=card)["average"]}W', - "[italic]Limit": f'{get_power_stats(card=card)["limit"]}W', - "[italic]Default": f'{get_power_stats(card=card)["default"]}W', - "[italic]Capability": f'{get_power_stats(card=card)["capability"]}W', + "Limit": f'{get_power_stats(card=card)["limit"]}W', + "Default": f'{get_power_stats(card=card)["default"]}W', + "Capability": f'{get_power_stats(card=card)["capability"]}W', "Fan RPM": f'{get_fan_rpm(card=card)}', "Edge temp": f"{get_temp_stat(name='edge', card=card)}C", "Junction temp": f"{get_temp_stat(name='junction', card=card)}C", @@ -124,14 +124,18 @@ class GPUStatsWidget(Static): def on_mount(self) -> None: '''Fires when stats widget 'mounted', behaves like on first showing''' - self.update_log("[bold green]App started, logging begin!") - self.update_log(f"[bold]Discovered AMD GPUs: [/]{list(AMDGPU_CARDS)}") + self.update_log("[bold green]App started, logging begin!\n") # construct the table columns columns = list(self.get_column_data_mapping(None).keys()) - self.update_log('[bold]Stats table columns:') for column in columns: - self.stats_table.add_column(label=column, key=column) - self.update_log(f' - "{column}"') + if column in ['Limit', 'Default', 'Capability']: + label = '[italic]' + column + self.stats_table.add_column(label=label, key=column) + else: + self.stats_table.add_column(label=column, key=column) + # self.update_log(f' - "{column}"') + self.update_log(f'[bold]Stat columns:') + self.update_log(dump(data=columns, default_flow_style=False, sort_keys=True)) # do a one-off stat collection, populate table before the interval self.get_stats() # stand up the stat-collecting interval, twice per second @@ -164,7 +168,7 @@ class GPUStatsWidget(Static): ] self.stats_table.add_row(*styled_row, key=card) hwmon_dir = AMDGPU_CARDS[card] - self.update_log(f"[bold]Stats table: [/]added row for '{card}', info dir: '{hwmon_dir}'") + self.update_log(f"Added row for '{card}', stats dir: '{hwmon_dir}'") else: # Update existing rows, retaining styling/justification for column, value in self.data.items(): From 9af60178dc00013b31888a48dde9abd6d47b4335 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 2 Jun 2023 23:32:01 -0500 Subject: [PATCH 06/10] linting: remove superfluous fstring --- src/amdgpu_stats/tui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/amdgpu_stats/tui.py b/src/amdgpu_stats/tui.py index c3d4a14..d19c2d8 100644 --- a/src/amdgpu_stats/tui.py +++ b/src/amdgpu_stats/tui.py @@ -134,7 +134,7 @@ class GPUStatsWidget(Static): else: self.stats_table.add_column(label=column, key=column) # self.update_log(f' - "{column}"') - self.update_log(f'[bold]Stat columns:') + self.update_log('[bold]Stat columns:') self.update_log(dump(data=columns, default_flow_style=False, sort_keys=True)) # do a one-off stat collection, populate table before the interval self.get_stats() From 8ee1e5f2b4041fc946708234ff3a2d1b5dda5cd9 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Sat, 3 Jun 2023 01:55:12 -0500 Subject: [PATCH 07/10] add 'graphs', adjust labels for brevity --- src/amdgpu_stats/style.css | 8 ++++++++ src/amdgpu_stats/tui.py | 41 +++++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/amdgpu_stats/style.css b/src/amdgpu_stats/style.css index efc665d..947085c 100644 --- a/src/amdgpu_stats/style.css +++ b/src/amdgpu_stats/style.css @@ -16,6 +16,14 @@ GPUStatsWidget { width: 100%; } +.tab_graphs { + layout: grid; +} + +.graph_section { + box-sizing: content-box; +} + Container { height: 1fr; } diff --git a/src/amdgpu_stats/tui.py b/src/amdgpu_stats/tui.py index d19c2d8..1eee3da 100644 --- a/src/amdgpu_stats/tui.py +++ b/src/amdgpu_stats/tui.py @@ -22,11 +22,13 @@ from rich.text import Text from textual import work from textual.binding import Binding from textual.app import App, ComposeResult -from textual.containers import Container +from textual.containers import Container, Vertical from textual.widgets import ( DataTable, Footer, Header, + Label, + ProgressBar, Static, TabbedContent, TabPane, @@ -107,6 +109,19 @@ class GPUStatsWidget(Static): timer_stats = None # mark the table as needing initialization (with rows) table_needs_init = True + card_bars = [] + for card in AMDGPU_CARDS: + card_bars.append((card, + ProgressBar(total=100.0, + show_eta=False, + id='bar_' + card + '_util'), + ProgressBar(total=100.0, + show_eta=False, + id='bar_' + card + '_poweravg'), + ProgressBar(total=100.0, + show_eta=False, + id='bar_' + card + '_powercap')) + ) def __init__(self, *args, cards=None, **kwargs): super().__init__(*args, **kwargs) @@ -129,8 +144,8 @@ class GPUStatsWidget(Static): columns = list(self.get_column_data_mapping(None).keys()) for column in columns: if column in ['Limit', 'Default', 'Capability']: - label = '[italic]' + column - self.stats_table.add_column(label=label, key=column) + self.stats_table.add_column(label='[italic]' + column, + key=column) else: self.stats_table.add_column(label=column, key=column) # self.update_log(f' - "{column}"') @@ -146,6 +161,17 @@ class GPUStatsWidget(Static): with self.tabbed_container: with TabPane("Stats", id="tab_stats"): yield self.stats_table + with TabPane("Graphs", id="tab_graphs", classes="tab_graphs"): + for card, util_bar, power_bar_avg, power_bar_cap in self.card_bars: + yield Vertical( + Label(f'[bold]{card}'), + Label('Core:'), + util_bar, + Label('Power [italic](limit)[/i]:'), + power_bar_avg, + Label('Power [italic](capability)[/i]:'), + power_bar_cap, + classes='graph_section') with TabPane("Logs", id="tab_logs"): yield self.text_log @@ -158,6 +184,12 @@ class GPUStatsWidget(Static): '''Function to fetch stats / update the table for each AMD GPU found''' for card in self.cards: self.data = self.get_column_data_mapping(card) + # Update usage bars + self.query_one(f'#bar_{card}_util').update(total=100, progress=float(self.data['Utilization'].replace('%', ''))) + self.query_one(f'#bar_{card}_poweravg').update(total=float(self.data['Limit'].replace('W', '')), + progress=float(self.data['Power'].replace('W', ''))) + self.query_one(f'#bar_{card}_powercap').update(total=float(self.data['Capability'].replace('W', '')), + progress=float(self.data['Power'].replace('W', ''))) # handle the table data appopriately # if needs populated anew or updated if self.table_needs_init: @@ -254,7 +286,10 @@ class app(App): # pylint: disable=invalid-name def action_custom_tab(self) -> None: """Toggle between the 'Stats' and 'Logs' tabs""" + # walk/cycle the tabs if self.stats_widget.tabbed_container.active == "tab_stats": + new_tab = 'tab_graphs' + elif self.stats_widget.tabbed_container.active == "tab_graphs": new_tab = 'tab_logs' else: new_tab = 'tab_stats' From 2bf4e67d3f15b6349f981826a74bc5d45ac724df Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Sat, 3 Jun 2023 02:00:17 -0500 Subject: [PATCH 08/10] deps: reflect Textual, allow Py3.8 --- .github/workflows/pylint.yml | 2 +- pyproject.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index f4d9b83..bda3e4a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index c34f486..91a290b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,10 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.9" -textual = ">=0.18.0" +python = "^3.8" +textual = ">=0.22.0" humanfriendly = ">=10.0" +pyyaml = "^6.0" [tool.poetry.scripts] amdgpu-stats = "amdgpu_stats:textual_run" From 1a9b97098873cbde20c56e0c20ffe4809eaf633c Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Sat, 3 Jun 2023 02:02:50 -0500 Subject: [PATCH 09/10] requirements: reflect added pyyaml --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d77690a..ffd6e5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ textual>=0.16.0 humanfriendly==10.0 +pyyaml==6.0 From 91d0c449bb3a9e26a7816f8b1eca7b003a1f76bf Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Sat, 3 Jun 2023 02:05:56 -0500 Subject: [PATCH 10/10] add graphs screen --- README.md | 3 +- screens/graphs.svg | 130 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 screens/graphs.svg diff --git a/README.md b/README.md index 82f0d84..ce15469 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ A Python module/TUI for AMD GPU statistics -![Screenshot of main screen](https://raw.githubusercontent.com/joshlay/amdgpu_stats/master/screens/main.svg "Main screen") +![Screenshot of the main stats table](https://raw.githubusercontent.com/joshlay/amdgpu_stats/master/screens/main.svg "Main screen") +![Screenshot of the 'graphing' scroll bars](https://raw.githubusercontent.com/joshlay/amdgpu_stats/master/screens/graphs.svg "Graphs") Tested _only_ on `RX6000` series cards; APUs and more _may_ be supported. Please file an issue if finding incompatibility! diff --git a/screens/graphs.svg b/screens/graphs.svg new file mode 100644 index 0000000..bfe5284 --- /dev/null +++ b/screens/graphs.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AMD GPU Stats + + + + + + + + + + AMD GPU Stats — cards: ['card0']18:57:42 + +StatsGraphsLogs +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +card0 +Core: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━69% +Power (limit): +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━15% +Power (capability): +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━13% + + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + C  Colors  T  Tab switch  S  Screenshot  ↑  Scroll Logs  ↓  Scroll Logs  Q  Quit  + + +