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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
psdisk "github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
psnet "github.com/shirou/gopsutil/v3/net"
|
||||
ps "github.com/shirou/gopsutil/v3/process"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
@@ -17,15 +20,35 @@ import (
|
||||
type SysInfo struct {
|
||||
ctx context.Context
|
||||
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 {
|
||||
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()
|
||||
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() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -34,27 +57,19 @@ func (s *SysInfo) startEmitter() {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
info, _ := s.CollectOnce()
|
||||
// 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)
|
||||
}
|
||||
info, _ := s.Snapshot()
|
||||
runtime.EventsEmit(s.ctx, "metrics", info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ProcessInfo struct {
|
||||
PID int32 `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Mem uint64 `json:"mem"`
|
||||
PID int32 `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Mem uint64 `json:"mem"`
|
||||
ReadBps uint64 `json:"read_bps"`
|
||||
WriteBps uint64 `json:"write_bps"`
|
||||
}
|
||||
|
||||
// GPUProcessInfo holds per-process GPU stats from nvidia-smi pmon.
|
||||
@@ -77,15 +92,33 @@ type Metrics struct {
|
||||
GPUUsed uint64 `json:"gpu_used_mem,omitempty"`
|
||||
GPUUtil float64 `json:"gpu_util_percent,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.
|
||||
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)
|
||||
vm, _ := mem.VirtualMemory()
|
||||
|
||||
// Collect ALL processes, then sort and keep top 50
|
||||
// Collect all processes with disk I/O rates
|
||||
procs, _ := ps.Processes()
|
||||
newProcIO := make(map[int32][2]uint64, len(procs))
|
||||
var list []ProcessInfo
|
||||
for _, p := range procs {
|
||||
name, _ := p.Name()
|
||||
@@ -95,7 +128,28 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
||||
if memInfo != nil {
|
||||
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.Slice(list, func(i, j int) bool {
|
||||
@@ -110,16 +164,58 @@ func (s *SysInfo) CollectOnce() (*Metrics, error) {
|
||||
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{
|
||||
CPUPercent: cpuVal,
|
||||
TotalMem: vm.Total,
|
||||
FreeMem: vm.Available,
|
||||
Processes: list,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
GPUName: "",
|
||||
GPUTotal: 0,
|
||||
GPUUsed: 0,
|
||||
GPUUtil: 0,
|
||||
CPUPercent: cpuVal,
|
||||
TotalMem: vm.Total,
|
||||
FreeMem: vm.Available,
|
||||
Processes: list,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
DiskReadBps: diskReadBps,
|
||||
DiskWriteBps: diskWriteBps,
|
||||
NetRecvBps: netRecvBps,
|
||||
NetSendBps: netSendBps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -198,8 +294,26 @@ func queryGPU(ctx context.Context) (string, uint64, uint64, float64) {
|
||||
|
||||
// GetMetrics is an exported method callable from frontend.
|
||||
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.
|
||||
type ProcessDetail struct {
|
||||
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
|
||||
// require a Wails runtime context — safe to call from server mode.
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user