gardesk/gartop / 0ddda28

Browse files

Add GPU collector reading from sysfs/drm

Authored by espadonne
SHA
0ddda28a8ea62a1e00dd335d3b61ed52a74967e1
Parents
4f99c07
Tree
2af5917

2 changed files

StatusFile+-
A gartop/src/collector/gpu.rs 231 0
M gartop/src/collector/mod.rs 3 1
gartop/src/collector/gpu.rsadded
@@ -0,0 +1,231 @@
1
+//! GPU information collection from sysfs/drm
2
+
3
+use crate::error::Result;
4
+use gartop_ipc::{GpuDevice, GpuStats};
5
+use std::fs;
6
+use std::path::Path;
7
+use std::time::{SystemTime, UNIX_EPOCH};
8
+
9
+/// GPU information collector.
10
+pub struct GpuCollector;
11
+
12
+impl GpuCollector {
13
+    /// Create a new GPU collector.
14
+    pub fn new() -> Result<Self> {
15
+        Ok(Self)
16
+    }
17
+
18
+    /// Collect current GPU stats from all devices.
19
+    pub fn collect(&mut self) -> Result<GpuStats> {
20
+        let mut devices = Vec::new();
21
+
22
+        // Check for DRM devices (AMD, Intel)
23
+        let drm_path = Path::new("/sys/class/drm");
24
+        if drm_path.exists() {
25
+            if let Ok(entries) = fs::read_dir(drm_path) {
26
+                for entry in entries.flatten() {
27
+                    let name = entry.file_name();
28
+                    let name_str = name.to_string_lossy();
29
+
30
+                    // Only process card* entries, not renderD*
31
+                    if name_str.starts_with("card") && !name_str.contains('-') {
32
+                        if let Some(device) = Self::read_drm_device(&entry.path(), &name_str) {
33
+                            devices.push(device);
34
+                        }
35
+                    }
36
+                }
37
+            }
38
+        }
39
+
40
+        let timestamp = SystemTime::now()
41
+            .duration_since(UNIX_EPOCH)
42
+            .map(|d| d.as_millis() as u64)
43
+            .unwrap_or(0);
44
+
45
+        Ok(GpuStats { devices, timestamp })
46
+    }
47
+
48
+    /// Read GPU info from a DRM device directory.
49
+    fn read_drm_device(card_path: &Path, card_name: &str) -> Option<GpuDevice> {
50
+        let device_path = card_path.join("device");
51
+        if !device_path.exists() {
52
+            return None;
53
+        }
54
+
55
+        // Read GPU model from various sources
56
+        let model = Self::read_gpu_model(&device_path);
57
+
58
+        // GPU utilization (AMD: gpu_busy_percent)
59
+        let usage_percent = Self::read_file_f64(device_path.join("gpu_busy_percent"))
60
+            .unwrap_or(0.0);
61
+
62
+        // VRAM stats (AMD)
63
+        let vram_used = Self::read_file_u64(device_path.join("mem_info_vram_used"))
64
+            .unwrap_or(0);
65
+        let vram_total = Self::read_file_u64(device_path.join("mem_info_vram_total"))
66
+            .unwrap_or(0);
67
+
68
+        // GPU clock from hwmon (varies by vendor)
69
+        let clock_mhz = Self::find_gpu_clock(&device_path);
70
+
71
+        // GPU temperature from hwmon
72
+        let temp_celsius = Self::find_gpu_temp(&device_path);
73
+
74
+        // Power usage from hwmon (in microwatts, convert to watts)
75
+        let power_watts = Self::find_gpu_power(&device_path);
76
+
77
+        // Skip if we couldn't get any meaningful data
78
+        if usage_percent == 0.0 && vram_total == 0 && clock_mhz.is_none() && temp_celsius.is_none() {
79
+            return None;
80
+        }
81
+
82
+        Some(GpuDevice {
83
+            name: card_name.to_string(),
84
+            model,
85
+            usage_percent,
86
+            vram_used,
87
+            vram_total,
88
+            clock_mhz,
89
+            temp_celsius,
90
+            power_watts,
91
+        })
92
+    }
93
+
94
+    /// Read GPU model name from device info.
95
+    fn read_gpu_model(device_path: &Path) -> String {
96
+        // Try uevent for DRIVER info
97
+        if let Some(uevent) = Self::read_file(device_path.join("uevent")) {
98
+            for line in uevent.lines() {
99
+                if let Some(driver) = line.strip_prefix("DRIVER=") {
100
+                    return driver.to_string();
101
+                }
102
+            }
103
+        }
104
+
105
+        // Try product name from DRM
106
+        if let Some(name) = Self::read_file(device_path.join("product_name")) {
107
+            return name.trim().to_string();
108
+        }
109
+
110
+        // Try vendor/device for PCI
111
+        let vendor = Self::read_file(device_path.join("vendor"))
112
+            .map(|s| s.trim().to_string())
113
+            .unwrap_or_default();
114
+        let device = Self::read_file(device_path.join("device"))
115
+            .map(|s| s.trim().to_string())
116
+            .unwrap_or_default();
117
+
118
+        if !vendor.is_empty() && !device.is_empty() {
119
+            return format!("{} {}", vendor, device);
120
+        }
121
+
122
+        "Unknown GPU".to_string()
123
+    }
124
+
125
+    /// Find GPU clock frequency from hwmon.
126
+    fn find_gpu_clock(device_path: &Path) -> Option<u32> {
127
+        let hwmon_path = device_path.join("hwmon");
128
+        if !hwmon_path.exists() {
129
+            return None;
130
+        }
131
+
132
+        if let Ok(entries) = fs::read_dir(&hwmon_path) {
133
+            for entry in entries.flatten() {
134
+                let hwmon_dir = entry.path();
135
+
136
+                // Try freq1_input (GPU clock in Hz)
137
+                if let Some(hz) = Self::read_file_u64(hwmon_dir.join("freq1_input")) {
138
+                    return Some((hz / 1_000_000) as u32); // Hz to MHz
139
+                }
140
+
141
+                // Try pp_dpm_sclk for AMD (current clock with * marker)
142
+                if let Some(sclk) = Self::read_file(device_path.join("pp_dpm_sclk")) {
143
+                    for line in sclk.lines() {
144
+                        if line.contains('*') {
145
+                            // Format: "0: 500Mhz *" or similar
146
+                            if let Some(mhz_str) = line.split_whitespace().nth(1) {
147
+                                if let Some(mhz) = mhz_str.strip_suffix("Mhz")
148
+                                    .or_else(|| mhz_str.strip_suffix("MHz"))
149
+                                {
150
+                                    if let Ok(val) = mhz.parse::<u32>() {
151
+                                        return Some(val);
152
+                                    }
153
+                                }
154
+                            }
155
+                        }
156
+                    }
157
+                }
158
+            }
159
+        }
160
+
161
+        None
162
+    }
163
+
164
+    /// Find GPU temperature from hwmon.
165
+    fn find_gpu_temp(device_path: &Path) -> Option<f64> {
166
+        let hwmon_path = device_path.join("hwmon");
167
+        if !hwmon_path.exists() {
168
+            return None;
169
+        }
170
+
171
+        if let Ok(entries) = fs::read_dir(&hwmon_path) {
172
+            for entry in entries.flatten() {
173
+                let hwmon_dir = entry.path();
174
+
175
+                // Look for temp1_input (GPU edge temp)
176
+                if let Some(millidegrees) = Self::read_file_i64(hwmon_dir.join("temp1_input")) {
177
+                    return Some(millidegrees as f64 / 1000.0);
178
+                }
179
+            }
180
+        }
181
+
182
+        None
183
+    }
184
+
185
+    /// Find GPU power usage from hwmon.
186
+    fn find_gpu_power(device_path: &Path) -> Option<f64> {
187
+        let hwmon_path = device_path.join("hwmon");
188
+        if !hwmon_path.exists() {
189
+            return None;
190
+        }
191
+
192
+        if let Ok(entries) = fs::read_dir(&hwmon_path) {
193
+            for entry in entries.flatten() {
194
+                let hwmon_dir = entry.path();
195
+
196
+                // power1_average (microwatts)
197
+                if let Some(microwatts) = Self::read_file_u64(hwmon_dir.join("power1_average")) {
198
+                    return Some(microwatts as f64 / 1_000_000.0);
199
+                }
200
+            }
201
+        }
202
+
203
+        None
204
+    }
205
+
206
+    /// Read a file to string.
207
+    fn read_file<P: AsRef<Path>>(path: P) -> Option<String> {
208
+        fs::read_to_string(path).ok()
209
+    }
210
+
211
+    /// Read a file as u64.
212
+    fn read_file_u64<P: AsRef<Path>>(path: P) -> Option<u64> {
213
+        Self::read_file(path)?.trim().parse().ok()
214
+    }
215
+
216
+    /// Read a file as i64.
217
+    fn read_file_i64<P: AsRef<Path>>(path: P) -> Option<i64> {
218
+        Self::read_file(path)?.trim().parse().ok()
219
+    }
220
+
221
+    /// Read a file as f64.
222
+    fn read_file_f64<P: AsRef<Path>>(path: P) -> Option<f64> {
223
+        Self::read_file(path)?.trim().parse().ok()
224
+    }
225
+}
226
+
227
+impl Default for GpuCollector {
228
+    fn default() -> Self {
229
+        Self
230
+    }
231
+}
gartop/src/collector/mod.rsmodified
@@ -1,9 +1,10 @@
11
 //! System data collectors
22
 //!
3
-//! Collects CPU, memory, process, network, disk, and temperature data from procfs/sysfs.
3
+//! Collects CPU, memory, process, network, disk, temperature, and GPU data from procfs/sysfs.
44
 
55
 mod cpu;
66
 mod disk;
7
+mod gpu;
78
 mod history;
89
 mod memory;
910
 mod network;
@@ -13,6 +14,7 @@ mod temperature;
1314
 
1415
 pub use cpu::CpuCollector;
1516
 pub use disk::DiskCollector;
17
+pub use gpu::GpuCollector;
1618
 pub use history::History;
1719
 pub use memory::MemoryCollector;
1820
 pub use network::NetworkCollector;