new feature: gpu support

This commit is contained in:
jasonwitty 2025-08-11 12:04:55 -07:00
parent a4f69a5f7d
commit 20278d67f1
11 changed files with 525 additions and 33 deletions

83
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,83 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'socktop'",
"cargo": {
"args": [
"build",
"--bin=socktop",
"--package=socktop"
],
"filter": {
"name": "socktop",
"kind": "bin"
}
},
"args": ["ws://127.0.0.1:3000/ws"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'socktop'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=socktop",
"--package=socktop"
],
"filter": {
"name": "socktop",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'socktop_agent'",
"cargo": {
"args": [
"build",
"--bin=socktop_agent",
"--package=socktop_agent"
],
"filter": {
"name": "socktop_agent",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'socktop_agent'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=socktop_agent",
"--package=socktop_agent"
],
"filter": {
"name": "socktop_agent",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

236
Cargo.lock generated
View File

@ -253,6 +253,16 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@ -534,6 +544,21 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "gfxinfo"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14f33b12fba19d158bebe0262fd2b6c49fee6f0e0e089f4f3fdc11c017548473"
dependencies = [
"core-foundation",
"io-kit-sys",
"libdrm_amdgpu_sys",
"nvml-wrapper",
"serde",
"windows 0.61.3",
"wmi",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.31.1"
@ -650,7 +675,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core", "windows-core 0.61.2",
] ]
[[package]] [[package]]
@ -794,6 +819,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "io-kit-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
dependencies = [
"core-foundation-sys",
"mach2",
]
[[package]] [[package]]
name = "io-uring" name = "io-uring"
version = "0.7.9" version = "0.7.9"
@ -842,6 +877,25 @@ version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libdrm_amdgpu_sys"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a7bf95f3c5d8a8ae4a5c0c46fe2271c16fdf8fd4b26978f44b082a0f8ebaaae"
dependencies = [
"libc",
]
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.53.3",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.15" version = "0.4.15"
@ -879,6 +933,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -967,6 +1030,29 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "nvml-wrapper"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9bff0aa1d48904a1385ea2a8b97576fbdcbc9a3cfccd0d31fe978e1c4038c5"
dependencies = [
"bitflags",
"libloading",
"nvml-wrapper-sys",
"static_assertions",
"thiserror 1.0.69",
"wrapcenum-derive",
]
[[package]]
name = "nvml-wrapper-sys"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "698d45156f28781a4e79652b6ebe2eaa0589057d588d3aec1333f6466f13fcb5"
dependencies = [
"libloading",
]
[[package]] [[package]]
name = "objc2-core-foundation" name = "objc2-core-foundation"
version = "0.3.1" version = "0.3.1"
@ -1389,6 +1475,8 @@ dependencies = [
"axum", "axum",
"futures", "futures",
"futures-util", "futures-util",
"gfxinfo",
"nvml-wrapper",
"serde", "serde",
"serde_json", "serde_json",
"sysinfo", "sysinfo",
@ -1476,7 +1564,7 @@ dependencies = [
"ntapi", "ntapi",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-kit", "objc2-io-kit",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@ -1485,7 +1573,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
] ]
[[package]] [[package]]
@ -1499,6 +1596,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.9" version = "1.1.9"
@ -1665,7 +1773,7 @@ dependencies = [
"log", "log",
"rand", "rand",
"sha1", "sha1",
"thiserror", "thiserror 1.0.69",
"utf-8", "utf-8",
] ]
@ -1825,17 +1933,39 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529"
dependencies = [
"windows-collections 0.1.1",
"windows-core 0.60.1",
"windows-future 0.1.1",
"windows-link",
"windows-numerics 0.1.1",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.3" version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [ dependencies = [
"windows-collections", "windows-collections 0.2.0",
"windows-core", "windows-core 0.61.2",
"windows-future", "windows-future 0.2.1",
"windows-link", "windows-link",
"windows-numerics", "windows-numerics 0.2.0",
]
[[package]]
name = "windows-collections"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec"
dependencies = [
"windows-core 0.60.1",
] ]
[[package]] [[package]]
@ -1844,7 +1974,20 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.60.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247"
dependencies = [
"windows-implement 0.59.0",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings 0.3.1",
] ]
[[package]] [[package]]
@ -1853,11 +1996,21 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [ dependencies = [
"windows-implement", "windows-implement 0.60.0",
"windows-interface", "windows-interface",
"windows-link", "windows-link",
"windows-result", "windows-result",
"windows-strings", "windows-strings 0.4.2",
]
[[package]]
name = "windows-future"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0"
dependencies = [
"windows-core 0.60.1",
"windows-link",
] ]
[[package]] [[package]]
@ -1866,11 +2019,22 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.61.2",
"windows-link", "windows-link",
"windows-threading", "windows-threading",
] ]
[[package]]
name = "windows-implement"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.0" version = "0.60.0"
@ -1899,13 +2063,23 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-numerics"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed"
dependencies = [
"windows-core 0.60.1",
"windows-link",
]
[[package]] [[package]]
name = "windows-numerics" name = "windows-numerics"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.61.2",
"windows-link", "windows-link",
] ]
@ -1918,6 +2092,15 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.4.2" version = "0.4.2"
@ -2149,6 +2332,33 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "wmi"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f902b4592b911109e7352bcfec7b754b07ec71e514d7dfa280eaef924c1cb08"
dependencies = [
"chrono",
"futures",
"log",
"serde",
"thiserror 2.0.12",
"windows 0.60.0",
"windows-core 0.60.1",
]
[[package]]
name = "wrapcenum-derive"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.1" version = "0.6.1"

