feat: add network connections and SM utilization to process metrics
This commit is contained in:
@@ -70,15 +70,17 @@ type ProcessInfo struct {
|
|||||||
Mem uint64 `json:"mem"`
|
Mem uint64 `json:"mem"`
|
||||||
ReadBps uint64 `json:"read_bps"`
|
ReadBps uint64 `json:"read_bps"`
|
||||||
WriteBps uint64 `json:"write_bps"`
|
WriteBps uint64 `json:"write_bps"`
|
||||||
|
NetConns int `json:"net_conns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GPUProcessInfo holds per-process GPU stats from nvidia-smi pmon.
|
// GPUProcessInfo holds per-process GPU stats from nvidia-smi pmon.
|
||||||
type GPUProcessInfo struct {
|
type GPUProcessInfo struct {
|
||||||
PID int32 `json:"pid"`
|
PID int32 `json:"pid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"` // "C", "G", or "C+G"
|
Type string `json:"type"` // "C", "G", or "C+G"
|
||||||
VRAM uint64 `json:"vram_mb"` // framebuffer memory in MB
|
VRAM uint64 `json:"vram_mb"` // framebuffer memory in MB
|
||||||
RAM uint64 `json:"ram"` // resident set size in bytes
|
RAM uint64 `json:"ram"` // resident set size in bytes
|
||||||
|
SMUtil uint32 `json:"sm_util"` // SM (CUDA core) utilization %
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
@@ -116,6 +118,16 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
|||||||
cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false)
|
cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false)
|
||||||
vm, _ := mem.VirtualMemory()
|
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
|
// Collect all processes with disk I/O rates
|
||||||
procs, _ := ps.Processes()
|
procs, _ := ps.Processes()
|
||||||
newProcIO := make(map[int32][2]uint64, len(procs))
|
newProcIO := make(map[int32][2]uint64, len(procs))
|
||||||
@@ -149,6 +161,7 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
|||||||
Mem: memBytes,
|
Mem: memBytes,
|
||||||
ReadBps: readBps,
|
ReadBps: readBps,
|
||||||
WriteBps: writeBps,
|
WriteBps: writeBps,
|
||||||
|
NetConns: netConnByPid[p.Pid],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Sort: primary by CPU desc, secondary by Mem desc
|
// 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.
|
// queryGPUProcesses lists all processes currently using the GPU via nvidia-smi pmon.
|
||||||
func queryGPUProcesses(ctx context.Context) []GPUProcessInfo {
|
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()
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -233,26 +248,26 @@ func queryGPUProcesses(ctx context.Context) []GPUProcessInfo {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
// expected: [gpuIdx, pid, type, fbMB, ccpmMB, name...]
|
// expected: [gpuIdx, pid, type, sm%, mem%, enc%, dec%, fbMB, ccpmMB, name...]
|
||||||
if len(fields) < 5 {
|
if len(fields) < 9 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pid64, err := strconv.ParseInt(fields[1], 10, 32)
|
pid64, err := strconv.ParseInt(fields[1], 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
vram, _ := strconv.ParseUint(fields[3], 10, 64)
|
sm64, _ := strconv.ParseUint(fields[3], 10, 32)
|
||||||
name := ""
|
vram, _ := strconv.ParseUint(fields[7], 10, 64)
|
||||||
if len(fields) >= 6 {
|
name := strings.Join(fields[9:], " ")
|
||||||
name = strings.Join(fields[5:], " ")
|
if name == "" {
|
||||||
} else {
|
|
||||||
name = fields[len(fields)-1]
|
name = fields[len(fields)-1]
|
||||||
}
|
}
|
||||||
gp := GPUProcessInfo{
|
gp := GPUProcessInfo{
|
||||||
PID: int32(pid64),
|
PID: int32(pid64),
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: fields[2],
|
Type: fields[2],
|
||||||
VRAM: vram,
|
VRAM: vram,
|
||||||
|
SMUtil: uint32(sm64),
|
||||||
}
|
}
|
||||||
// look up RAM (RSS) for this process
|
// look up RAM (RSS) for this process
|
||||||
if p, err := ps.NewProcess(int32(pid64)); err == nil {
|
if p, err := ps.NewProcess(int32(pid64)); err == nil {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type ProcessInfo = {
|
|||||||
mem: number
|
mem: number
|
||||||
read_bps: number
|
read_bps: number
|
||||||
write_bps: number
|
write_bps: number
|
||||||
|
net_conns: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type GPUProcessInfo = {
|
type GPUProcessInfo = {
|
||||||
@@ -24,6 +25,7 @@ type GPUProcessInfo = {
|
|||||||
type: string
|
type: string
|
||||||
vram_mb: number
|
vram_mb: number
|
||||||
ram: number
|
ram: number
|
||||||
|
sm_util: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metrics = {
|
type Metrics = {
|
||||||
@@ -70,7 +72,7 @@ type ProcessDetail = {
|
|||||||
conn_count: number
|
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 }
|
type NavItem = { pid: number; name: string }
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -244,7 +246,7 @@ function ProcessDetailPanel({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Panel */}
|
{/* Panel */}
|
||||||
<div className="fixed inset-y-0 right-0 w-[420px] z-50 bg-zinc-950 border-l border-zinc-800 shadow-2xl flex flex-col overflow-hidden">
|
<div className="fixed inset-y-0 right-0 w-full sm:w-[520px] lg:w-[600px] z-50 bg-zinc-950 border-l border-zinc-800 shadow-2xl flex flex-col overflow-hidden">
|
||||||
{/* Panel header */}
|
{/* Panel header */}
|
||||||
<div className="flex items-start justify-between px-5 py-4 border-b border-zinc-800/80 shrink-0">
|
<div className="flex items-start justify-between px-5 py-4 border-b border-zinc-800/80 shrink-0">
|
||||||
<div className="min-w-0 flex-1 pr-4">
|
<div className="min-w-0 flex-1 pr-4">
|
||||||
@@ -523,7 +525,8 @@ function ProcessDetailPanel({
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [metrics, setMetrics] = useState<Metrics | null>(null)
|
const [metrics, setMetrics] = useState<Metrics | null>(null)
|
||||||
const [sortBy, setSortBy] = useState<SortField>('cpu')
|
const [sortField, setSortField] = useState<SortField>('cpu')
|
||||||
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
|
||||||
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 [procOpen, setProcOpen] = useState(true)
|
const [procOpen, setProcOpen] = useState(true)
|
||||||
@@ -543,8 +546,12 @@ export default function App() {
|
|||||||
return () => EventsOff('metrics')
|
return () => EventsOff('metrics')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleSort = useCallback((field: SortField) => {
|
const handleSort = useCallback((field: SortField) => {
|
||||||
setSortBy(field)
|
setSortField(prev => {
|
||||||
|
if (prev === field) setSortDir(d => d === 'asc' ? 'desc' : 'asc')
|
||||||
|
else setSortDir('desc')
|
||||||
|
return field
|
||||||
|
})
|
||||||
setPage(0)
|
setPage(0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -565,10 +572,19 @@ export default function App() {
|
|||||||
? [...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) => {
|
.sort((a, b) => {
|
||||||
if (sortBy === 'disk') return (b.read_bps + b.write_bps) - (a.read_bps + a.write_bps)
|
let va: number | string = 0, vb: number | string = 0
|
||||||
if (sortBy === 'vram') return (gpuByPid.get(b.pid)?.vram_mb ?? 0) - (gpuByPid.get(a.pid)?.vram_mb ?? 0)
|
switch (sortField) {
|
||||||
if (sortBy === 'cpu') return b.cpu - a.cpu
|
case 'pid': va = a.pid; vb = b.pid; break
|
||||||
return b.mem - a.mem
|
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)
|
const totalPages = Math.ceil(sortedProcesses.length / pageSize)
|
||||||
@@ -596,7 +612,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="p-5 space-y-4 max-w-4xl mx-auto">
|
<main className="px-3 py-4 sm:p-5 space-y-4 w-full">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-600 pointer-events-none" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-600 pointer-events-none" />
|
||||||
@@ -658,32 +674,13 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{procOpen && (
|
|
||||||
<div className="flex gap-1 ml-2">
|
|
||||||
{(['cpu', 'mem', 'disk', ...(metrics?.gpu_name ? ['vram'] : [])] as SortField[]).map((field) => (
|
|
||||||
<button
|
|
||||||
key={field}
|
|
||||||
onClick={() => toggleSort(field)}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-0.5 px-2.5 py-1 rounded-md text-xs font-medium transition-colors',
|
|
||||||
sortBy === field
|
|
||||||
? 'bg-zinc-800 text-zinc-100'
|
|
||||||
: 'text-zinc-600 hover:text-zinc-400 hover:bg-zinc-900'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.toUpperCase()}
|
|
||||||
{sortBy === field && <ChevronDown className="w-3 h-3 ml-0.5" />}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{procOpen && (
|
{procOpen && (
|
||||||
<CardContent className="pt-1 px-0 pb-0">
|
<CardContent className="pt-1 px-0 pb-0">
|
||||||
{/* System resource bars */}
|
{/* System resource bars */}
|
||||||
<div className="grid grid-cols-2 gap-4 px-5 pb-4 pt-2 border-b border-zinc-800/60">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 px-4 pb-4 pt-2 border-b border-zinc-800/60">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
<div className="flex justify-between text-xs text-zinc-600 mb-1.5">
|
||||||
<span className="flex items-center gap-1"><Cpu className="w-3 h-3" /> CPU</span>
|
<span className="flex items-center gap-1"><Cpu className="w-3 h-3" /> CPU</span>
|
||||||
@@ -731,15 +728,45 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<table className="w-full text-sm">
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm min-w-[560px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-zinc-800/80">
|
<tr className="border-b border-zinc-800/80">
|
||||||
<th className="py-2 px-5 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider w-16 cursor-help" title="Process ID — unique identifier assigned by the OS">PID</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>
|
{ f: 'pid', label: 'PID', title: 'Process ID', align: 'left' },
|
||||||
<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>
|
{ f: 'name', label: 'Name', title: 'Executable name', align: 'left' },
|
||||||
<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>
|
{ f: 'cpu', label: 'CPU', title: 'CPU % of one core', align: 'right' },
|
||||||
<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>
|
{ f: 'mem', label: 'Mem', title: 'Memory % of total RAM', align: 'right' },
|
||||||
{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>}
|
{ 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 (
|
||||||
|
<th key={f} title={title}
|
||||||
|
onClick={sortable ? () => 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'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn('inline-flex items-center gap-0.5', align === 'right' && 'justify-end', align === 'center' && 'justify-center')}>
|
||||||
|
{label}
|
||||||
|
{sortable && (active
|
||||||
|
? <ChevronDown className={cn('w-3 h-3 shrink-0 transition-transform', sortDir === 'asc' && 'rotate-180')} />
|
||||||
|
: <ChevronDown className="w-3 h-3 shrink-0 opacity-20" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -754,8 +781,8 @@ export default function App() {
|
|||||||
: 'hover:bg-zinc-900/60'
|
: 'hover:bg-zinc-900/60'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td className="py-2 px-5 text-zinc-700 text-xs font-mono">{p.pid}</td>
|
<td className="py-2 px-3 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 px-3 text-zinc-300 truncate max-w-[160px]">{p.name}</td>
|
||||||
<td className="py-2 px-3 text-right">
|
<td className="py-2 px-3 text-right">
|
||||||
<div className="flex flex-col items-end gap-0.5">
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
<span className={cn('tabular-nums font-mono text-xs', p.cpu >= 50 ? 'text-red-400' : p.cpu >= 20 ? 'text-amber-400' : 'text-zinc-500')}>
|
<span className={cn('tabular-nums font-mono text-xs', p.cpu >= 50 ? 'text-red-400' : p.cpu >= 20 ? 'text-amber-400' : 'text-zinc-500')}>
|
||||||
@@ -788,35 +815,65 @@ export default function App() {
|
|||||||
<span className="font-mono text-xs text-zinc-800">—</span>
|
<span className="font-mono text-xs text-zinc-800">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
{metrics?.gpu_name && (gpuByPid.has(p.pid) ? (
|
<td className="py-2 px-3 text-right">
|
||||||
|
{p.net_conns > 0 ? (
|
||||||
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
|
<span className="tabular-nums font-mono text-[10px] text-zinc-500">{p.net_conns}</span>
|
||||||
|
<div className="w-12 h-1 bg-zinc-800 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-emerald-600/40 rounded-full" style={{ width: `${Math.min(100, p.net_conns / 50 * 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">
|
||||||
|
{gpuByPid.get(p.pid)!.sm_util > 0 ? (
|
||||||
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
|
<span className={cn('tabular-nums font-mono text-xs',
|
||||||
|
gpuByPid.get(p.pid)!.sm_util >= 50 ? 'text-violet-400' : 'text-violet-600'
|
||||||
|
)}>{gpuByPid.get(p.pid)!.sm_util}%</span>
|
||||||
|
<div className="w-12 h-1 bg-zinc-800 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-violet-500/40 rounded-full" style={{ width: `${Math.min(100, gpuByPid.get(p.pid)!.sm_util)}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : <span className="font-mono text-xs text-zinc-800">—</span>}
|
||||||
|
</td>
|
||||||
<td className="py-2 px-3 text-right">
|
<td className="py-2 px-3 text-right">
|
||||||
<div className="flex flex-col items-end gap-0.5">
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
<span
|
<span className="tabular-nums font-mono text-[10px] text-zinc-500">{gpuByPid.get(p.pid)!.vram_mb} MB</span>
|
||||||
title={gpuByPid.get(p.pid)!.type === 'C' ? 'Compute (CUDA/OpenCL)' : gpuByPid.get(p.pid)!.type === 'G' ? 'Graphics (rendering)' : 'Compute + Graphics'}
|
<div className="w-12 h-1 bg-zinc-800 rounded-full overflow-hidden">
|
||||||
className={cn('text-[10px] font-mono px-1 py-0.5 rounded cursor-help',
|
<div className="h-full bg-violet-600/40 rounded-full" style={{ width: `${metrics.gpu_total_mem ? Math.min(100, gpuByPid.get(p.pid)!.vram_mb * 1024 * 1024 / metrics.gpu_total_mem * 100) : 0}%` }} />
|
||||||
gpuByPid.get(p.pid)!.type === 'C' ? 'bg-violet-900/50 text-violet-300' :
|
</div>
|
||||||
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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
<td className="py-2 px-3 text-center">
|
||||||
|
<span
|
||||||
|
title={gpuByPid.get(p.pid)!.type === 'C' ? 'Compute (CUDA/OpenCL/Vulkan)' : gpuByPid.get(p.pid)!.type === 'G' ? 'Graphics (display/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>
|
||||||
|
</td>
|
||||||
|
</>) : (<>
|
||||||
<td className="py-2 px-3 text-right text-zinc-800 text-xs">—</td>
|
<td className="py-2 px-3 text-right text-zinc-800 text-xs">—</td>
|
||||||
))}
|
<td className="py-2 px-3 text-right text-zinc-800 text-xs">—</td>
|
||||||
|
<td className="py-2 px-3 text-center text-zinc-800 text-xs">—</td>
|
||||||
|
</>))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{pagedProcesses.length === 0 && (
|
{pagedProcesses.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={metrics?.gpu_name ? 6 : 5} className="py-10 text-center text-zinc-700 text-sm">
|
<td colSpan={metrics?.gpu_name ? 9 : 6} className="py-10 text-center text-zinc-700 text-sm">
|
||||||
Waiting for metrics…
|
Waiting for metrics…
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 0 && (
|
{totalPages > 0 && (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export namespace backend {
|
|||||||
type: string;
|
type: string;
|
||||||
vram_mb: number;
|
vram_mb: number;
|
||||||
ram: number;
|
ram: number;
|
||||||
|
sm_util: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new GPUProcessInfo(source);
|
return new GPUProcessInfo(source);
|
||||||
@@ -18,6 +19,7 @@ export namespace backend {
|
|||||||
this.type = source["type"];
|
this.type = source["type"];
|
||||||
this.vram_mb = source["vram_mb"];
|
this.vram_mb = source["vram_mb"];
|
||||||
this.ram = source["ram"];
|
this.ram = source["ram"];
|
||||||
|
this.sm_util = source["sm_util"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class ProcessInfo {
|
export class ProcessInfo {
|
||||||
@@ -27,6 +29,7 @@ export namespace backend {
|
|||||||
mem: number;
|
mem: number;
|
||||||
read_bps: number;
|
read_bps: number;
|
||||||
write_bps: number;
|
write_bps: number;
|
||||||
|
net_conns: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new ProcessInfo(source);
|
return new ProcessInfo(source);
|
||||||
@@ -40,6 +43,7 @@ export namespace backend {
|
|||||||
this.mem = source["mem"];
|
this.mem = source["mem"];
|
||||||
this.read_bps = source["read_bps"];
|
this.read_bps = source["read_bps"];
|
||||||
this.write_bps = source["write_bps"];
|
this.write_bps = source["write_bps"];
|
||||||
|
this.net_conns = source["net_conns"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class Metrics {
|
export class Metrics {
|
||||||
|
|||||||
Reference in New Issue
Block a user