feat: enhance metrics collection with disk and network I/O rates, and update frontend to display new data
This commit is contained in:
@@ -6,10 +6,13 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
|
psdisk "github.com/shirou/gopsutil/v3/disk"
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
psnet "github.com/shirou/gopsutil/v3/net"
|
||||||
ps "github.com/shirou/gopsutil/v3/process"
|
ps "github.com/shirou/gopsutil/v3/process"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
@@ -17,15 +20,35 @@ import (
|
|||||||
type SysInfo struct {
|
type SysInfo struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
mu sync.Mutex
|
||||||
|
lastProcIO map[int32][2]uint64 // pid -> [readBytes, writeBytes]
|
||||||
|
lastDiskIO [2]uint64 // [totalReadBytes, totalWriteBytes]
|
||||||
|
lastNetIO [2]uint64 // [bytesRecv, bytesSent]
|
||||||
|
lastTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSysInfo(ctx context.Context) *SysInfo {
|
func NewSysInfo(ctx context.Context) *SysInfo {
|
||||||
cctx, cancel := context.WithCancel(ctx)
|
cctx, cancel := context.WithCancel(ctx)
|
||||||
s := &SysInfo{ctx: cctx, cancel: cancel}
|
s := &SysInfo{
|
||||||
|
ctx: cctx,
|
||||||
|
cancel: cancel,
|
||||||
|
lastProcIO: make(map[int32][2]uint64),
|
||||||
|
}
|
||||||
go s.startEmitter()
|
go s.startEmitter()
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSysInfoHeadless creates a SysInfo for use without a Wails GUI context.
|
||||||
|
// Use Snapshot() to collect metrics; no Wails events are emitted.
|
||||||
|
func NewSysInfoHeadless(ctx context.Context) *SysInfo {
|
||||||
|
cctx, cancel := context.WithCancel(ctx)
|
||||||
|
return &SysInfo{
|
||||||
|
ctx: cctx,
|
||||||
|
cancel: cancel,
|
||||||
|
lastProcIO: make(map[int32][2]uint64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SysInfo) startEmitter() {
|
func (s *SysInfo) startEmitter() {
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -34,27 +57,19 @@ func (s *SysInfo) startEmitter() {
|
|||||||
case <-s.ctx.Done():
|
case <-s.ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
info, _ := s.CollectOnce()
|
info, _ := s.Snapshot()
|
||||||
// attach GPU global stats
|
|
||||||
name, tot, used, util := queryGPU(s.ctx)
|
|
||||||
if name != "" {
|
|
||||||
info.GPUName = name
|
|
||||||
info.GPUTotal = tot
|
|
||||||
info.GPUUsed = used
|
|
||||||
info.GPUUtil = util
|
|
||||||
// attach per-process GPU stats
|
|
||||||
info.GPUProcesses = queryGPUProcesses(s.ctx)
|
|
||||||
}
|
|
||||||
runtime.EventsEmit(s.ctx, "metrics", info)
|
runtime.EventsEmit(s.ctx, "metrics", info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProcessInfo struct {
|
type ProcessInfo struct {
|
||||||
PID int32 `json:"pid"`
|
PID int32 `json:"pid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CPU float64 `json:"cpu"`
|
CPU float64 `json:"cpu"`
|
||||||
Mem uint64 `json:"mem"`
|
Mem uint64 `json:"mem"`
|
||||||
|
ReadBps uint64 `json:"read_bps"`
|
||||||
|
WriteBps uint64 `json:"write_bps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GPUProcessInfo holds per-process GPU stats from nvidia-smi pmon.
|
// GPUProcessInfo holds per-process GPU stats from nvidia-smi pmon.
|
||||||
@@ -77,15 +92,33 @@ type Metrics struct {
|
|||||||
GPUUsed uint64 `json:"gpu_used_mem,omitempty"`
|
GPUUsed uint64 `json:"gpu_used_mem,omitempty"`
|
||||||
GPUUtil float64 `json:"gpu_util_percent,omitempty"`
|
GPUUtil float64 `json:"gpu_util_percent,omitempty"`
|
||||||
GPUProcesses []GPUProcessInfo `json:"gpu_processes,omitempty"`
|
GPUProcesses []GPUProcessInfo `json:"gpu_processes,omitempty"`
|
||||||
|
DiskReadBps uint64 `json:"disk_read_bps"`
|
||||||
|
DiskWriteBps uint64 `json:"disk_write_bps"`
|
||||||
|
NetRecvBps uint64 `json:"net_recv_bps"`
|
||||||
|
NetSendBps uint64 `json:"net_send_bps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CollectOnce gathers a snapshot of system metrics.
|
// CollectOnce gathers a snapshot of system metrics.
|
||||||
func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Snapshot previous IO state
|
||||||
|
s.mu.Lock()
|
||||||
|
elapsed := 0.0
|
||||||
|
if !s.lastTime.IsZero() {
|
||||||
|
elapsed = now.Sub(s.lastTime).Seconds()
|
||||||
|
}
|
||||||
|
prevProcIO := s.lastProcIO
|
||||||
|
prevDiskIO := s.lastDiskIO
|
||||||
|
prevNetIO := s.lastNetIO
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false)
|
cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false)
|
||||||
vm, _ := mem.VirtualMemory()
|
vm, _ := mem.VirtualMemory()
|
||||||
|
|
||||||
// Collect ALL processes, then sort and keep top 50
|
// Collect all processes with disk I/O rates
|
||||||
procs, _ := ps.Processes()
|
procs, _ := ps.Processes()
|
||||||
|
newProcIO := make(map[int32][2]uint64, len(procs))
|
||||||
var list []ProcessInfo
|
var list []ProcessInfo
|
||||||
for _, p := range procs {
|
for _, p := range procs {
|
||||||
name, _ := p.Name()
|
name, _ := p.Name()
|
||||||
@@ -95,7 +128,28 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
|||||||
if memInfo != nil {
|
if memInfo != nil {
|
||||||
memBytes = memInfo.RSS
|
memBytes = memInfo.RSS
|
||||||
}
|
}
|
||||||
list = append(list, ProcessInfo{PID: p.Pid, Name: name, CPU: cpuPct, Mem: memBytes})
|
var readBps, writeBps uint64
|
||||||
|
if ioStats, err := p.IOCounters(); err == nil {
|
||||||
|
newProcIO[p.Pid] = [2]uint64{ioStats.ReadBytes, ioStats.WriteBytes}
|
||||||
|
if elapsed > 0 {
|
||||||
|
if prev, ok := prevProcIO[p.Pid]; ok {
|
||||||
|
if ioStats.ReadBytes >= prev[0] {
|
||||||
|
readBps = uint64(float64(ioStats.ReadBytes-prev[0]) / elapsed)
|
||||||
|
}
|
||||||
|
if ioStats.WriteBytes >= prev[1] {
|
||||||
|
writeBps = uint64(float64(ioStats.WriteBytes-prev[1]) / elapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list = append(list, ProcessInfo{
|
||||||
|
PID: p.Pid,
|
||||||
|
Name: name,
|
||||||
|
CPU: cpuPct,
|
||||||
|
Mem: memBytes,
|
||||||
|
ReadBps: readBps,
|
||||||
|
WriteBps: writeBps,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// Sort: primary by CPU desc, secondary by Mem desc
|
// Sort: primary by CPU desc, secondary by Mem desc
|
||||||
sort.Slice(list, func(i, j int) bool {
|
sort.Slice(list, func(i, j int) bool {
|
||||||
@@ -110,16 +164,58 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
|||||||
cpuVal = cpuPercents[0]
|
cpuVal = cpuPercents[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// System-wide disk I/O rates
|
||||||
|
var diskReadBps, diskWriteBps uint64
|
||||||
|
var newDiskIO [2]uint64
|
||||||
|
if diskCounters, err := psdisk.IOCounters(); err == nil {
|
||||||
|
for _, d := range diskCounters {
|
||||||
|
newDiskIO[0] += d.ReadBytes
|
||||||
|
newDiskIO[1] += d.WriteBytes
|
||||||
|
}
|
||||||
|
if elapsed > 0 && (prevDiskIO[0] > 0 || prevDiskIO[1] > 0) {
|
||||||
|
if newDiskIO[0] >= prevDiskIO[0] {
|
||||||
|
diskReadBps = uint64(float64(newDiskIO[0]-prevDiskIO[0]) / elapsed)
|
||||||
|
}
|
||||||
|
if newDiskIO[1] >= prevDiskIO[1] {
|
||||||
|
diskWriteBps = uint64(float64(newDiskIO[1]-prevDiskIO[1]) / elapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System-wide network I/O rates
|
||||||
|
var netRecvBps, netSendBps uint64
|
||||||
|
var newNetIO [2]uint64
|
||||||
|
if netCounters, err := psnet.IOCounters(false); err == nil && len(netCounters) > 0 {
|
||||||
|
newNetIO[0] = netCounters[0].BytesRecv
|
||||||
|
newNetIO[1] = netCounters[0].BytesSent
|
||||||
|
if elapsed > 0 && (prevNetIO[0] > 0 || prevNetIO[1] > 0) {
|
||||||
|
if newNetIO[0] >= prevNetIO[0] {
|
||||||
|
netRecvBps = uint64(float64(newNetIO[0]-prevNetIO[0]) / elapsed)
|
||||||
|
}
|
||||||
|
if newNetIO[1] >= prevNetIO[1] {
|
||||||
|
netSendBps = uint64(float64(newNetIO[1]-prevNetIO[1]) / elapsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state under lock
|
||||||
|
s.mu.Lock()
|
||||||
|
s.lastTime = now
|
||||||
|
s.lastProcIO = newProcIO
|
||||||
|
s.lastDiskIO = newDiskIO
|
||||||
|
s.lastNetIO = newNetIO
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
return &Metrics{
|
return &Metrics{
|
||||||
CPUPercent: cpuVal,
|
CPUPercent: cpuVal,
|
||||||
TotalMem: vm.Total,
|
TotalMem: vm.Total,
|
||||||
FreeMem: vm.Available,
|
FreeMem: vm.Available,
|
||||||
Processes: list,
|
Processes: list,
|
||||||
Timestamp: time.Now().UnixMilli(),
|
Timestamp: time.Now().UnixMilli(),
|
||||||
GPUName: "",
|
DiskReadBps: diskReadBps,
|
||||||
GPUTotal: 0,
|
DiskWriteBps: diskWriteBps,
|
||||||
GPUUsed: 0,
|
NetRecvBps: netRecvBps,
|
||||||
GPUUtil: 0,
|
NetSendBps: netSendBps,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +294,26 @@ func queryGPU(ctx context.Context) (string, uint64, uint64, float64) {
|
|||||||
|
|
||||||
// GetMetrics is an exported method callable from frontend.
|
// GetMetrics is an exported method callable from frontend.
|
||||||
func (s *SysInfo) GetMetrics() (*Metrics, error) {
|
func (s *SysInfo) GetMetrics() (*Metrics, error) {
|
||||||
return s.CollectOnce()
|
return s.Snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot collects a complete metrics snapshot including GPU data.
|
||||||
|
func (s *SysInfo) Snapshot() (*Metrics, error) {
|
||||||
|
m, err := s.CollectOnce()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name, tot, used, util := queryGPU(s.ctx)
|
||||||
|
if name != "" {
|
||||||
|
m.GPUName = name
|
||||||
|
m.GPUTotal = tot
|
||||||
|
m.GPUUsed = used
|
||||||
|
m.GPUUtil = util
|
||||||
|
m.GPUProcesses = queryGPUProcesses(s.ctx)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessDetail contains all available details about a single process.
|
// ProcessDetail contains all available details about a single process.
|
||||||
type ProcessDetail struct {
|
type ProcessDetail struct {
|
||||||
PID int32 `json:"pid"`
|
PID int32 `json:"pid"`
|
||||||
@@ -317,7 +431,7 @@ func KillProcess(pid int32, force bool) error {
|
|||||||
// Unlike NewSysInfo, it does not start a background emitter and does not
|
// Unlike NewSysInfo, it does not start a background emitter and does not
|
||||||
// require a Wails runtime context — safe to call from server mode.
|
// require a Wails runtime context — safe to call from server mode.
|
||||||
func CollectFull(ctx context.Context) (*Metrics, error) {
|
func CollectFull(ctx context.Context) (*Metrics, error) {
|
||||||
s := &SysInfo{ctx: ctx}
|
s := &SysInfo{ctx: ctx, lastProcIO: make(map[int32][2]uint64)}
|
||||||
m, err := s.CollectOnce()
|
m, err := s.CollectOnce()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from 'react'
|
import React, { useEffect, useState, useCallback, useRef } from 'react'
|
||||||
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime'
|
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime'
|
||||||
import { Activity, Cpu, Database, Monitor, ChevronDown, ChevronRight, Layers, X, RefreshCw, OctagonX, Search } from 'lucide-react'
|
import { Activity, Cpu, Database, Monitor, ChevronDown, ChevronRight, X, RefreshCw, OctagonX, Search, HardDrive, Network } from 'lucide-react'
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -14,6 +14,8 @@ type ProcessInfo = {
|
|||||||
name: string
|
name: string
|
||||||
cpu: number
|
cpu: number
|
||||||
mem: number
|
mem: number
|
||||||
|
read_bps: number
|
||||||
|
write_bps: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type GPUProcessInfo = {
|
type GPUProcessInfo = {
|
||||||
@@ -35,6 +37,10 @@ type Metrics = {
|
|||||||
gpu_used_mem?: number
|
gpu_used_mem?: number
|
||||||
gpu_util_percent?: number
|
gpu_util_percent?: number
|
||||||
gpu_processes?: GPUProcessInfo[]
|
gpu_processes?: GPUProcessInfo[]
|
||||||
|
disk_read_bps: number
|
||||||
|
disk_write_bps: number
|
||||||
|
net_recv_bps: number
|
||||||
|
net_send_bps: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProcessDetail = {
|
type ProcessDetail = {
|
||||||
@@ -64,7 +70,7 @@ type ProcessDetail = {
|
|||||||
conn_count: number
|
conn_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortField = 'cpu' | 'mem'
|
type SortField = 'cpu' | 'mem' | 'disk' | 'vram'
|
||||||
type NavItem = { pid: number; name: string }
|
type NavItem = { pid: number; name: string }
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -520,10 +526,7 @@ export default function App() {
|
|||||||
const [sortBy, setSortBy] = useState<SortField>('cpu')
|
const [sortBy, setSortBy] = useState<SortField>('cpu')
|
||||||
const [selectedPid, setSelectedPid] = useState<number | null>(null)
|
const [selectedPid, setSelectedPid] = useState<number | null>(null)
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const [gpuPage, setGpuPage] = useState(0)
|
|
||||||
const [procOpen, setProcOpen] = useState(true)
|
const [procOpen, setProcOpen] = useState(true)
|
||||||
const [gpuOpen, setGpuOpen] = useState(true)
|
|
||||||
const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram')
|
|
||||||
const [navStack, setNavStack] = useState<NavItem[]>([])
|
const [navStack, setNavStack] = useState<NavItem[]>([])
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
@@ -533,7 +536,6 @@ export default function App() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [gpuPageSize, setGpuPageSize] = useState(4)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (m: Metrics) => setMetrics(m)
|
const handler = (m: Metrics) => setMetrics(m)
|
||||||
@@ -556,17 +558,18 @@ export default function App() {
|
|||||||
const gpuUtilPct = metrics?.gpu_util_percent ?? 0
|
const gpuUtilPct = metrics?.gpu_util_percent ?? 0
|
||||||
|
|
||||||
const searchLower = search.toLowerCase()
|
const searchLower = search.toLowerCase()
|
||||||
const gpuProcesses = [...(metrics?.gpu_processes ?? [])]
|
const gpuByPid = new Map<number, GPUProcessInfo>(
|
||||||
.filter(p => !search || p.name.toLowerCase().includes(searchLower) || String(p.pid).includes(search))
|
(metrics?.gpu_processes ?? []).map(g => [g.pid, g])
|
||||||
.sort((a, b) => gpuSortBy === 'vram' ? b.vram_mb - a.vram_mb : b.ram - a.ram)
|
)
|
||||||
const gpuTotalPages = Math.ceil(gpuProcesses.length / gpuPageSize)
|
|
||||||
const safeGpuPage = Math.min(gpuPage, Math.max(0, gpuTotalPages - 1))
|
|
||||||
const pagedGpuProcesses = gpuProcesses.slice(safeGpuPage * gpuPageSize, (safeGpuPage + 1) * gpuPageSize)
|
|
||||||
|
|
||||||
const sortedProcesses = metrics
|
const sortedProcesses = metrics
|
||||||
? [...metrics.processes]
|
? [...metrics.processes]
|
||||||
.filter(p => !search || p.name.toLowerCase().includes(searchLower) || String(p.pid).includes(search))
|
.filter(p => !search || p.name.toLowerCase().includes(searchLower) || String(p.pid).includes(search))
|
||||||
.sort((a, b) => b[sortBy] - a[sortBy])
|
.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
|
||||||
|
})
|
||||||
: []
|
: []
|
||||||
const totalPages = Math.ceil(sortedProcesses.length / pageSize)
|
const totalPages = Math.ceil(sortedProcesses.length / pageSize)
|
||||||
const safePage = Math.min(page, Math.max(0, totalPages - 1))
|
const safePage = Math.min(page, Math.max(0, totalPages - 1))
|
||||||
@@ -601,12 +604,12 @@ export default function App() {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name or PID…"
|
placeholder="Search by name or PID…"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => { setSearch(e.target.value); setPage(0); setGpuPage(0) }}
|
onChange={e => { setSearch(e.target.value); setPage(0) }}
|
||||||
className="w-full bg-zinc-900/60 border border-zinc-800 rounded-lg pl-8 pr-8 py-2 text-sm text-zinc-300 placeholder-zinc-700 focus:outline-none focus:border-zinc-600 transition-colors"
|
className="w-full bg-zinc-900/60 border border-zinc-800 rounded-lg pl-8 pr-8 py-2 text-sm text-zinc-300 placeholder-zinc-700 focus:outline-none focus:border-zinc-600 transition-colors"
|
||||||
/>
|
/>
|
||||||
{search && (
|
{search && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setSearch(''); setPage(0); setGpuPage(0) }}
|
onClick={() => { setSearch(''); setPage(0) }}
|
||||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
||||||
>
|
>
|
||||||
<X className="w-3.5 h-3.5" />
|
<X className="w-3.5 h-3.5" />
|
||||||
@@ -640,12 +643,24 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-[10px] text-zinc-600 mb-0.5">Memory</p>
|
<p className="text-[10px] text-zinc-600 mb-0.5">Memory</p>
|
||||||
<span className={cn('text-sm font-bold tabular-nums', memPct >= 90 ? 'text-red-400' : memPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{fmtBytes(usedMem)}</span>
|
<span className={cn('text-sm font-bold tabular-nums', memPct >= 90 ? 'text-red-400' : memPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{memPct.toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
{metrics?.gpu_name && (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-[10px] text-zinc-600 mb-0.5">VRAM</p>
|
||||||
|
<span className={cn('text-sm font-bold tabular-nums', gpuUsedPct >= 90 ? 'text-red-400' : gpuUsedPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{metrics.gpu_used_mem ? fmtBytes(metrics.gpu_used_mem) : '—'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{metrics?.gpu_name && (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-[10px] text-zinc-600 mb-0.5">GPU</p>
|
||||||
|
<span className={cn('text-sm font-bold tabular-nums', gpuUtilPct >= 90 ? 'text-red-400' : gpuUtilPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{gpuUtilPct.toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{procOpen && (
|
{procOpen && (
|
||||||
<div className="flex gap-1 ml-2">
|
<div className="flex gap-1 ml-2">
|
||||||
{(['cpu', 'mem'] as SortField[]).map((field) => (
|
{(['cpu', 'mem', 'disk', ...(metrics?.gpu_name ? ['vram'] : [])] as SortField[]).map((field) => (
|
||||||
<button
|
<button
|
||||||
key={field}
|
key={field}
|
||||||
onClick={() => toggleSort(field)}
|
onClick={() => toggleSort(field)}
|
||||||
@@ -683,6 +698,38 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<Progress value={memPct} />
|
<Progress value={memPct} />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
||||||
|
<span className="flex items-center gap-1"><HardDrive className="w-3 h-3" /> Disk</span>
|
||||||
|
<span className="font-mono text-zinc-400">↓{fmtBytes(metrics?.disk_read_bps ?? 0)}/s ↑{fmtBytes(metrics?.disk_write_bps ?? 0)}/s</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={Math.min(100, ((metrics?.disk_read_bps ?? 0) + (metrics?.disk_write_bps ?? 0)) / (500 * 1024 * 1024) * 100)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
||||||
|
<span className="flex items-center gap-1"><Network className="w-3 h-3" /> Network</span>
|
||||||
|
<span className="font-mono text-zinc-400">↓{fmtBytes(metrics?.net_recv_bps ?? 0)}/s ↑{fmtBytes(metrics?.net_send_bps ?? 0)}/s</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={Math.min(100, ((metrics?.net_recv_bps ?? 0) + (metrics?.net_send_bps ?? 0)) / (125 * 1024 * 1024) * 100)} />
|
||||||
|
</div>
|
||||||
|
{metrics?.gpu_name && (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
||||||
|
<span className="flex items-center gap-1"><Monitor className="w-3 h-3" /> VRAM <span className="text-zinc-700 ml-1 normal-case tracking-normal font-normal">{metrics.gpu_name}</span></span>
|
||||||
|
<span className={cn('font-mono', gpuUsedPct >= 90 ? 'text-red-400' : gpuUsedPct >= 70 ? 'text-amber-400' : 'text-zinc-400')}>{metrics.gpu_used_mem ? fmtBytes(metrics.gpu_used_mem) : '—'} / {metrics.gpu_total_mem ? fmtBytes(metrics.gpu_total_mem) : '—'}</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={gpuUsedPct} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{metrics?.gpu_name && (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
||||||
|
<span className="flex items-center gap-1"><Monitor className="w-3 h-3" /> GPU Util</span>
|
||||||
|
<span className={cn('font-mono', gpuUtilPct >= 90 ? 'text-red-400' : gpuUtilPct >= 70 ? 'text-amber-400' : 'text-zinc-400')}>{gpuUtilPct.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={gpuUtilPct} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -691,6 +738,8 @@ export default function App() {
|
|||||||
<th className="py-2 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider cursor-help" title="Executable name of the process">Name</th>
|
<th className="py-2 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider cursor-help" title="Executable name of the process">Name</th>
|
||||||
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title="CPU usage as a % of a single core (can exceed 100% on multi-threaded processes)">CPU</th>
|
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title="CPU usage as a % of a single core (can exceed 100% on multi-threaded processes)">CPU</th>
|
||||||
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title="Physical RAM (RSS) currently used by this process">Memory</th>
|
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title="Physical RAM (RSS) currently used by this process">Memory</th>
|
||||||
|
<th className="py-2 px-3 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-24 cursor-help" title="Disk I/O — combined read + write throughput for this process">Disk</th>
|
||||||
|
{metrics?.gpu_name && <th className="py-2 px-3 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-24 cursor-help" title="GPU type and VRAM — C = Compute, G = Graphics, C+G = both">GPU</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -707,26 +756,61 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
<td className="py-2 px-5 text-zinc-700 text-xs font-mono">{p.pid}</td>
|
<td className="py-2 px-5 text-zinc-700 text-xs font-mono">{p.pid}</td>
|
||||||
<td className="py-2 text-zinc-300 truncate max-w-xs">{p.name}</td>
|
<td className="py-2 text-zinc-300 truncate max-w-xs">{p.name}</td>
|
||||||
<td className="py-2 px-5 text-right">
|
<td className="py-2 px-3 text-right">
|
||||||
<span
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
className={cn(
|
<span className={cn('tabular-nums font-mono text-xs', p.cpu >= 50 ? 'text-red-400' : p.cpu >= 20 ? 'text-amber-400' : 'text-zinc-500')}>
|
||||||
'tabular-nums font-mono text-xs',
|
{p.cpu.toFixed(1)}%
|
||||||
p.cpu >= 50 ? 'text-red-400' : p.cpu >= 20 ? 'text-amber-400' : 'text-zinc-500'
|
</span>
|
||||||
)}
|
<div className="w-14 h-1 bg-zinc-800 rounded-full overflow-hidden">
|
||||||
>
|
<div className="h-full rounded-full" style={{ width: `${Math.min(100, p.cpu)}%`, backgroundColor: p.cpu >= 50 ? 'rgb(248 113 113/0.7)' : p.cpu >= 20 ? 'rgb(251 191 36/0.7)' : 'rgb(113 113 122/0.4)' }} />
|
||||||
{p.cpu.toFixed(1)}%
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-5 text-right">
|
<td className="py-2 px-3 text-right">
|
||||||
<span className="tabular-nums font-mono text-xs text-zinc-500">
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
{fmtBytes(p.mem)}
|
<span className="tabular-nums font-mono text-xs text-zinc-500">
|
||||||
</span>
|
{metrics ? ((p.mem / metrics.total_mem) * 100).toFixed(1) : '0'}%
|
||||||
|
</span>
|
||||||
|
<div className="w-14 h-1 bg-zinc-800 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-sky-700/50 rounded-full" style={{ width: `${metrics ? Math.min(100, (p.mem / metrics.total_mem) * 100) : 0}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="py-2 px-3 text-right">
|
||||||
|
{(p.read_bps + p.write_bps) > 0 ? (
|
||||||
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
|
<span className="tabular-nums font-mono text-[10px] text-zinc-500">{fmtBytes(p.read_bps + p.write_bps)}/s</span>
|
||||||
|
<div className="w-14 h-1 bg-zinc-800 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-sky-600/40 rounded-full" style={{ width: `${Math.min(100, (p.read_bps + p.write_bps) / (100 * 1024 * 1024) * 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="font-mono text-xs text-zinc-800">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
{metrics?.gpu_name && (gpuByPid.has(p.pid) ? (
|
||||||
|
<td className="py-2 px-3 text-right">
|
||||||
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
|
<span
|
||||||
|
title={gpuByPid.get(p.pid)!.type === 'C' ? 'Compute (CUDA/OpenCL)' : gpuByPid.get(p.pid)!.type === 'G' ? 'Graphics (rendering)' : 'Compute + Graphics'}
|
||||||
|
className={cn('text-[10px] font-mono px-1 py-0.5 rounded cursor-help',
|
||||||
|
gpuByPid.get(p.pid)!.type === 'C' ? 'bg-violet-900/50 text-violet-300' :
|
||||||
|
gpuByPid.get(p.pid)!.type === 'G' ? 'bg-blue-900/50 text-blue-300' :
|
||||||
|
'bg-indigo-900/50 text-indigo-300'
|
||||||
|
)}>
|
||||||
|
{gpuByPid.get(p.pid)!.type}
|
||||||
|
</span>
|
||||||
|
<span className="tabular-nums font-mono text-[10px] text-zinc-600">{gpuByPid.get(p.pid)!.vram_mb} MB</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
<td className="py-2 px-3 text-right text-zinc-800 text-xs">—</td>
|
||||||
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{pagedProcesses.length === 0 && (
|
{pagedProcesses.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className="py-10 text-center text-zinc-700 text-sm">
|
<td colSpan={metrics?.gpu_name ? 6 : 5} className="py-10 text-center text-zinc-700 text-sm">
|
||||||
Waiting for metrics…
|
Waiting for metrics…
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -802,185 +886,6 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* GPU */}
|
|
||||||
{metrics?.gpu_name && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
className="cursor-pointer select-none"
|
|
||||||
onClick={() => setGpuOpen(o => !o)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{gpuOpen
|
|
||||||
? <ChevronDown className="w-3.5 h-3.5 text-zinc-600" />
|
|
||||||
: <ChevronRight className="w-3.5 h-3.5 text-zinc-600" />
|
|
||||||
}
|
|
||||||
<Monitor className="w-3.5 h-3.5 text-zinc-500" />
|
|
||||||
<CardTitle>GPU</CardTitle>
|
|
||||||
<span className="text-xs text-zinc-600 font-normal normal-case tracking-normal ml-1">
|
|
||||||
{metrics.gpu_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4" onClick={e => e.stopPropagation()}>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-[10px] text-zinc-600 mb-0.5">VRAM</p>
|
|
||||||
<span className={cn('text-sm font-bold tabular-nums', gpuUsedPct >= 90 ? 'text-red-400' : gpuUsedPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{metrics.gpu_used_mem ? fmtBytes(metrics.gpu_used_mem) : '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-[10px] text-zinc-600 mb-0.5">Util</p>
|
|
||||||
<span className={cn('text-sm font-bold tabular-nums', gpuUtilPct >= 90 ? 'text-red-400' : gpuUtilPct >= 70 ? 'text-amber-400' : 'text-zinc-100')}>{gpuUtilPct.toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
{gpuOpen && (
|
|
||||||
<div className="flex gap-1 ml-2">
|
|
||||||
{(['vram', 'ram'] as const).map((field) => (
|
|
||||||
<button
|
|
||||||
key={field}
|
|
||||||
onClick={() => { setGpuSortBy(field); setGpuPage(0) }}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-0.5 px-2.5 py-1 rounded-md text-xs font-medium transition-colors',
|
|
||||||
gpuSortBy === field
|
|
||||||
? 'bg-zinc-800 text-zinc-100'
|
|
||||||
: 'text-zinc-600 hover:text-zinc-400 hover:bg-zinc-900'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.toUpperCase()}
|
|
||||||
{gpuSortBy === field && <ChevronDown className="w-3 h-3 ml-0.5" />}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
{gpuOpen && (
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
|
||||||
<span>VRAM usage</span>
|
|
||||||
<span>
|
|
||||||
{metrics.gpu_used_mem ? fmtBytes(metrics.gpu_used_mem) : '—'}
|
|
||||||
{' / '}
|
|
||||||
{metrics.gpu_total_mem ? fmtBytes(metrics.gpu_total_mem) : '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={gpuUsedPct} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
|
||||||
<span>GPU utilization</span>
|
|
||||||
<span>{gpuUtilPct.toFixed(1)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={gpuUtilPct} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{metrics.gpu_processes && metrics.gpu_processes.length > 0 && (
|
|
||||||
<div className="mt-4 pt-4 border-t border-zinc-800/60">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<Layers className="w-3 h-3 text-zinc-600" />
|
|
||||||
<span className="text-xs font-semibold uppercase tracking-widest text-zinc-500">
|
|
||||||
GPU Processes
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-zinc-700">{gpuProcesses.length} total</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider w-14 cursor-help" title="Process ID">PID</th>
|
|
||||||
<th className="pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider cursor-help" title="Process name using this GPU">Name</th>
|
|
||||||
<th className="pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider w-16 cursor-help" title="C = Compute (CUDA/OpenCL), G = Graphics (rendering), C+G = both">Type</th>
|
|
||||||
<th className="pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-24 cursor-help" title="GPU VRAM used by this process (video memory on the graphics card)">VRAM</th>
|
|
||||||
<th className="pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-28 cursor-help" title="System RAM (CPU-side memory) used by this process">RAM</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{pagedGpuProcesses.map((p) => (
|
|
||||||
<tr
|
|
||||||
key={p.pid}
|
|
||||||
className="border-t border-zinc-900 hover:bg-zinc-800/30 transition-colors cursor-pointer"
|
|
||||||
onClick={() => openDetail(p.pid)}
|
|
||||||
>
|
|
||||||
<td className="py-1.5 text-zinc-700 text-xs font-mono">{p.pid}</td>
|
|
||||||
<td className="py-1.5 text-zinc-300 text-xs truncate max-w-[180px]">{p.name}</td>
|
|
||||||
<td className="py-1.5">
|
|
||||||
<span
|
|
||||||
title={
|
|
||||||
p.type === 'C' ? 'C — Compute: uses the GPU for compute workloads (CUDA, OpenCL, Vulkan Compute…)' :
|
|
||||||
p.type === 'G' ? 'G — Graphics: uses the GPU for rendering and display output' :
|
|
||||||
'C+G — Compute + Graphics: uses the GPU for both compute and rendering workloads'
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
'inline-block px-1.5 py-0.5 rounded text-xs font-mono font-semibold cursor-help',
|
|
||||||
p.type === 'C' ? 'bg-violet-900/50 text-violet-300' :
|
|
||||||
p.type === 'G' ? 'bg-blue-900/50 text-blue-300' :
|
|
||||||
'bg-indigo-900/50 text-indigo-300'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{p.type}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-1.5 text-right text-xs font-mono text-zinc-400">{p.vram_mb} MB</td>
|
|
||||||
<td className="py-1.5 text-right text-xs font-mono text-zinc-500">{p.ram > 0 ? fmtBytes(p.ram) : '—'}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{/* GPU pagination */}
|
|
||||||
{gpuTotalPages > 0 && (
|
|
||||||
<div className="flex items-center justify-between pt-2 mt-1 border-t border-zinc-900">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-xs text-zinc-600">
|
|
||||||
{safeGpuPage * gpuPageSize + 1}–{Math.min((safeGpuPage + 1) * gpuPageSize, gpuProcesses.length)} of {gpuProcesses.length}
|
|
||||||
</span>
|
|
||||||
<div className="flex rounded border border-zinc-800 overflow-hidden" title="Items per page">
|
|
||||||
{[4, 10, 20, 50].map(n => (
|
|
||||||
<button key={n} onClick={() => { setGpuPageSize(n); setGpuPage(0) }}
|
|
||||||
className={cn('px-1.5 py-0.5 text-xs transition-colors', gpuPageSize === n ? 'bg-zinc-700 text-zinc-200' : 'bg-zinc-900 text-zinc-600 hover:text-zinc-400 hover:bg-zinc-800')}>
|
|
||||||
{n}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<button onClick={() => setGpuPage(0)} disabled={safeGpuPage === 0}
|
|
||||||
className="px-2 py-1 rounded text-xs text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">«</button>
|
|
||||||
<button onClick={() => setGpuPage(p => Math.max(0, p - 1))} disabled={safeGpuPage === 0}
|
|
||||||
className="px-2 py-1 rounded text-xs text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">‹</button>
|
|
||||||
{Array.from({ length: gpuTotalPages }, (_, i) => i)
|
|
||||||
.filter(i => i === 0 || i === gpuTotalPages - 1 || Math.abs(i - safeGpuPage) <= 1)
|
|
||||||
.reduce<(number | '…')[]>((acc, i, idx, arr) => {
|
|
||||||
if (idx > 0 && typeof arr[idx - 1] === 'number' && (i as number) - (arr[idx - 1] as number) > 1) acc.push('…')
|
|
||||||
acc.push(i)
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
.map((item, idx) =>
|
|
||||||
item === '…' ? (
|
|
||||||
<span key={`ge${idx}`} className="px-1 text-xs text-zinc-700">…</span>
|
|
||||||
) : (
|
|
||||||
<button key={item} onClick={() => setGpuPage(item as number)}
|
|
||||||
className={cn('w-7 h-7 rounded text-xs font-medium transition-colors',
|
|
||||||
safeGpuPage === item ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800'
|
|
||||||
)}>{(item as number) + 1}</button>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<button onClick={() => setGpuPage(p => Math.min(gpuTotalPages - 1, p + 1))} disabled={safeGpuPage >= gpuTotalPages - 1}
|
|
||||||
className="px-2 py-1 rounded text-xs text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">›</button>
|
|
||||||
<button onClick={() => setGpuPage(gpuTotalPages - 1)} disabled={safeGpuPage >= gpuTotalPages - 1}
|
|
||||||
className="px-2 py-1 rounded text-xs text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">»</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Detail panel */}
|
{/* Detail panel */}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export namespace backend {
|
|||||||
name: string;
|
name: string;
|
||||||
cpu: number;
|
cpu: number;
|
||||||
mem: number;
|
mem: number;
|
||||||
|
read_bps: number;
|
||||||
|
write_bps: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ProcessInfo(source);
|
return new ProcessInfo(source);
|
||||||
@@ -36,6 +38,8 @@ export namespace backend {
|
|||||||
this.name = source["name"];
|
this.name = source["name"];
|
||||||
this.cpu = source["cpu"];
|
this.cpu = source["cpu"];
|
||||||
this.mem = source["mem"];
|
this.mem = source["mem"];
|
||||||
|
this.read_bps = source["read_bps"];
|
||||||
|
this.write_bps = source["write_bps"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class Metrics {
|
export class Metrics {
|
||||||
@@ -49,6 +53,10 @@ export namespace backend {
|
|||||||
gpu_used_mem?: number;
|
gpu_used_mem?: number;
|
||||||
gpu_util_percent?: number;
|
gpu_util_percent?: number;
|
||||||
gpu_processes?: GPUProcessInfo[];
|
gpu_processes?: GPUProcessInfo[];
|
||||||
|
disk_read_bps: number;
|
||||||
|
disk_write_bps: number;
|
||||||
|
net_recv_bps: number;
|
||||||
|
net_send_bps: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Metrics(source);
|
return new Metrics(source);
|
||||||
@@ -66,6 +74,10 @@ export namespace backend {
|
|||||||
this.gpu_used_mem = source["gpu_used_mem"];
|
this.gpu_used_mem = source["gpu_used_mem"];
|
||||||
this.gpu_util_percent = source["gpu_util_percent"];
|
this.gpu_util_percent = source["gpu_util_percent"];
|
||||||
this.gpu_processes = this.convertValues(source["gpu_processes"], GPUProcessInfo);
|
this.gpu_processes = this.convertValues(source["gpu_processes"], GPUProcessInfo);
|
||||||
|
this.disk_read_bps = source["disk_read_bps"];
|
||||||
|
this.disk_write_bps = source["disk_write_bps"];
|
||||||
|
this.net_recv_bps = source["net_recv_bps"];
|
||||||
|
this.net_send_bps = source["net_send_bps"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func runServer(port int) {
|
func runServer(port int) {
|
||||||
_, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
streamSys := backend.NewSysInfoHeadless(ctx)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// GET /api/metrics — latest snapshot
|
// GET /api/metrics — latest snapshot
|
||||||
@@ -61,7 +63,7 @@ func runServer(port int) {
|
|||||||
case <-r.Context().Done():
|
case <-r.Context().Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
m, err := backend.CollectFull(r.Context())
|
m, err := streamSys.Snapshot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user