View File

@ -13,7 +13,8 @@ use crossterm::{
}; };
use ratatui::{ use ratatui::{
backend::CrosstermBackend, backend::CrosstermBackend,
layout::{Constraint, Direction, Rect}, // add Rect layout::{Constraint, Direction, Rect},
//style::Color, // + add Color
Terminal, Terminal,
}; };
use tokio::time::sleep; use tokio::time::sleep;
@ -33,6 +34,7 @@ use crate::ui::{
processes::draw_top_processes, processes::draw_top_processes,
swap::draw_swap, swap::draw_swap,
}; };
use crate::ui::gpu::draw_gpu;
use crate::ws::{connect, request_metrics}; use crate::ws::{connect, request_metrics};
pub struct App { pub struct App {
@ -243,52 +245,77 @@ impl App {
self.last_metrics = Some(m); self.last_metrics = Some(m);
} }
fn draw(&mut self, f: &mut ratatui::Frame<'_>) { pub fn draw(&mut self, f: &mut ratatui::Frame<'_>) {
let area = f.area(); let area = f.area();
// Root rows: header, top (cpu avg + per-core), memory, swap, bottom
let rows = ratatui::layout::Layout::default() let rows = ratatui::layout::Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Length(1), Constraint::Length(1), // header
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), // top row
Constraint::Length(3), Constraint::Length(3), // memory (left) + GPU (right, part 1)
Constraint::Length(3), Constraint::Length(3), // swap (left) + GPU (right, part 2)
Constraint::Min(10), Constraint::Min(10), // bottom: disks + net (left), top procs (right)
]) ])
.split(area); .split(area);
// Header
draw_header(f, rows[0], self.last_metrics.as_ref()); draw_header(f, rows[0], self.last_metrics.as_ref());
let top = ratatui::layout::Layout::default() // Top row: left CPU avg, right Per-core (full top-right)
let top_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)]) .constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[1]); .split(rows[1]);
draw_cpu_avg_graph(f, top[0], &self.cpu_hist, self.last_metrics.as_ref()); draw_cpu_avg_graph(f, top_lr[0], &self.cpu_hist, self.last_metrics.as_ref());
draw_per_core_bars( draw_per_core_bars(
f, f,
top[1], top_lr[1],
self.last_metrics.as_ref(), self.last_metrics.as_ref(),
&self.per_core_hist, &self.per_core_hist,
self.per_core_scroll, self.per_core_scroll,
); );
draw_mem(f, rows[2], self.last_metrics.as_ref()); // Memory + Swap rows split into left/right columns
draw_swap(f, rows[3], self.last_metrics.as_ref()); let mem_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[2]);
let swap_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[3]);
let bottom = ratatui::layout::Layout::default() // Left: Memory + Swap
draw_mem(f, mem_lr[0], self.last_metrics.as_ref());
draw_swap(f, swap_lr[0], self.last_metrics.as_ref());
// Right: GPU spans the same vertical space as Memory + Swap
let gpu_area = ratatui::layout::Rect {
x: mem_lr[1].x,
y: mem_lr[1].y,
width: mem_lr[1].width,
height: mem_lr[1].height + swap_lr[1].height,
};
draw_gpu(f, gpu_area, self.last_metrics.as_ref());
// Bottom area: left = Disks + Network, right = Top Processes
let bottom_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)]) .constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[4]); .split(rows[4]);
// Left bottom: Disks + Net stacked (network "back up")
let left_stack = ratatui::layout::Layout::default() let left_stack = ratatui::layout::Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Min(6), Constraint::Min(7), // Disks grow
Constraint::Length(4), Constraint::Length(3), // Download
Constraint::Length(4), Constraint::Length(3), // Upload
]) ])
.split(bottom[0]); .split(bottom_lr[0]);
draw_disks(f, left_stack[0], self.last_metrics.as_ref()); draw_disks(f, left_stack[0], self.last_metrics.as_ref());
draw_net_spark( draw_net_spark(
@ -314,7 +341,8 @@ impl App {
ratatui::style::Color::Blue, ratatui::style::Color::Blue,
); );
draw_top_processes(f, bottom[1], self.last_metrics.as_ref()); // Right bottom: Top Processes fills the column
draw_top_processes(f, bottom_lr[1], self.last_metrics.as_ref());
} }
} }

