Test Failed
Pull Request — master (#3388)
by
unknown
04:40
created

glances.plugins.gpu.cards.amd.get_fan_speed()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 3
rs 10
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
#     │               │       ├── in1_input
28
#     │               │       └── temp1_input
29
#     │               ├── mem_info_gtt_total
30
#     │               ├── mem_info_gtt_used
31
#     │               ├── mem_info_vram_total
32
#     │               ├── mem_info_vram_used
33
#     │               ├── pp_dpm_mclk
34
#     │               ├── pp_dpm_sclk
35
#     │               └── revision
36
#     └── kernel
37
#         └── debug
38
#             └── dri
39
#                 └── 0
40
#                     └── amdgpu_pm_info
41
42
import functools
43
import glob
44
import os
45
import re
46
from typing import List, Optional
47
48
DRM_ROOT_FOLDER: str = '/sys/class/drm'
49
DEVICE_FOLDER_PATTERN: str = 'card[0-9]/device'
50
AMDGPU_IDS_FILE: str = '/usr/share/libdrm/amdgpu.ids'
51
PCI_DEVICE_ID: str = 'device'
52
PCI_REVISION_ID: str = 'revision'
53
GPU_PROC_PERCENT: str = 'gpu_busy_percent'
54
GPU_MEM_TOTAL: str = 'mem_info_vram_total'
55
GPU_MEM_USED: str = 'mem_info_vram_used'
56
GTT_MEM_TOTAL: str = 'mem_info_gtt_total'
57
GTT_MEM_USED: str = 'mem_info_gtt_used'
58
HWMON_NORTHBRIDGE_VOLTAGE_PATTERN: str = 'hwmon/hwmon[0-9]/in1_input'
59
HWMON_TEMPERATURE_PATTERN = 'hwmon/hwmon[0-9]/temp[0-9]_input'
60
61
62
class AmdGPU:
63
    """GPU card class."""
64
65
    def __init__(self, drm_root_folder: str = DRM_ROOT_FOLDER):
66
        """Init AMD  GPU card class."""
67
        self.drm_root_folder = drm_root_folder
68
        self.device_folders = get_device_list(drm_root_folder)
69
70
    def exit(self):
71
        """Close AMD GPU class."""
72
73
    def get_device_stats(self):
74
        """Get AMD GPU stats."""
75
        stats = []
76
77
        for index, device in enumerate(self.device_folders):
78
            device_stats = {}
79
            # Dictionary key is the GPU_ID
80
            device_stats['key'] = 'gpu_id'
81
            # GPU id (for multiple GPU, start at 0)
82
            device_stats['gpu_id'] = f'amd{index}'
83
            # GPU name
84
            device_stats['name'] = get_device_name(device)
85
            # Memory consumption in % (not available on all GPU)
86
            device_stats['mem'] = get_mem(device)
87
            # Processor consumption in %
88
            device_stats['proc'] = get_proc(device)
89
            # Processor temperature in °C
90
            device_stats['temperature'] = get_temperature(device)
91
            # Fan speed in %
92
            device_stats['fan_speed'] = get_fan_speed(device)
93
            stats.append(device_stats)
94
95
        return stats
96
97
98
def get_device_list(drm_root_folder: str) -> List[str]:
99
    """Return a list of path to the device stats."""
100
    ret = []
101
    for device_folder in glob.glob(DEVICE_FOLDER_PATTERN, root_dir=drm_root_folder):
102
        if os.path.isfile(os.path.join(drm_root_folder, device_folder, GPU_PROC_PERCENT)):
103
            # If the GPU busy file is present then take the card into account
104
            ret.append(os.path.join(drm_root_folder, 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_total = read_file(device_folder, GPU_MEM_TOTAL)
145
    mem_info_used = read_file(device_folder, GPU_MEM_USED)
146
    if mem_info_total and mem_info_used:
147
        mem_info_total = int(mem_info_total)
148
        mem_info_used = int(mem_info_used)
149
        # Detect integrated GPU by looking for APU-only Northbridge voltage.
150
        # See https://docs.kernel.org/gpu/amdgpu/thermal.html
151
        if glob.glob(HWMON_NORTHBRIDGE_VOLTAGE_PATTERN, root_dir=device_folder):
152
            mem_info_gtt_total = read_file(device_folder, GTT_MEM_TOTAL)
153
            mem_info_gtt_used = read_file(device_folder, GTT_MEM_USED)
154
            if mem_info_gtt_total and mem_info_gtt_used:
155
                # Integrated GPU allocates static VRAM and dynamic GTT from the same system memory.
156
                mem_info_total += int(mem_info_gtt_total)
157
                mem_info_used += int(mem_info_gtt_used)
158
        if mem_info_total > 0:
159
            return round(mem_info_used / mem_info_total * 100)
160
    return None
161
162
163
def get_proc(device_folder: str) -> Optional[int]:
164
    """Return the processor consumption in %."""
165
    if gpu_busy_percent := read_file(device_folder, GPU_PROC_PERCENT):
166
        return int(gpu_busy_percent)
167
    return None
168
169
170
def get_temperature(device_folder: str) -> Optional[int]:
171
    """Return the processor temperature in °C (mean of all HWMON)"""
172
    temp_input = []
173
    for temp_file in glob.glob(HWMON_TEMPERATURE_PATTERN, root_dir=device_folder):
174
        if a_temp_input := read_file(device_folder, temp_file):
175
            temp_input.append(int(a_temp_input))
176
        else:
177
            return None
178
    if temp_input:
179
        return round(sum(temp_input) / len(temp_input) / 1000)
180
    return None
181
182
183
def get_fan_speed(device_folder: str) -> Optional[int]:
184
    """Return the fan speed in %."""
185
    return None
186