feat: add Card and Progress components, utility functions, and improve styles
- Introduced Card component with CardHeader, CardTitle, and CardContent subcomponents for better UI structure. - Added Progress component to visually represent progress with customizable thresholds. - Created utility function `cn` for conditional class names using clsx and tailwind-merge. - Updated styles to integrate Tailwind CSS and improved base styles for better consistency. - Configured Tailwind CSS for dark mode and extended theme with custom fonts and animations. - Added TypeScript configuration for better type safety and module resolution. - Enhanced Vite configuration with path aliasing for cleaner imports. - Updated Wails backend definitions to include new process detail and GPU process info structures.
This commit is contained in:
@@ -3,6 +3,7 @@ package backend
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -34,15 +35,16 @@ func (s *SysInfo) startEmitter() {
|
||||
return
|
||||
case <-ticker.C:
|
||||
info, _ := s.CollectOnce()
|
||||
// try to attach GPU info if available
|
||||
// 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)
|
||||
}
|
||||
// emit via Wails runtime
|
||||
runtime.EventsEmit(s.ctx, "metrics", info)
|
||||
}
|
||||
}
|
||||
@@ -55,16 +57,26 @@ type ProcessInfo struct {
|
||||
Mem uint64 `json:"mem"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type Metrics struct {
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
TotalMem uint64 `json:"total_mem"`
|
||||
FreeMem uint64 `json:"free_mem"`
|
||||
Processes []ProcessInfo `json:"processes"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
GPUName string `json:"gpu_name,omitempty"`
|
||||
GPUTotal uint64 `json:"gpu_total_mem,omitempty"`
|
||||
GPUUsed uint64 `json:"gpu_used_mem,omitempty"`
|
||||
GPUUtil float64 `json:"gpu_util_percent,omitempty"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
TotalMem uint64 `json:"total_mem"`
|
||||
FreeMem uint64 `json:"free_mem"`
|
||||
Processes []ProcessInfo `json:"processes"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
GPUName string `json:"gpu_name,omitempty"`
|
||||
GPUTotal uint64 `json:"gpu_total_mem,omitempty"`
|
||||
GPUUsed uint64 `json:"gpu_used_mem,omitempty"`
|
||||
GPUUtil float64 `json:"gpu_util_percent,omitempty"`
|
||||
GPUProcesses []GPUProcessInfo `json:"gpu_processes,omitempty"`
|
||||
}
|
||||
|
||||
// CollectOnce gathers a snapshot of system metrics.
|
||||
@@ -72,13 +84,10 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
||||
cpuPercents, _ := cpu.PercentWithContext(s.ctx, 0, false)
|
||||
vm, _ := mem.VirtualMemory()
|
||||
|
||||
// collect top processes by CPU (sample few)
|
||||
// Collect ALL processes, then sort and keep top 50
|
||||
procs, _ := ps.Processes()
|
||||
var list []ProcessInfo
|
||||
for i, p := range procs {
|
||||
if i >= 30 {
|
||||
break
|
||||
}
|
||||
for _, p := range procs {
|
||||
name, _ := p.Name()
|
||||
cpuPct, _ := p.CPUPercent()
|
||||
memInfo, _ := p.MemoryInfo()
|
||||
@@ -88,6 +97,13 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
||||
}
|
||||
list = append(list, ProcessInfo{PID: p.Pid, Name: name, CPU: cpuPct, Mem: memBytes})
|
||||
}
|
||||
// Sort: primary by CPU desc, secondary by Mem desc
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].CPU != list[j].CPU {
|
||||
return list[i].CPU > list[j].CPU
|
||||
}
|
||||
return list[i].Mem > list[j].Mem
|
||||
})
|
||||
|
||||
var cpuVal float64
|
||||
if len(cpuPercents) > 0 {
|
||||
@@ -107,6 +123,52 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var result []GPUProcessInfo
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
// expected: [gpuIdx, pid, type, fbMB, ccpmMB, name...]
|
||||
if len(fields) < 5 {
|
||||
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 {
|
||||
name = fields[len(fields)-1]
|
||||
}
|
||||
gp := GPUProcessInfo{
|
||||
PID: int32(pid64),
|
||||
Name: name,
|
||||
Type: fields[2],
|
||||
VRAM: vram,
|
||||
}
|
||||
// look up RAM (RSS) for this process
|
||||
if p, err := ps.NewProcess(int32(pid64)); err == nil {
|
||||
if mi, err := p.MemoryInfoWithContext(ctx); err == nil && mi != nil {
|
||||
gp.RAM = mi.RSS
|
||||
}
|
||||
}
|
||||
result = append(result, gp)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// try to query GPU info via nvidia-smi; returns name, totalMB, usedMB, utilPercent
|
||||
func queryGPU(ctx context.Context) (string, uint64, uint64, float64) {
|
||||
// Use nvidia-smi if available
|
||||
@@ -138,3 +200,108 @@ func queryGPU(ctx context.Context) (string, uint64, uint64, float64) {
|
||||
func (s *SysInfo) GetMetrics() (*Metrics, error) {
|
||||
return s.CollectOnce()
|
||||
}
|
||||
// ProcessDetail contains all available details about a single process.
|
||||
type ProcessDetail struct {
|
||||
PID int32 `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
Exe string `json:"exe"`
|
||||
Cmdline string `json:"cmdline"`
|
||||
Cwd string `json:"cwd"`
|
||||
Status string `json:"status"`
|
||||
Username string `json:"username"`
|
||||
CreatedAt int64 `json:"created_at"` // unix ms
|
||||
ParentPID int32 `json:"parent_pid"`
|
||||
Nice int32 `json:"nice"`
|
||||
NumThreads int32 `json:"num_threads"`
|
||||
NumFDs int32 `json:"num_fds"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
RSS uint64 `json:"rss"`
|
||||
VMS uint64 `json:"vms"`
|
||||
Swap uint64 `json:"swap"`
|
||||
MemPercent float32 `json:"mem_percent"`
|
||||
ReadBytes uint64 `json:"read_bytes"`
|
||||
WriteBytes uint64 `json:"write_bytes"`
|
||||
ReadOps uint64 `json:"read_ops"`
|
||||
WriteOps uint64 `json:"write_ops"`
|
||||
OpenFilesCount int `json:"open_files_count"`
|
||||
ConnCount int `json:"conn_count"`
|
||||
}
|
||||
|
||||
// FetchProcessDetail collects all available details for a given PID.
|
||||
// Fields that can't be read (e.g. permission denied) are left at zero/empty.
|
||||
func FetchProcessDetail(pid int32) (*ProcessDetail, error) {
|
||||
p, err := ps.NewProcess(pid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &ProcessDetail{PID: pid}
|
||||
if v, e := p.Name(); e == nil {
|
||||
d.Name = v
|
||||
}
|
||||
if v, e := p.Exe(); e == nil {
|
||||
d.Exe = v
|
||||
}
|
||||
if v, e := p.Cmdline(); e == nil {
|
||||
d.Cmdline = v
|
||||
}
|
||||
if v, e := p.Cwd(); e == nil {
|
||||
d.Cwd = v
|
||||
}
|
||||
if vv, e := p.Status(); e == nil && len(vv) > 0 {
|
||||
d.Status = vv[0]
|
||||
}
|
||||
if v, e := p.Username(); e == nil {
|
||||
d.Username = v
|
||||
}
|
||||
if v, e := p.CreateTime(); e == nil {
|
||||
d.CreatedAt = v
|
||||
}
|
||||
if v, e := p.Ppid(); e == nil {
|
||||
d.ParentPID = v
|
||||
}
|
||||
if v, e := p.Nice(); e == nil {
|
||||
d.Nice = v
|
||||
}
|
||||
if v, e := p.NumThreads(); e == nil {
|
||||
d.NumThreads = v
|
||||
}
|
||||
if v, e := p.NumFDs(); e == nil {
|
||||
d.NumFDs = v
|
||||
}
|
||||
if v, e := p.CPUPercent(); e == nil {
|
||||
d.CPUPercent = v
|
||||
}
|
||||
if v, e := p.MemoryInfo(); e == nil && v != nil {
|
||||
d.RSS = v.RSS
|
||||
d.VMS = v.VMS
|
||||
d.Swap = v.Swap
|
||||
}
|
||||
if v, e := p.MemoryPercent(); e == nil {
|
||||
d.MemPercent = v
|
||||
}
|
||||
if v, e := p.IOCounters(); e == nil && v != nil {
|
||||
d.ReadBytes = v.ReadBytes
|
||||
d.WriteBytes = v.WriteBytes
|
||||
d.ReadOps = v.ReadCount
|
||||
d.WriteOps = v.WriteCount
|
||||
}
|
||||
if v, e := p.OpenFiles(); e == nil {
|
||||
d.OpenFilesCount = len(v)
|
||||
}
|
||||
if v, e := p.Connections(); e == nil {
|
||||
d.ConnCount = len(v)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// KillProcess sends SIGTERM to the process. Use force=true to send SIGKILL.
|
||||
func KillProcess(pid int32, force bool) error {
|
||||
p, err := ps.NewProcess(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if force {
|
||||
return p.Kill()
|
||||
}
|
||||
return p.Terminate()
|
||||
}
|
||||
Reference in New Issue
Block a user