View File

@ -24,6 +24,15 @@ pub struct ProcessInfo {
pub mem_bytes: u64, pub mem_bytes: u64,
} }
#[derive(Debug, Clone, serde::Deserialize)]
pub struct GpuMetrics {
pub name: String,
pub utilization_gpu_pct: u32,
pub mem_used_bytes: u64,
pub mem_total_bytes: u64,
// pub vendor: Option<String>,
}
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct Metrics { pub struct Metrics {
pub cpu_total: f32, pub cpu_total: f32,
@ -38,4 +47,5 @@ pub struct Metrics {
pub disks: Vec<Disk>, pub disks: Vec<Disk>,
pub networks: Vec<Network>, pub networks: Vec<Network>,
pub top_processes: Vec<ProcessInfo>, pub top_processes: Vec<ProcessInfo>,
pub gpus: Option<Vec<GpuMetrics>>,
} }

115
socktop/src/ui/gpu.rs Normal file
View File

@ -0,0 +1,115 @@
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, Borders, Gauge, Paragraph},
};
use crate::types::Metrics;
fn fmt_bytes(b: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let fb = b as f64;
if fb >= GB { format!("{:.1}G", fb / GB) }
else if fb >= MB { format!("{:.1}M", fb / MB) }
else if fb >= KB { format!("{:.1}K", fb / KB) }
else { format!("{}B", b) }
}
pub fn draw_gpu(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let mut area = area;
let block = Block::default().borders(Borders::ALL).title("GPU");
f.render_widget(block, area);
// Guard: need some space inside the block
if area.height <= 2 || area.width <= 2 {
return;
}
// Inner padding consistent with the rest of the app
area.y += 1;
area.height = area.height.saturating_sub(2);
area.x += 1;
area.width = area.width.saturating_sub(2);
let Some(metrics) = m else { return; };
let Some(gpus) = metrics.gpus.as_ref() else {
f.render_widget(Paragraph::new("No GPUs"), area);
return;
};
if gpus.is_empty() {
f.render_widget(Paragraph::new("No GPUs"), area);
return;
}
// Show 3 rows per GPU: name, util bar, vram bar.
if area.height < 3 {
return;
}
let per_gpu_rows: u16 = 3;
let max_gpus = (area.height / per_gpu_rows) as usize;
let count = gpus.len().min(max_gpus);
let constraints = std::iter::repeat(Constraint::Length(1))
.take(count * per_gpu_rows as usize)
.collect::<Vec<_>>();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
// Per bar horizontal layout: [gauge] [value]
let split_bar = |r: Rect| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(8), // gauge column
Constraint::Length(24), // value column
])
.split(r)
};
for i in 0..count {
let g = &gpus[i];
// Row 1: GPU name (and temp can be appended later)
let name_text = format!("{}", g.name);
f.render_widget(
Paragraph::new(Span::raw(name_text)).style(Style::default().fg(Color::Gray)),
rows[i * 3],
);
// Row 2: Utilization bar + right label
let util_cols = split_bar(rows[i * 3 + 1]);
let util = g.utilization_gpu_pct.min(100) as u16;
let util_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Green))
.label(Span::raw(""))
.ratio(util as f64 / 100.0);
f.render_widget(util_gauge, util_cols[0]);
f.render_widget(
Paragraph::new(Span::raw(format!("util: {}%", util))).style(Style::default().fg(Color::Gray)),
util_cols[1],
);
// Row 3: VRAM bar + right label
let mem_cols = split_bar(rows[i * 3 + 2]);
let used = g.mem_used_bytes;
let total = g.mem_total_bytes.max(1);
let mem_ratio = used as f64 / total as f64;
let mem_pct = (mem_ratio * 100.0).round() as u16;
let mem_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::LightMagenta))
.label(Span::raw(""))
.ratio(mem_ratio);
f.render_widget(mem_gauge, mem_cols[0]);
f.render_widget(
Paragraph::new(Span::raw(format!("vram: {}/{} ({}%)", fmt_bytes(used), fmt_bytes(total), mem_pct)))
.style(Style::default().fg(Color::Gray)),
mem_cols[1],
);
}
}

