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

glances.plugins.gpu.cards.amd.get_mem()   B

Complexity

Conditions 8

Size

Total Lines 20
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 17
nop 1
dl 0
loc 20
rs 7.3333
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
amdgpu.ids source: https://cgit.freedesktop.org/drm/libdrm/tree/data/amdgpu.ids
15
"""
16
17
# Example
18
# tests-data/plugins/gpu/amd/
19
# └── sys
20
#     ├── class
21
#     │   └── drm
22
#     │       └── card0
23
#     │           └── device
24
#     │               ├── device
25
#     │               ├── gpu_busy_percent
26
#     │               ├── hwmon
27
#     │               │   └── hwmon0
28
#     │               │       └── temp1_input
29
#     │               ├── mem_info_vram_total
30
#     │               ├── mem_info_vram_used
31
#     │               ├── pp_dpm_mclk
32
#     │               ├── pp_dpm_sclk
33
#     │               └── revision
34
#     └── kernel
35
#         └── debug
36
#             └── dri
37
#                 └── 0
38
#                     └── amdgpu_pm_info
39
40
import os
41
import re
42
from typing import Optional
43
import functools
44
45
DRM_ROOT_FOLDER: str = '/sys/class/drm'
46
CARD_REGEX: str = r"^card\d$"
47
DEVICE_FOLDER: str = 'device'
48
AMDGPU_IDS: str = '/usr/share/libdrm/amdgpu.ids'
49
PCI_DEVICE_ID: str = 'device'
50
PCI_REVISION_ID: str = 'revision'
51
GPU_PROC_PERCENT: str = 'gpu_busy_percent'
52
GPU_MEM_TOTAL: str = 'mem_info_vram_total'
53
GPU_MEM_USED: str = 'mem_info_vram_used'
54
HWMON_REGEXP: str = r"^hwmon\d$"
55
GPU_TEMPERATURE_REGEXP: str = r"^temp\d_input"
56
57
58
class AmdGPU:
59
    """GPU card class."""
60
61
    def __init__(self, drm_root_folder: str = DRM_ROOT_FOLDER):
62
        """Init AMD  GPU card class."""
63
        self.drm_root_folder = drm_root_folder
64
        self.device_folders = get_device_list(drm_root_folder)
65
66
    def exit(self):
67
        """Close AMD GPU class."""
68
69
    def get_device_stats(self):
70
        """Get AMD GPU stats."""
71
        stats = []
72
73
        for index, device in enumerate(self.device_folders):
74
            device_stats = {}
75
            # Dictionary key is the GPU_ID
76
            device_stats['key'] = 'gpu_id'
77
            # GPU id (for multiple GPU, start at 0)
78
            device_stats['gpu_id'] = f'amd{index}'
79
            # GPU name
80
            device_stats['name'] = get_device_name(device)
81
            # Memory consumption in % (not available on all GPU)
82
            device_stats['mem'] = get_mem(device)
83
            # Processor consumption in %
84
            device_stats['proc'] = get_proc(device)
85
            # Processor temperature in °C
86
            device_stats['temperature'] = get_temperature(device)
87
            # Fan speed in %
88
            device_stats['fan_speed'] = get_fan_speed(device)
89
            stats.append(device_stats)
90
91
        return stats
92
93
94
def get_device_list(drm_root_folder: str) -> list:
95
    """Return a list of path to the device stats."""
96
    ret = []
97
    for root, dirs, _ in os.walk(drm_root_folder):
98
        for d in dirs:
99
            if (
100
                re.match(CARD_REGEX, d)
101
                and DEVICE_FOLDER in os.listdir(os.path.join(root, d))
102
                and os.path.isfile(os.path.join(root, d, DEVICE_FOLDER, GPU_PROC_PERCENT))
103
            ):
104
                # If the GPU busy file is present then take the card into account
105
                ret.append(os.path.join(root, d, DEVICE_FOLDER))
106
    return ret
107
108
109
def read_file(*path_segments: str) -> Optional[str]:
110
    """Return content of file."""
111
    path = os.path.join(*path_segments)
112
    if os.path.isfile(path):
113
        with open(path) as f:
114
            try:
115
                return f.read().strip()
116
            except PermissionError:
117
                # Catch exception (see issue #3125)
118
                return None
119
    return None
120
121
122
@functools.cache
123
def get_device_name(device_folder: str) -> str:
124
    """Return the GPU name."""
125
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)
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(.+)$', re.MULTILINE)
136
        if match := pattern.search(amdgpu_ids):
137
            return match.group(1)
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