mirror of https://github.com/jetkvm/kvm.git
refactor(audio): improve process monitoring with dynamic clock ticks
- Extract monitoring constants and configuration into centralized locations - Implement dynamic clock ticks detection for more accurate CPU metrics - Add warmup samples and bounds checking for CPU percentage calculation - Replace hardcoded values with constants for better maintainability
This commit is contained in:
parent
76174f4486
commit
88679cda2f
|
@ -22,7 +22,7 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||||
|
|
||||||
func (p *AudioBufferPool) Get() []byte {
|
func (p *AudioBufferPool) Get() []byte {
|
||||||
if buf := p.pool.Get(); buf != nil {
|
if buf := p.pool.Get(); buf != nil {
|
||||||
return buf.([]byte)
|
return *buf.(*[]byte)
|
||||||
}
|
}
|
||||||
return make([]byte, 0, p.bufferSize)
|
return make([]byte, 0, p.bufferSize)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ func (p *AudioBufferPool) Get() []byte {
|
||||||
func (p *AudioBufferPool) Put(buf []byte) {
|
func (p *AudioBufferPool) Put(buf []byte) {
|
||||||
if cap(buf) >= p.bufferSize {
|
if cap(buf) >= p.bufferSize {
|
||||||
resetBuf := buf[:0]
|
resetBuf := buf[:0]
|
||||||
p.pool.Put(resetBuf)
|
p.pool.Put(&resetBuf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package audio
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// MonitoringConfig contains configuration constants for audio monitoring
|
||||||
|
type MonitoringConfig struct {
|
||||||
|
// MetricsUpdateInterval defines how often metrics are collected and broadcast
|
||||||
|
MetricsUpdateInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultMonitoringConfig returns the default monitoring configuration
|
||||||
|
func DefaultMonitoringConfig() MonitoringConfig {
|
||||||
|
return MonitoringConfig{
|
||||||
|
MetricsUpdateInterval: 1000 * time.Millisecond, // 1 second interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global monitoring configuration instance
|
||||||
|
var monitoringConfig = DefaultMonitoringConfig()
|
||||||
|
|
||||||
|
// GetMetricsUpdateInterval returns the current metrics update interval
|
||||||
|
func GetMetricsUpdateInterval() time.Duration {
|
||||||
|
return monitoringConfig.MetricsUpdateInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMetricsUpdateInterval sets the metrics update interval
|
||||||
|
func SetMetricsUpdateInterval(interval time.Duration) {
|
||||||
|
monitoringConfig.MetricsUpdateInterval = interval
|
||||||
|
}
|
|
@ -370,8 +370,8 @@ func (aeb *AudioEventBroadcaster) sendCurrentMetrics(subscriber *AudioEventSubsc
|
||||||
|
|
||||||
// startMetricsBroadcasting starts a goroutine that periodically broadcasts metrics
|
// startMetricsBroadcasting starts a goroutine that periodically broadcasts metrics
|
||||||
func (aeb *AudioEventBroadcaster) startMetricsBroadcasting() {
|
func (aeb *AudioEventBroadcaster) startMetricsBroadcasting() {
|
||||||
// Use 1000ms interval to match process monitor frequency for synchronized metrics
|
// Use centralized interval to match process monitor frequency for synchronized metrics
|
||||||
ticker := time.NewTicker(1000 * time.Millisecond)
|
ticker := time.NewTicker(GetMetricsUpdateInterval())
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
|
|
|
@ -13,6 +13,28 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Constants for process monitoring
|
||||||
|
const (
|
||||||
|
// System constants
|
||||||
|
pageSize = 4096
|
||||||
|
maxCPUPercent = 100.0
|
||||||
|
minCPUPercent = 0.01
|
||||||
|
defaultClockTicks = 250.0 // Common for embedded ARM systems
|
||||||
|
defaultMemoryGB = 8
|
||||||
|
|
||||||
|
// Monitoring thresholds
|
||||||
|
maxWarmupSamples = 3
|
||||||
|
warmupCPUSamples = 2
|
||||||
|
logThrottleInterval = 10
|
||||||
|
|
||||||
|
// Channel buffer size
|
||||||
|
metricsChannelBuffer = 100
|
||||||
|
|
||||||
|
// Clock tick detection ranges
|
||||||
|
minValidClockTicks = 50
|
||||||
|
maxValidClockTicks = 1000
|
||||||
|
)
|
||||||
|
|
||||||
// ProcessMetrics represents CPU and memory usage metrics for a process
|
// ProcessMetrics represents CPU and memory usage metrics for a process
|
||||||
type ProcessMetrics struct {
|
type ProcessMetrics struct {
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
|
@ -34,15 +56,18 @@ type ProcessMonitor struct {
|
||||||
updateInterval time.Duration
|
updateInterval time.Duration
|
||||||
totalMemory int64
|
totalMemory int64
|
||||||
memoryOnce sync.Once
|
memoryOnce sync.Once
|
||||||
|
clockTicks float64
|
||||||
|
clockTicksOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// processState tracks the state needed for CPU calculation
|
// processState tracks the state needed for CPU calculation
|
||||||
type processState struct {
|
type processState struct {
|
||||||
name string
|
name string
|
||||||
lastCPUTime int64
|
lastCPUTime int64
|
||||||
lastSysTime int64
|
lastSysTime int64
|
||||||
lastUserTime int64
|
lastUserTime int64
|
||||||
lastSample time.Time
|
lastSample time.Time
|
||||||
|
warmupSamples int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessMonitor creates a new process monitor
|
// NewProcessMonitor creates a new process monitor
|
||||||
|
@ -51,8 +76,8 @@ func NewProcessMonitor() *ProcessMonitor {
|
||||||
logger: logging.GetDefaultLogger().With().Str("component", "process-monitor").Logger(),
|
logger: logging.GetDefaultLogger().With().Str("component", "process-monitor").Logger(),
|
||||||
monitoredPIDs: make(map[int]*processState),
|
monitoredPIDs: make(map[int]*processState),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
metricsChan: make(chan ProcessMetrics, 100),
|
metricsChan: make(chan ProcessMetrics, metricsChannelBuffer),
|
||||||
updateInterval: 1000 * time.Millisecond, // Update every 1000ms to sync with websocket broadcasts
|
updateInterval: GetMetricsUpdateInterval(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,26 +217,15 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
|
||||||
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
|
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
|
||||||
rss, _ := strconv.ParseInt(fields[23], 10, 64)
|
rss, _ := strconv.ParseInt(fields[23], 10, 64)
|
||||||
|
|
||||||
const pageSize = 4096
|
|
||||||
metric.MemoryRSS = rss * pageSize
|
metric.MemoryRSS = rss * pageSize
|
||||||
metric.MemoryVMS = vsize
|
metric.MemoryVMS = vsize
|
||||||
|
|
||||||
// Calculate CPU percentage
|
// Calculate CPU percentage
|
||||||
if !state.lastSample.IsZero() {
|
metric.CPUPercent = pm.calculateCPUPercent(totalCPUTime, state, now)
|
||||||
timeDelta := now.Sub(state.lastSample).Seconds()
|
|
||||||
cpuDelta := float64(totalCPUTime - state.lastCPUTime)
|
|
||||||
|
|
||||||
// Convert from clock ticks to seconds (assuming 100 Hz)
|
// Increment warmup counter
|
||||||
clockTicks := 100.0
|
if state.warmupSamples < maxWarmupSamples {
|
||||||
cpuSeconds := cpuDelta / clockTicks
|
state.warmupSamples++
|
||||||
|
|
||||||
if timeDelta > 0 {
|
|
||||||
metric.CPUPercent = (cpuSeconds / timeDelta) * 100.0
|
|
||||||
// Cap CPU percentage at 100% to handle multi-core usage
|
|
||||||
if metric.CPUPercent > 100.0 {
|
|
||||||
metric.CPUPercent = 100.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate memory percentage (RSS / total system memory)
|
// Calculate memory percentage (RSS / total system memory)
|
||||||
|
@ -228,11 +242,106 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
|
||||||
return metric, nil
|
return metric, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculateCPUPercent calculates CPU percentage for a process
|
||||||
|
func (pm *ProcessMonitor) calculateCPUPercent(totalCPUTime int64, state *processState, now time.Time) float64 {
|
||||||
|
if state.lastSample.IsZero() {
|
||||||
|
// First sample - initialize baseline
|
||||||
|
state.warmupSamples = 0
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
timeDelta := now.Sub(state.lastSample).Seconds()
|
||||||
|
cpuDelta := float64(totalCPUTime - state.lastCPUTime)
|
||||||
|
|
||||||
|
if timeDelta <= 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if cpuDelta > 0 {
|
||||||
|
// Convert from clock ticks to seconds using actual system clock ticks
|
||||||
|
clockTicks := pm.getClockTicks()
|
||||||
|
cpuSeconds := cpuDelta / clockTicks
|
||||||
|
cpuPercent := (cpuSeconds / timeDelta) * 100.0
|
||||||
|
|
||||||
|
// Apply bounds
|
||||||
|
if cpuPercent > maxCPUPercent {
|
||||||
|
cpuPercent = maxCPUPercent
|
||||||
|
}
|
||||||
|
if cpuPercent < minCPUPercent {
|
||||||
|
cpuPercent = minCPUPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
return cpuPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
// No CPU delta - process was idle
|
||||||
|
if state.warmupSamples < warmupCPUSamples {
|
||||||
|
// During warmup, provide a small non-zero value to indicate process is alive
|
||||||
|
return minCPUPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessMonitor) getClockTicks() float64 {
|
||||||
|
pm.clockTicksOnce.Do(func() {
|
||||||
|
// Try to detect actual clock ticks from kernel boot parameters or /proc/stat
|
||||||
|
if data, err := os.ReadFile("/proc/cmdline"); err == nil {
|
||||||
|
// Look for HZ parameter in kernel command line
|
||||||
|
cmdline := string(data)
|
||||||
|
if strings.Contains(cmdline, "HZ=") {
|
||||||
|
fields := strings.Fields(cmdline)
|
||||||
|
for _, field := range fields {
|
||||||
|
if strings.HasPrefix(field, "HZ=") {
|
||||||
|
if hz, err := strconv.ParseFloat(field[3:], 64); err == nil && hz > 0 {
|
||||||
|
pm.clockTicks = hz
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try reading from /proc/timer_list for more accurate detection
|
||||||
|
if data, err := os.ReadFile("/proc/timer_list"); err == nil {
|
||||||
|
timer := string(data)
|
||||||
|
// Look for tick device frequency
|
||||||
|
lines := strings.Split(timer, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "tick_period:") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 {
|
||||||
|
if period, err := strconv.ParseInt(fields[1], 10, 64); err == nil && period > 0 {
|
||||||
|
// Convert nanoseconds to Hz
|
||||||
|
hz := 1000000000.0 / float64(period)
|
||||||
|
if hz >= minValidClockTicks && hz <= maxValidClockTicks {
|
||||||
|
pm.clockTicks = hz
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Most embedded ARM systems (like jetKVM) use 250 Hz or 1000 Hz
|
||||||
|
// rather than the traditional 100 Hz
|
||||||
|
pm.clockTicks = defaultClockTicks
|
||||||
|
pm.logger.Warn().Float64("clock_ticks", pm.clockTicks).Msg("Using fallback clock ticks value")
|
||||||
|
|
||||||
|
// Log successful detection for non-fallback values
|
||||||
|
if pm.clockTicks != defaultClockTicks {
|
||||||
|
pm.logger.Info().Float64("clock_ticks", pm.clockTicks).Msg("Detected system clock ticks")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return pm.clockTicks
|
||||||
|
}
|
||||||
|
|
||||||
func (pm *ProcessMonitor) getTotalMemory() int64 {
|
func (pm *ProcessMonitor) getTotalMemory() int64 {
|
||||||
pm.memoryOnce.Do(func() {
|
pm.memoryOnce.Do(func() {
|
||||||
file, err := os.Open("/proc/meminfo")
|
file, err := os.Open("/proc/meminfo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pm.totalMemory = 8 * 1024 * 1024 * 1024 // Default 8GB
|
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
@ -251,7 +360,7 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pm.totalMemory = 8 * 1024 * 1024 * 1024 // Fallback
|
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024 // Fallback
|
||||||
})
|
})
|
||||||
return pm.totalMemory
|
return pm.totalMemory
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue