mirror of
https://github.com/jetkvm/kvm.git
synced 2025-09-16 08:38:14 +00:00
This change replaces all instances of GetConfig() function calls with direct access to the Config variable throughout the audio package. The modification improves performance by eliminating function call overhead and simplifies the codebase by removing unnecessary indirection. The commit also includes minor optimizations in validation logic and connection handling, while maintaining all existing functionality. Error handling remains robust with appropriate fallbacks when config values are not available. Additional improvements include: - Enhanced connection health monitoring in UnifiedAudioClient - Optimized validation functions using cached config values - Reduced memory allocations in hot paths - Improved error recovery during quality changes
334 lines
10 KiB
Go
334 lines
10 KiB
Go
package audio
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// LatencyMonitor tracks and optimizes audio latency in real-time
|
|
type LatencyMonitor struct {
|
|
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
|
|
currentLatency int64 // Current latency in nanoseconds (atomic)
|
|
averageLatency int64 // Rolling average latency in nanoseconds (atomic)
|
|
minLatency int64 // Minimum observed latency in nanoseconds (atomic)
|
|
maxLatency int64 // Maximum observed latency in nanoseconds (atomic)
|
|
latencySamples int64 // Number of latency samples collected (atomic)
|
|
jitterAccumulator int64 // Accumulated jitter for variance calculation (atomic)
|
|
lastOptimization int64 // Timestamp of last optimization in nanoseconds (atomic)
|
|
|
|
config LatencyConfig
|
|
logger zerolog.Logger
|
|
|
|
// Control channels
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
|
|
// Optimization callbacks
|
|
optimizationCallbacks []OptimizationCallback
|
|
mutex sync.RWMutex
|
|
|
|
// Performance tracking
|
|
latencyHistory []LatencyMeasurement
|
|
historyMutex sync.RWMutex
|
|
}
|
|
|
|
// LatencyConfig holds configuration for latency monitoring
|
|
type LatencyConfig struct {
|
|
TargetLatency time.Duration // Target latency to maintain
|
|
MaxLatency time.Duration // Maximum acceptable latency
|
|
OptimizationInterval time.Duration // How often to run optimization
|
|
HistorySize int // Number of latency measurements to keep
|
|
JitterThreshold time.Duration // Jitter threshold for optimization
|
|
AdaptiveThreshold float64 // Threshold for adaptive adjustments (0.0-1.0)
|
|
}
|
|
|
|
// LatencyMeasurement represents a single latency measurement
|
|
type LatencyMeasurement struct {
|
|
Timestamp time.Time
|
|
Latency time.Duration
|
|
Jitter time.Duration
|
|
Source string // Source of the measurement (e.g., "input", "output", "processing")
|
|
}
|
|
|
|
// OptimizationCallback is called when latency optimization is triggered
|
|
type OptimizationCallback func(metrics LatencyMetrics) error
|
|
|
|
// LatencyMetrics provides comprehensive latency statistics
|
|
type LatencyMetrics struct {
|
|
Current time.Duration
|
|
Average time.Duration
|
|
Min time.Duration
|
|
Max time.Duration
|
|
Jitter time.Duration
|
|
SampleCount int64
|
|
Trend LatencyTrend
|
|
}
|
|
|
|
// LatencyTrend indicates the direction of latency changes
|
|
type LatencyTrend int
|
|
|
|
const (
|
|
LatencyTrendStable LatencyTrend = iota
|
|
LatencyTrendIncreasing
|
|
LatencyTrendDecreasing
|
|
LatencyTrendVolatile
|
|
)
|
|
|
|
// DefaultLatencyConfig returns a sensible default configuration
|
|
func DefaultLatencyConfig() LatencyConfig {
|
|
config := Config
|
|
return LatencyConfig{
|
|
TargetLatency: config.LatencyMonitorTarget,
|
|
MaxLatency: config.MaxLatencyThreshold,
|
|
OptimizationInterval: config.LatencyOptimizationInterval,
|
|
HistorySize: config.LatencyHistorySize,
|
|
JitterThreshold: config.JitterThreshold,
|
|
AdaptiveThreshold: config.LatencyAdaptiveThreshold,
|
|
}
|
|
}
|
|
|
|
// NewLatencyMonitor creates a new latency monitoring system
|
|
func NewLatencyMonitor(config LatencyConfig, logger zerolog.Logger) *LatencyMonitor {
|
|
// Validate latency configuration
|
|
if err := ValidateLatencyConfig(config); err != nil {
|
|
// Log validation error and use default configuration
|
|
logger.Error().Err(err).Msg("Invalid latency configuration provided, using defaults")
|
|
config = DefaultLatencyConfig()
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &LatencyMonitor{
|
|
config: config,
|
|
logger: logger.With().Str("component", "latency-monitor").Logger(),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
latencyHistory: make([]LatencyMeasurement, 0, config.HistorySize),
|
|
minLatency: int64(time.Hour), // Initialize to high value
|
|
}
|
|
}
|
|
|
|
// Start begins latency monitoring and optimization
|
|
func (lm *LatencyMonitor) Start() {
|
|
lm.wg.Add(1)
|
|
go lm.monitoringLoop()
|
|
}
|
|
|
|
// Stop stops the latency monitor
|
|
func (lm *LatencyMonitor) Stop() {
|
|
lm.cancel()
|
|
lm.wg.Wait()
|
|
}
|
|
|
|
// RecordLatency records a new latency measurement
|
|
func (lm *LatencyMonitor) RecordLatency(latency time.Duration, source string) {
|
|
now := time.Now()
|
|
latencyNanos := latency.Nanoseconds()
|
|
|
|
// Update atomic counters
|
|
atomic.StoreInt64(&lm.currentLatency, latencyNanos)
|
|
atomic.AddInt64(&lm.latencySamples, 1)
|
|
|
|
// Update min/max
|
|
for {
|
|
oldMin := atomic.LoadInt64(&lm.minLatency)
|
|
if latencyNanos >= oldMin || atomic.CompareAndSwapInt64(&lm.minLatency, oldMin, latencyNanos) {
|
|
break
|
|
}
|
|
}
|
|
|
|
for {
|
|
oldMax := atomic.LoadInt64(&lm.maxLatency)
|
|
if latencyNanos <= oldMax || atomic.CompareAndSwapInt64(&lm.maxLatency, oldMax, latencyNanos) {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Update rolling average using exponential moving average
|
|
oldAvg := atomic.LoadInt64(&lm.averageLatency)
|
|
newAvg := oldAvg + (latencyNanos-oldAvg)/10 // Alpha = 0.1
|
|
atomic.StoreInt64(&lm.averageLatency, newAvg)
|
|
|
|
// Calculate jitter (difference from average)
|
|
jitter := latencyNanos - newAvg
|
|
if jitter < 0 {
|
|
jitter = -jitter
|
|
}
|
|
atomic.AddInt64(&lm.jitterAccumulator, jitter)
|
|
|
|
// Store in history
|
|
lm.historyMutex.Lock()
|
|
measurement := LatencyMeasurement{
|
|
Timestamp: now,
|
|
Latency: latency,
|
|
Jitter: time.Duration(jitter),
|
|
Source: source,
|
|
}
|
|
|
|
if len(lm.latencyHistory) >= lm.config.HistorySize {
|
|
// Remove oldest measurement
|
|
copy(lm.latencyHistory, lm.latencyHistory[1:])
|
|
lm.latencyHistory[len(lm.latencyHistory)-1] = measurement
|
|
} else {
|
|
lm.latencyHistory = append(lm.latencyHistory, measurement)
|
|
}
|
|
lm.historyMutex.Unlock()
|
|
}
|
|
|
|
// GetMetrics returns current latency metrics
|
|
func (lm *LatencyMonitor) GetMetrics() LatencyMetrics {
|
|
current := atomic.LoadInt64(&lm.currentLatency)
|
|
average := atomic.LoadInt64(&lm.averageLatency)
|
|
min := atomic.LoadInt64(&lm.minLatency)
|
|
max := atomic.LoadInt64(&lm.maxLatency)
|
|
samples := atomic.LoadInt64(&lm.latencySamples)
|
|
jitterSum := atomic.LoadInt64(&lm.jitterAccumulator)
|
|
|
|
var jitter time.Duration
|
|
if samples > 0 {
|
|
jitter = time.Duration(jitterSum / samples)
|
|
}
|
|
|
|
return LatencyMetrics{
|
|
Current: time.Duration(current),
|
|
Average: time.Duration(average),
|
|
Min: time.Duration(min),
|
|
Max: time.Duration(max),
|
|
Jitter: jitter,
|
|
SampleCount: samples,
|
|
Trend: lm.calculateTrend(),
|
|
}
|
|
}
|
|
|
|
// AddOptimizationCallback adds a callback for latency optimization
|
|
func (lm *LatencyMonitor) AddOptimizationCallback(callback OptimizationCallback) {
|
|
lm.mutex.Lock()
|
|
lm.optimizationCallbacks = append(lm.optimizationCallbacks, callback)
|
|
lm.mutex.Unlock()
|
|
}
|
|
|
|
// monitoringLoop runs the main monitoring and optimization loop
|
|
func (lm *LatencyMonitor) monitoringLoop() {
|
|
defer lm.wg.Done()
|
|
|
|
ticker := time.NewTicker(lm.config.OptimizationInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-lm.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
lm.runOptimization()
|
|
}
|
|
}
|
|
}
|
|
|
|
// runOptimization checks if optimization is needed and triggers callbacks with threshold validation.
|
|
//
|
|
// Validation Rules:
|
|
// - Current latency must not exceed MaxLatency (default: 200ms)
|
|
// - Average latency checked against adaptive threshold: TargetLatency * (1 + AdaptiveThreshold)
|
|
// - Jitter must not exceed JitterThreshold (default: 20ms)
|
|
// - All latency values must be non-negative durations
|
|
//
|
|
// Optimization Triggers:
|
|
// - Current latency > MaxLatency: Immediate optimization needed
|
|
// - Average latency > adaptive threshold: Gradual optimization needed
|
|
// - Jitter > JitterThreshold: Stability optimization needed
|
|
//
|
|
// Threshold Calculations:
|
|
// - Adaptive threshold = TargetLatency * (1.0 + AdaptiveThreshold)
|
|
// - Default: 50ms * (1.0 + 0.8) = 90ms adaptive threshold
|
|
// - Provides buffer above target before triggering optimization
|
|
//
|
|
// The function ensures real-time audio performance by monitoring multiple
|
|
// latency metrics and triggering optimization callbacks when thresholds are exceeded.
|
|
func (lm *LatencyMonitor) runOptimization() {
|
|
metrics := lm.GetMetrics()
|
|
|
|
// Check if optimization is needed
|
|
needsOptimization := false
|
|
|
|
// Check if current latency exceeds threshold
|
|
if metrics.Current > lm.config.MaxLatency {
|
|
needsOptimization = true
|
|
lm.logger.Warn().Dur("current_latency", metrics.Current).Dur("max_latency", lm.config.MaxLatency).Msg("latency exceeds maximum threshold")
|
|
}
|
|
|
|
// Check if average latency is above adaptive threshold
|
|
adaptiveThreshold := time.Duration(float64(lm.config.TargetLatency.Nanoseconds()) * (1.0 + lm.config.AdaptiveThreshold))
|
|
if metrics.Average > adaptiveThreshold {
|
|
needsOptimization = true
|
|
}
|
|
|
|
// Check if jitter is too high
|
|
if metrics.Jitter > lm.config.JitterThreshold {
|
|
needsOptimization = true
|
|
}
|
|
|
|
if needsOptimization {
|
|
atomic.StoreInt64(&lm.lastOptimization, time.Now().UnixNano())
|
|
|
|
// Run optimization callbacks
|
|
lm.mutex.RLock()
|
|
callbacks := make([]OptimizationCallback, len(lm.optimizationCallbacks))
|
|
copy(callbacks, lm.optimizationCallbacks)
|
|
lm.mutex.RUnlock()
|
|
|
|
for _, callback := range callbacks {
|
|
if err := callback(metrics); err != nil {
|
|
lm.logger.Error().Err(err).Msg("optimization callback failed")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// calculateTrend analyzes recent latency measurements to determine trend
|
|
func (lm *LatencyMonitor) calculateTrend() LatencyTrend {
|
|
lm.historyMutex.RLock()
|
|
defer lm.historyMutex.RUnlock()
|
|
|
|
if len(lm.latencyHistory) < 10 {
|
|
return LatencyTrendStable
|
|
}
|
|
|
|
// Analyze last 10 measurements
|
|
recentMeasurements := lm.latencyHistory[len(lm.latencyHistory)-10:]
|
|
|
|
var increasing, decreasing int
|
|
for i := 1; i < len(recentMeasurements); i++ {
|
|
if recentMeasurements[i].Latency > recentMeasurements[i-1].Latency {
|
|
increasing++
|
|
} else if recentMeasurements[i].Latency < recentMeasurements[i-1].Latency {
|
|
decreasing++
|
|
}
|
|
}
|
|
|
|
// Determine trend based on direction changes
|
|
if increasing > 6 {
|
|
return LatencyTrendIncreasing
|
|
} else if decreasing > 6 {
|
|
return LatencyTrendDecreasing
|
|
} else if increasing+decreasing > 7 {
|
|
return LatencyTrendVolatile
|
|
}
|
|
|
|
return LatencyTrendStable
|
|
}
|
|
|
|
// GetLatencyHistory returns a copy of recent latency measurements
|
|
func (lm *LatencyMonitor) GetLatencyHistory() []LatencyMeasurement {
|
|
lm.historyMutex.RLock()
|
|
defer lm.historyMutex.RUnlock()
|
|
|
|
history := make([]LatencyMeasurement, len(lm.latencyHistory))
|
|
copy(history, lm.latencyHistory)
|
|
return history
|
|
}
|