View File

@ -2,6 +2,7 @@
pub mod cpu; pub mod cpu;
pub mod disks; pub mod disks;
pub mod gpu;
pub mod header; pub mod header;
pub mod mem; pub mod mem;
pub mod net; pub mod net;

View File

@ -14,4 +14,6 @@ serde_json = "1"
futures = "0.3" futures = "0.3"
futures-util = "0.3.31" futures-util = "0.3.31"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
nvml-wrapper = "0.10"
gfxinfo = "0.1.2"

26
socktop_agent/src/gpu.rs Normal file
View File

@ -0,0 +1,26 @@
// gpu.rs
use gfxinfo::active_gpu;
#[derive(Debug, Clone, serde::Serialize)]
pub struct GpuMetrics {
pub name: String,
pub utilization_gpu_pct: u32, // 0..100
pub mem_used_bytes: u64,
pub mem_total_bytes: u64,
// pub vendor: String,
}
pub fn collect_all_gpus() -> Result<Vec<GpuMetrics>, Box<dyn std::error::Error>> {
let gpu = active_gpu()?; // Use ? to unwrap Result
let info = gpu.info();
let metrics = GpuMetrics {
name: gpu.model().to_string(),
utilization_gpu_pct: info.load_pct() as u32,
mem_used_bytes: info.used_vram(),
mem_total_bytes: info.total_vram(),
// vendor: gpu.vendor().to_string(),
};
Ok(vec![metrics])
}

View File

@ -6,6 +6,7 @@ mod sampler;
mod state; mod state;
mod types; mod types;
mod ws; mod ws;
mod gpu;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use std::{ use std::{

View File

@ -1,10 +1,18 @@
//! Metrics collection using sysinfo. Keeps sysinfo handles in AppState to //! Metrics collection using sysinfo. Keeps sysinfo handles in AppState to
//! avoid repeated allocations and allow efficient refreshes. //! avoid repeated allocations and allow efficient refreshes.
use crate::gpu::collect_all_gpus;
use crate::state::AppState; use crate::state::AppState;
use crate::types::{DiskInfo, Metrics, NetworkInfo, ProcessInfo}; use crate::types::{DiskInfo, Metrics, NetworkInfo, ProcessInfo};
use sysinfo::{Components, System}; use sysinfo::{Components, System};
pub async fn collect_metrics(state: &AppState) -> Metrics { pub async fn collect_metrics(state: &AppState) -> Metrics {
// System (CPU/mem/proc) // System (CPU/mem/proc)
let mut sys = state.sys.lock().await; let mut sys = state.sys.lock().await;
@ -99,6 +107,11 @@ pub async fn collect_metrics(state: &AppState) -> Metrics {
}); });
} }
let gpus = match collect_all_gpus() {
Ok(v) if !v.is_empty() => Some(v),
_ => None,
};
Metrics { Metrics {
cpu_total: sys.global_cpu_usage(), cpu_total: sys.global_cpu_usage(),
cpu_per_core: sys.cpus().iter().map(|c| c.cpu_usage()).collect(), cpu_per_core: sys.cpus().iter().map(|c| c.cpu_usage()).collect(),
@ -112,6 +125,7 @@ pub async fn collect_metrics(state: &AppState) -> Metrics {
disks, disks,
networks, networks,
top_processes: procs, top_processes: procs,
gpus,
} }
} }

View File

@ -1,6 +1,7 @@
//! Data types sent to the client over WebSocket. //! Data types sent to the client over WebSocket.
//! Keep this module minimal and stable — it defines the wire format. //! Keep this module minimal and stable — it defines the wire format.
use crate::gpu::GpuMetrics;
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
@ -26,7 +27,7 @@ pub struct NetworkInfo {
pub transmitted: u64, pub transmitted: u64,
} }
#[derive(Debug, Serialize, Clone)] #[derive(Serialize)]
pub struct Metrics { pub struct Metrics {
pub cpu_total: f32, pub cpu_total: f32,
pub cpu_per_core: Vec<f32>, pub cpu_per_core: Vec<f32>,
@ -40,4 +41,5 @@ pub struct Metrics {
pub disks: Vec<DiskInfo>, pub disks: Vec<DiskInfo>,
pub networks: Vec<NetworkInfo>, pub networks: Vec<NetworkInfo>,
pub top_processes: Vec<ProcessInfo>, pub top_processes: Vec<ProcessInfo>,
pub gpus: Option<Vec<GpuMetrics>>, // new
} }