From 9e343b3cc7ef748b6b7572b8c3dd2f89dd532a71 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 25 Aug 2025 19:02:29 +0000 Subject: [PATCH] refactor(audio): move hardcoded values to config for better flexibility - Replace hardcoded values with configurable parameters in audio components - Add new config fields for adaptive buffer sizes and frame pool settings - Implement memory guard in ZeroCopyFramePool to prevent excessive allocations --- internal/audio/adaptive_buffer.go | 6 ++--- internal/audio/config_constants.go | 27 +++++++++++++++++++ internal/audio/input_ipc.go | 14 +++++----- internal/audio/ipc.go | 10 ++++--- internal/audio/relay.go | 2 +- internal/audio/zero_copy.go | 43 +++++++++++++++++++++++++----- 6 files changed, 81 insertions(+), 21 deletions(-) diff --git a/internal/audio/adaptive_buffer.go b/internal/audio/adaptive_buffer.go index 64fbc06..f462e80 100644 --- a/internal/audio/adaptive_buffer.go +++ b/internal/audio/adaptive_buffer.go @@ -37,9 +37,9 @@ type AdaptiveBufferConfig struct { func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig { return AdaptiveBufferConfig{ // Conservative buffer sizes for 256MB RAM constraint - MinBufferSize: 3, // Minimum 3 frames (slightly higher for stability) - MaxBufferSize: 20, // Maximum 20 frames (increased for high load scenarios) - DefaultBufferSize: 6, // Default 6 frames (increased for better stability) + MinBufferSize: GetConfig().AdaptiveMinBufferSize, + MaxBufferSize: GetConfig().AdaptiveMaxBufferSize, + DefaultBufferSize: GetConfig().AdaptiveDefaultBufferSize, // CPU thresholds optimized for single-core ARM Cortex A7 under load LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU diff --git a/internal/audio/config_constants.go b/internal/audio/config_constants.go index b09e5aa..116aa42 100644 --- a/internal/audio/config_constants.go +++ b/internal/audio/config_constants.go @@ -698,6 +698,28 @@ type AudioConfigConstants struct { OutputSizeThreshold int TargetLevel float64 + // Adaptive Buffer Configuration - Controls dynamic buffer sizing for optimal performance + // Used in: adaptive_buffer.go for dynamic buffer management + // Impact: Controls buffer size adaptation based on system load and latency + + // AdaptiveMinBufferSize defines minimum buffer size in frames for adaptive buffering. + // Used in: adaptive_buffer.go DefaultAdaptiveBufferConfig() + // Impact: Lower values reduce latency but may cause underruns under high load. + // Default 3 frames provides stability while maintaining low latency. + AdaptiveMinBufferSize int + + // AdaptiveMaxBufferSize defines maximum buffer size in frames for adaptive buffering. + // Used in: adaptive_buffer.go DefaultAdaptiveBufferConfig() + // Impact: Higher values handle load spikes but increase maximum latency. + // Default 20 frames accommodates high load scenarios without excessive latency. + AdaptiveMaxBufferSize int + + // AdaptiveDefaultBufferSize defines default buffer size in frames for adaptive buffering. + // Used in: adaptive_buffer.go DefaultAdaptiveBufferConfig() + // Impact: Starting point for buffer adaptation, affects initial latency. + // Default 6 frames balances initial latency with adaptation headroom. + AdaptiveDefaultBufferSize int + // Priority Scheduling AudioHighPriority int AudioMediumPriority int @@ -1159,6 +1181,11 @@ func DefaultAudioConfig() *AudioConfigConstants { HighMemoryThreshold: 0.75, TargetLatency: 20 * time.Millisecond, + // Adaptive Buffer Size Configuration + AdaptiveMinBufferSize: 3, // Minimum 3 frames for stability + AdaptiveMaxBufferSize: 20, // Maximum 20 frames for high load + AdaptiveDefaultBufferSize: 6, // Default 6 frames for balanced performance + // Adaptive Optimizer Configuration CooldownPeriod: 30 * time.Second, RollbackThreshold: 300 * time.Millisecond, diff --git a/internal/audio/input_ipc.go b/internal/audio/input_ipc.go index 931700f..91401c4 100644 --- a/internal/audio/input_ipc.go +++ b/internal/audio/input_ipc.go @@ -19,11 +19,11 @@ import ( var ( inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input) inputSocketName = "audio_input.sock" - writeTimeout = 15 * time.Millisecond // Non-blocking write timeout (increased for high load) + writeTimeout = GetConfig().WriteTimeout // Non-blocking write timeout ) const ( - headerSize = 17 // Fixed header size: 4+1+4+8 bytes + headerSize = 17 // Fixed header size: 4+1+4+8 bytes - matches GetConfig().HeaderSize ) var ( @@ -503,10 +503,12 @@ func (aic *AudioInputClient) Connect() error { aic.running = true return nil } - // Exponential backoff starting at 50ms - delay := time.Duration(50*(1< 500*time.Millisecond { - delay = 500 * time.Millisecond + // Exponential backoff starting from config + backoffStart := GetConfig().BackoffStart + delay := time.Duration(backoffStart.Nanoseconds()*(1< maxDelay { + delay = maxDelay } time.Sleep(delay) } diff --git a/internal/audio/ipc.go b/internal/audio/ipc.go index eaa8484..6798893 100644 --- a/internal/audio/ipc.go +++ b/internal/audio/ipc.go @@ -422,10 +422,12 @@ func (c *AudioClient) Connect() error { c.running = true return nil } - // Exponential backoff starting at 50ms - delay := time.Duration(50*(1< 400*time.Millisecond { - delay = 400 * time.Millisecond + // Exponential backoff starting from config + backoffStart := GetConfig().BackoffStart + delay := time.Duration(backoffStart.Nanoseconds()*(1< maxDelay { + delay = maxDelay } time.Sleep(delay) } diff --git a/internal/audio/relay.go b/internal/audio/relay.go index 001faf4..5c59f6d 100644 --- a/internal/audio/relay.go +++ b/internal/audio/relay.go @@ -132,7 +132,7 @@ func (r *AudioRelay) relayLoop() { defer r.wg.Done() r.logger.Debug().Msg("Audio relay loop started") - const maxConsecutiveErrors = 10 + var maxConsecutiveErrors = GetConfig().MaxConsecutiveErrors consecutiveErrors := 0 for { diff --git a/internal/audio/zero_copy.go b/internal/audio/zero_copy.go index a3fd582..7b542f3 100644 --- a/internal/audio/zero_copy.go +++ b/internal/audio/zero_copy.go @@ -23,6 +23,7 @@ type ZeroCopyFramePool struct { counter int64 // Frame counter (atomic) hitCount int64 // Pool hit counter (atomic) missCount int64 // Pool miss counter (atomic) + allocationCount int64 // Total allocations counter (atomic) // Other fields pool sync.Pool @@ -36,13 +37,23 @@ type ZeroCopyFramePool struct { // NewZeroCopyFramePool creates a new zero-copy frame pool func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool { - // Pre-allocate 15 frames for immediate availability - preallocSize := 15 - maxPoolSize := 50 // Limit total pool size - preallocated := make([]*ZeroCopyAudioFrame, 0, preallocSize) + // Pre-allocate frames for immediate availability + preallocSizeBytes := GetConfig().PreallocSize + maxPoolSize := GetConfig().MaxPoolSize // Limit total pool size + + // Calculate number of frames based on memory budget, not frame count + preallocFrameCount := preallocSizeBytes / maxFrameSize + if preallocFrameCount > maxPoolSize { + preallocFrameCount = maxPoolSize + } + if preallocFrameCount < 1 { + preallocFrameCount = 1 // Always preallocate at least one frame + } + + preallocated := make([]*ZeroCopyAudioFrame, 0, preallocFrameCount) // Pre-allocate frames to reduce initial allocation overhead - for i := 0; i < preallocSize; i++ { + for i := 0; i < preallocFrameCount; i++ { frame := &ZeroCopyAudioFrame{ data: make([]byte, 0, maxFrameSize), capacity: maxFrameSize, @@ -54,7 +65,7 @@ func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool { return &ZeroCopyFramePool{ maxSize: maxFrameSize, preallocated: preallocated, - preallocSize: preallocSize, + preallocSize: preallocFrameCount, maxPoolSize: maxPoolSize, pool: sync.Pool{ New: func() interface{} { @@ -70,6 +81,20 @@ func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool { // Get retrieves a zero-copy frame from the pool func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { + // Memory guard: Track allocation count to prevent excessive memory usage + allocationCount := atomic.LoadInt64(&p.allocationCount) + if allocationCount > int64(p.maxPoolSize*2) { + // If we've allocated too many frames, force pool reuse + atomic.AddInt64(&p.missCount, 1) + frame := p.pool.Get().(*ZeroCopyAudioFrame) + frame.mutex.Lock() + frame.refCount = 1 + frame.length = 0 + frame.data = frame.data[:0] + frame.mutex.Unlock() + return frame + } + // First try pre-allocated frames for fastest access p.mutex.Lock() if len(p.preallocated) > 0 { @@ -88,7 +113,8 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { } p.mutex.Unlock() - // Try sync.Pool next + // Try sync.Pool next and track allocation + atomic.AddInt64(&p.allocationCount, 1) frame := p.pool.Get().(*ZeroCopyAudioFrame) frame.mutex.Lock() frame.refCount = 1 @@ -230,6 +256,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats { hitCount := atomic.LoadInt64(&p.hitCount) missCount := atomic.LoadInt64(&p.missCount) + allocationCount := atomic.LoadInt64(&p.allocationCount) totalRequests := hitCount + missCount var hitRate float64 @@ -245,6 +272,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats { PreallocatedMax: int64(p.preallocSize), HitCount: hitCount, MissCount: missCount, + AllocationCount: allocationCount, HitRate: hitRate, } } @@ -258,6 +286,7 @@ type ZeroCopyFramePoolStats struct { PreallocatedMax int64 HitCount int64 MissCount int64 + AllocationCount int64 HitRate float64 // Percentage }