@@ -339,6 +339,11 @@ var (
339339 cycleConnections []net.ConnectionStat
340340 cycleCacheMu sync.RWMutex
341341 cycleCacheTime time.Time
342+
343+ // Process CPU tracking for delta-based calculation (like htop does)
344+ prevProcCPUTimes map [int32 ]float64 // PID -> total CPU time (user + system)
345+ prevProcCPUTime time.Time
346+ prevProcCPUMu sync.RWMutex
342347)
343348
344349// OTelConfig holds configuration for OpenTelemetry exporter
@@ -1830,6 +1835,18 @@ func collectProcesses(limit int) ([]ProcessInfo, error) {
18301835 return nil , err
18311836 }
18321837
1838+ // Get timing info for CPU delta calculation
1839+ prevProcCPUMu .RLock ()
1840+ prevTimes := prevProcCPUTimes
1841+ prevTime := prevProcCPUTime
1842+ prevProcCPUMu .RUnlock ()
1843+
1844+ elapsed := time .Since (prevTime ).Seconds ()
1845+ numCPU := float64 (runtime .NumCPU ())
1846+
1847+ // Current CPU times map for next cycle
1848+ currentTimes := make (map [int32 ]float64 )
1849+
18331850 var processes []ProcessInfo
18341851
18351852 for _ , p := range procs {
@@ -1838,11 +1855,9 @@ func collectProcesses(limit int) ([]ProcessInfo, error) {
18381855 continue
18391856 }
18401857
1841- // Only use MemoryPercent for filtering (non-blocking)
1842- // Skip CPUPercent - it blocks for 100ms per process!
18431858 memPercent , _ := p .MemoryPercent ()
18441859
1845- // Filter by memory only (processes with < 0.1% memory are not interesting)
1860+ // Filter by memory (processes with < 0.1% memory are not interesting)
18461861 if memPercent < 0.1 {
18471862 continue
18481863 }
@@ -1854,6 +1869,26 @@ func collectProcesses(limit int) ([]ProcessInfo, error) {
18541869
18551870 pi .MemoryPercent = float64 (memPercent )
18561871
1872+ // Get CPU times for delta calculation (non-blocking, just reads /proc/[pid]/stat)
1873+ if times , err := p .Times (); err == nil && times != nil {
1874+ totalTime := times .User + times .System
1875+ currentTimes [p .Pid ] = totalTime
1876+
1877+ // Calculate CPU% from delta if we have previous data
1878+ if prevTimes != nil && elapsed > 0 {
1879+ if prevTotal , ok := prevTimes [p .Pid ]; ok {
1880+ // CPU% = (delta CPU time / elapsed time) * 100 / numCPU
1881+ deltaTime := totalTime - prevTotal
1882+ if deltaTime >= 0 {
1883+ pi .CPUPercent = (deltaTime / elapsed ) * 100.0 / numCPU
1884+ if pi .CPUPercent > 100 {
1885+ pi .CPUPercent = 100
1886+ }
1887+ }
1888+ }
1889+ }
1890+ }
1891+
18571892 // Minimal syscalls: only cmdline and memory info
18581893 if cmdline , err := p .Cmdline (); err == nil {
18591894 pi .Command = truncateString (cmdline , 200 )
@@ -1869,14 +1904,26 @@ func collectProcesses(limit int) ([]ProcessInfo, error) {
18691904 pi .Status = string (status [0 ])
18701905 }
18711906
1907+ // Legacy fields
1908+ pi .CPUUsage = pi .CPUPercent
18721909 pi .MemoryUsage = pi .MemoryPercent
18731910 pi .MemoryKB = int64 (pi .MemoryRSS / 1024 )
18741911
18751912 processes = append (processes , pi )
18761913 }
18771914
1878- // Sort by memory usage (since we don't have CPU anymore)
1915+ // Save current times for next cycle
1916+ prevProcCPUMu .Lock ()
1917+ prevProcCPUTimes = currentTimes
1918+ prevProcCPUTime = time .Now ()
1919+ prevProcCPUMu .Unlock ()
1920+
1921+ // Sort by CPU+Memory combined (prioritize CPU, then memory)
18791922 sort .Slice (processes , func (i , j int ) bool {
1923+ // Primary sort by CPU, secondary by memory
1924+ if processes [i ].CPUPercent != processes [j ].CPUPercent {
1925+ return processes [i ].CPUPercent > processes [j ].CPUPercent
1926+ }
18801927 return processes [i ].MemoryPercent > processes [j ].MemoryPercent
18811928 })
18821929
0 commit comments