Test Failed
Pull Request — develop (#3376)
by
unknown
02:33
created

glances.plugins.gpu.cards.amd.AmdGPU.get_device_stats()   A

Complexity

Conditions 2

Size

Total Lines 23
Code Lines 13

Duplication

Lines 23
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nop 1
dl 23
loc 23
rs 9.75
c 0
b 0
f 0
1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""AMD Extension unit for Glances' GPU plugin.
10
11
The class grabs the stats from the /sys/class/drm/ directory.
12
13
See: https://wiki.archlinux.org/title/AMDGPU#Manually
14
"""
15
16
# Example
17
# tests-data/plugins/gpu/amd/
18
# └── sys
19
#     ├── class
20
#     │   └── drm
21
#     │       └── card0
22
#     │           └── device
23
#     │               ├── device
24
#     │               ├── gpu_busy_percent
25
#     │               ├── hwmon
26
#     │               │   └── hwmon0
27
#     │               │       └── temp1_input
28
#     │               ├── mem_info_vram_total
29
#     │               ├── mem_info_vram_used
30
#     │               ├── pp_dpm_mclk
31
#     │               ├── pp_dpm_sclk
32
#     │               └── revision
33
#     └── kernel
34
#         └── debug
35
#             └── dri
36
#                 └── 0
37
#                     └── amdgpu_pm_info
38
39
import os
40
import re
41
from typing import Optional
42
import functools
43
44
DRM_ROOT_FOLDER: str = '/sys/class/drm'
45
CARD_REGEX: str = r"^card\d$"
46
DEVICE_FOLDER: str = 'device'
47
AMDGPU_IDS_FILE: str = '/usr/share/libdrm/amdgpu.ids'
48
PCI_DEVICE_ID: str = 'device'
49
PCI_REVISION_ID: str = 'revision'
50
GPU_PROC_PERCENT: str = 'gpu_busy_percent'
51
GPU_MEM_TOTAL: str = 'mem_info_vram_total'
52
GPU_MEM_USED: str = 'mem_info_vram_used'
53
HWMON_REGEXP: str = r"^hwmon\d$"
54
GPU_TEMPERATURE_REGEXP: str = r"^temp\d_input"
55
56
57
class AmdGPU:
58
    """GPU card class."""
59
60
    def __init__(self, drm_root_folder: str = DRM_ROOT_FOLDER):
61
        """Init AMD  GPU card class."""
62
        self.drm_root_folder = drm_root_folder
63
        self.device_folders = get_device_list(drm_root_folder)
64
65
    def exit(self):
66
        """Close AMD GPU class."""
67
68
    def get_device_stats(self):
69
        """Get AMD GPU stats."""
70
        stats = []
71
72
        for index, device in enumerate(self.device_folders):
73
            device_stats = {}
74
            # Dictionary key is the GPU_ID
75
            device_stats['key'] = 'gpu_id'
76
            # GPU id (for multiple GPU, start at 0)
77
            device_stats['gpu_id'] = f'amd{index}'
78
            # GPU name
79
            device_stats['name'] = get_device_name(device)
80
            # Memory consumption in % (not available on all GPU)
81
            device_stats['mem'] = get_mem(device)
82
            # Processor consumption in %
83
            device_stats['proc'] = get_proc(device)
84
            # Processor temperature in °C
85
            device_stats['temperature'] = get_temperature(device)
86
            # Fan speed in %
87
            device_stats['fan_speed'] = get_fan_speed(device)
88
            stats.append(device_stats)
89
90
        return stats
91
92
93
def get_device_list(drm_root_folder: str) -> list:
94
    """Return a list of path to the device stats."""
95
    ret = []
96
    for root, dirs, _ in os.walk(drm_root_folder):
97
        for d in dirs:
98
            if (
99
                re.match(CARD_REGEX, d)
100
                and DEVICE_FOLDER in os.listdir(os.path.join(root, d))
101
                and os.path.isfile(os.path.join(root, d, DEVICE_FOLDER, GPU_PROC_PERCENT))
102
            ):
103
                # If the GPU busy file is present then take the card into account
104
                ret.append(os.path.join(root, d, DEVICE_FOLDER))
105
    return ret
106
107
108
def read_file(*path_segments: str) -> Optional[str]:
109
    """Return content of file."""
110
    path = os.path.join(*path_segments)
111
    if os.path.isfile(path):
112
        with open(path) as f:
113
            try:
114
                return f.read().strip()
115
            except PermissionError:
116
                # Catch exception (see issue #3125)
117
                return None
118
    return None
119
120
121
@functools.cache
122
def get_device_name(device_folder: str) -> str:
123
    """Return the GPU name."""
124
125
    # Table source: https://cgit.freedesktop.org/drm/libdrm/tree/data/amdgpu.ids
126
    device_id = read_file(device_folder, PCI_DEVICE_ID)
127
    revision_id = read_file(device_folder, PCI_REVISION_ID)
128
    amdgpu_ids = read_file(AMDGPU_IDS_FILE)
129
    if device_id and revision_id and amdgpu_ids:
130
        # Strip leading "0x" and convert to uppercase hexadecimal
131
        device_id = device_id[2:].upper()
132
        revision_id = revision_id[2:].upper()
133
        # Syntax:
134
        # device_id,	revision_id,	product_name        <-- single tab after comma
135
        pattern = re.compile(f'^{device_id},\\s{revision_id},\\s(?P<product_name>.+)$', re.MULTILINE)
136
        if match := pattern.search(amdgpu_ids):
137
            return match.group('product_name').removeprefix('AMD ').removesuffix(' Graphics')
138
139
    return 'AMD GPU'
140
141
142
def get_mem(device_folder: str) -> Optional[int]:
143
    """Return the memory consumption in %."""
144
    mem_info_vram_total = read_file(device_folder, GPU_MEM_TOTAL)
145
    mem_info_vram_used = read_file(device_folder, GPU_MEM_USED)
146
    if mem_info_vram_total and mem_info_vram_used:
147
        mem_info_vram_total = int(mem_info_vram_total)
148
        mem_info_vram_used = int(mem_info_vram_used)
149
        if mem_info_vram_total > 0:
150
            return round(mem_info_vram_used / mem_info_vram_total * 100)
151
    return None
152
153
154
def get_proc(device_folder: str) -> Optional[int]:
155
    """Return the processor consumption in %."""
156
    if gpu_busy_percent := read_file(device_folder, GPU_PROC_PERCENT):
157
        return int(gpu_busy_percent)
158
    return None
159
160
161
def get_temperature(device_folder: str) -> Optional[int]:
162
    """Return the processor temperature in °C (mean of all HWMON)"""
163
    temp_input = []
164
    for root, dirs, _ in os.walk(device_folder):
165
        for d in dirs:
166
            if re.match(HWMON_REGEXP, d):
167
                for _, _, files in os.walk(os.path.join(root, d)):
168
                    for f in files:
169
                        if re.match(GPU_TEMPERATURE_REGEXP, f):
170
                            if a_temp_input := read_file(root, d, f):
171
                                temp_input.append(int(a_temp_input))
172
                            else:
173
                                return None
174
    if temp_input:
175
        return round(sum(temp_input) / len(temp_input) / 1000)
176
    return None
177
178
179
def get_fan_speed(device_folder: str) -> Optional[int]:
180
    """Return the fan speed in %."""
181
    return None
182