diff --git a/backend/sysinfo.go b/backend/sysinfo.go index ccebcb7..9f32ac3 100644 --- a/backend/sysinfo.go +++ b/backend/sysinfo.go @@ -70,15 +70,17 @@ type ProcessInfo struct { Mem uint64 `json:"mem"` ReadBps uint64 `json:"read_bps"` WriteBps uint64 `json:"write_bps"` + NetConns int `json:"net_conns"` } // GPUProcessInfo holds per-process GPU stats from nvidia-smi pmon. type GPUProcessInfo struct { - PID int32 `json:"pid"` - Name string `json:"name"` - Type string `json:"type"` // "C", "G", or "C+G" - VRAM uint64 `json:"vram_mb"` // framebuffer memory in MB - RAM uint64 `json:"ram"` // resident set size in bytes + PID int32 `json:"pid"` + Name string `json:"name"` + Type string `json:"type"` // "C", "G", or "C+G" + VRAM uint64 `json:"vram_mb"` // framebuffer memory in MB + RAM uint64 `json:"ram"` // resident set size in bytes + SMUtil uint32 `json:"sm_util"` // SM (CUDA core) utilization % } type Metrics struct { @@ -116,6 +118,16 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) { cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false) vm, _ := mem.VirtualMemory() + // Aggregate network connections per PID in one call + netConnByPid := make(map[int32]int) + if allConns, err := psnet.Connections("all"); err == nil { + for _, c := range allConns { + if c.Pid > 0 { + netConnByPid[c.Pid]++ + } + } + } + // Collect all processes with disk I/O rates procs, _ := ps.Processes() newProcIO := make(map[int32][2]uint64, len(procs)) @@ -149,6 +161,7 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) { Mem: memBytes, ReadBps: readBps, WriteBps: writeBps, + NetConns: netConnByPid[p.Pid], }) } // Sort: primary by CPU desc, secondary by Mem desc @@ -221,7 +234,9 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) { // queryGPUProcesses lists all processes currently using the GPU via nvidia-smi pmon. func queryGPUProcesses(ctx context.Context) []GPUProcessInfo { - cmd := exec.CommandContext(ctx, "nvidia-smi", "pmon", "-s", "m", "-c", "1") + // -s mu: SM utilization + memory (fb) + // columns: gpuIdx pid type sm% mem% enc% dec% fbMB ccpmMB command... + cmd := exec.CommandContext(ctx, "nvidia-smi", "pmon", "-s", "mu", "-c", "1") out, err := cmd.Output() if err != nil { return nil @@ -233,26 +248,26 @@ func queryGPUProcesses(ctx context.Context) []GPUProcessInfo { continue } fields := strings.Fields(line) - // expected: [gpuIdx, pid, type, fbMB, ccpmMB, name...] - if len(fields) < 5 { + // expected: [gpuIdx, pid, type, sm%, mem%, enc%, dec%, fbMB, ccpmMB, name...] + if len(fields) < 9 { continue } pid64, err := strconv.ParseInt(fields[1], 10, 32) if err != nil { continue } - vram, _ := strconv.ParseUint(fields[3], 10, 64) - name := "" - if len(fields) >= 6 { - name = strings.Join(fields[5:], " ") - } else { + sm64, _ := strconv.ParseUint(fields[3], 10, 32) + vram, _ := strconv.ParseUint(fields[7], 10, 64) + name := strings.Join(fields[9:], " ") + if name == "" { name = fields[len(fields)-1] } gp := GPUProcessInfo{ - PID: int32(pid64), - Name: name, - Type: fields[2], - VRAM: vram, + PID: int32(pid64), + Name: name, + Type: fields[2], + VRAM: vram, + SMUtil: uint32(sm64), } // look up RAM (RSS) for this process if p, err := ps.NewProcess(int32(pid64)); err == nil { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 88bfd94..73ee0b9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ type ProcessInfo = { mem: number read_bps: number write_bps: number + net_conns: number } type GPUProcessInfo = { @@ -24,6 +25,7 @@ type GPUProcessInfo = { type: string vram_mb: number ram: number + sm_util: number } type Metrics = { @@ -70,7 +72,7 @@ type ProcessDetail = { conn_count: number } -type SortField = 'cpu' | 'mem' | 'disk' | 'vram' +type SortField = 'pid' | 'name' | 'cpu' | 'mem' | 'disk' | 'net' | 'vram' | 'gpu' type NavItem = { pid: number; name: string } // --------------------------------------------------------------------------- @@ -244,7 +246,7 @@ function ProcessDetailPanel({ /> {/* Panel */} -
+
{/* Panel header */}
@@ -523,7 +525,8 @@ function ProcessDetailPanel({ export default function App() { const [metrics, setMetrics] = useState(null) - const [sortBy, setSortBy] = useState('cpu') + const [sortField, setSortField] = useState('cpu') + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc') const [selectedPid, setSelectedPid] = useState(null) const [page, setPage] = useState(0) const [procOpen, setProcOpen] = useState(true) @@ -543,8 +546,12 @@ export default function App() { return () => EventsOff('metrics') }, []) - const toggleSort = useCallback((field: SortField) => { - setSortBy(field) + const handleSort = useCallback((field: SortField) => { + setSortField(prev => { + if (prev === field) setSortDir(d => d === 'asc' ? 'desc' : 'asc') + else setSortDir('desc') + return field + }) setPage(0) }, []) @@ -565,10 +572,19 @@ export default function App() { ? [...metrics.processes] .filter(p => !search || p.name.toLowerCase().includes(searchLower) || String(p.pid).includes(search)) .sort((a, b) => { - if (sortBy === 'disk') return (b.read_bps + b.write_bps) - (a.read_bps + a.write_bps) - if (sortBy === 'vram') return (gpuByPid.get(b.pid)?.vram_mb ?? 0) - (gpuByPid.get(a.pid)?.vram_mb ?? 0) - if (sortBy === 'cpu') return b.cpu - a.cpu - return b.mem - a.mem + let va: number | string = 0, vb: number | string = 0 + switch (sortField) { + case 'pid': va = a.pid; vb = b.pid; break + case 'name': va = a.name; vb = b.name; break + case 'cpu': va = a.cpu; vb = b.cpu; break + case 'mem': va = a.mem; vb = b.mem; break + case 'disk': va = a.read_bps + a.write_bps; vb = b.read_bps + b.write_bps; break + case 'net': va = a.net_conns; vb = b.net_conns; break + case 'vram': va = gpuByPid.get(a.pid)?.vram_mb ?? 0; vb = gpuByPid.get(b.pid)?.vram_mb ?? 0; break + case 'gpu': va = gpuByPid.get(a.pid)?.sm_util ?? 0; vb = gpuByPid.get(b.pid)?.sm_util ?? 0; break + } + if (typeof va === 'string') return sortDir === 'asc' ? va.localeCompare(vb as string) : (vb as string).localeCompare(va) + return sortDir === 'asc' ? (va as number) - (vb as number) : (vb as number) - (va as number) }) : [] const totalPages = Math.ceil(sortedProcesses.length / pageSize) @@ -596,7 +612,7 @@ export default function App() {
-
+
{/* Search */}
@@ -658,32 +674,13 @@ export default function App() {
)}
- {procOpen && ( -
- {(['cpu', 'mem', 'disk', ...(metrics?.gpu_name ? ['vram'] : [])] as SortField[]).map((field) => ( - - ))} -
- )}
{procOpen && ( {/* System resource bars */} -
+
CPU @@ -731,15 +728,45 @@ export default function App() {
)}
- +
+
- - - - - - {metrics?.gpu_name && } + {([ + { f: 'pid', label: 'PID', title: 'Process ID', align: 'left' }, + { f: 'name', label: 'Name', title: 'Executable name', align: 'left' }, + { f: 'cpu', label: 'CPU', title: 'CPU % of one core', align: 'right' }, + { f: 'mem', label: 'Mem', title: 'Memory % of total RAM', align: 'right' }, + { f: 'disk', label: 'Disk', title: 'Disk I/O throughput', align: 'right' }, + { f: 'net', label: 'Net', title: 'Active network connections', align: 'right' }, + ...(metrics?.gpu_name ? [ + { f: 'gpu', label: 'GPU%', title: 'GPU SM (shader) utilization', align: 'right' }, + { f: 'vram', label: 'VRAM', title: 'GPU VRAM used by this process', align: 'right' }, + { f: 'type', label: 'Type', title: 'C=Compute G=Graphics C+G=both', align: 'center' }, + ] : []), + ] as Array<{ f: string; label: string; title: string; align: string }>).map(({ f, label, title, align }) => { + const sortable = f !== 'type' + const active = sortField === f + return ( + + ) + })} @@ -754,8 +781,8 @@ export default function App() { : 'hover:bg-zinc-900/60' )} > - - + + - {metrics?.gpu_name && (gpuByPid.has(p.pid) ? ( + + {metrics?.gpu_name && (gpuByPid.has(p.pid) ? (<> + - ) : ( + + ) : (<> - ))} + + + ))} ))} {pagedProcesses.length === 0 && ( - )}
PIDNameCPUMemoryDiskGPU handleSort(f as SortField) : undefined} + className={cn( + 'py-2 px-3 text-xs font-medium uppercase tracking-wider select-none', + align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left', + sortable ? 'cursor-pointer' : '', + active ? 'text-zinc-300' : 'text-zinc-600 hover:text-zinc-400' + )} + > + + {label} + {sortable && (active + ? + : + )} + +
{p.pid}{p.name}{p.pid}{p.name}
= 50 ? 'text-red-400' : p.cpu >= 20 ? 'text-amber-400' : 'text-zinc-500')}> @@ -788,35 +815,65 @@ export default function App() { )}
+ {p.net_conns > 0 ? ( +
+ {p.net_conns} +
+
+
+
+ ) : } +
+ {gpuByPid.get(p.pid)!.sm_util > 0 ? ( +
+ = 50 ? 'text-violet-400' : 'text-violet-600' + )}>{gpuByPid.get(p.pid)!.sm_util}% +
+
+
+
+ ) : } +
- - {gpuByPid.get(p.pid)!.type} - - {gpuByPid.get(p.pid)!.vram_mb} MB + {gpuByPid.get(p.pid)!.vram_mb} MB +
+
+
+ + {gpuByPid.get(p.pid)!.type} + +
+ Waiting for metrics…
+
{/* Pagination */} {totalPages > 0 && ( diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 13555b9..04db765 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -6,6 +6,7 @@ export namespace backend { type: string; vram_mb: number; ram: number; + sm_util: number; static createFrom(source: any = {}) { return new GPUProcessInfo(source); @@ -18,6 +19,7 @@ export namespace backend { this.type = source["type"]; this.vram_mb = source["vram_mb"]; this.ram = source["ram"]; + this.sm_util = source["sm_util"]; } } export class ProcessInfo { @@ -27,6 +29,7 @@ export namespace backend { mem: number; read_bps: number; write_bps: number; + net_conns: number; static createFrom(source: any = {}) { return new ProcessInfo(source); @@ -40,6 +43,7 @@ export namespace backend { this.mem = source["mem"]; this.read_bps = source["read_bps"]; this.write_bps = source["write_bps"]; + this.net_conns = source["net_conns"]; } } export class Metrics {