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
This commit is contained in:
Alex P 2025-08-25 19:02:29 +00:00
parent 35a666ed31
commit 9e343b3cc7
6 changed files with 81 additions and 21 deletions

View File

@ -37,9 +37,9 @@ type AdaptiveBufferConfig struct {
func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig { func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
return AdaptiveBufferConfig{ return AdaptiveBufferConfig{
// Conservative buffer sizes for 256MB RAM constraint // Conservative buffer sizes for 256MB RAM constraint
MinBufferSize: 3, // Minimum 3 frames (slightly higher for stability) MinBufferSize: GetConfig().AdaptiveMinBufferSize,
MaxBufferSize: 20, // Maximum 20 frames (increased for high load scenarios) MaxBufferSize: GetConfig().AdaptiveMaxBufferSize,
DefaultBufferSize: 6, // Default 6 frames (increased for better stability) DefaultBufferSize: GetConfig().AdaptiveDefaultBufferSize,
// CPU thresholds optimized for single-core ARM Cortex A7 under load // CPU thresholds optimized for single-core ARM Cortex A7 under load
LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU

View File

@ -698,6 +698,28 @@ type AudioConfigConstants struct {
OutputSizeThreshold int OutputSizeThreshold int
TargetLevel float64 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 // Priority Scheduling
AudioHighPriority int AudioHighPriority int
AudioMediumPriority int AudioMediumPriority int
@ -1159,6 +1181,11 @@ func DefaultAudioConfig() *AudioConfigConstants {
HighMemoryThreshold: 0.75, HighMemoryThreshold: 0.75,
TargetLatency: 20 * time.Millisecond, 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 // Adaptive Optimizer Configuration
CooldownPeriod: 30 * time.Second, CooldownPeriod: 30 * time.Second,
RollbackThreshold: 300 * time.Millisecond, RollbackThreshold: 300 * time.Millisecond,

View File

@ -19,11 +19,11 @@ import (
var ( var (
inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input) inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input)
inputSocketName = "audio_input.sock" inputSocketName = "audio_input.sock"
writeTimeout = 15 * time.Millisecond // Non-blocking write timeout (increased for high load) writeTimeout = GetConfig().WriteTimeout // Non-blocking write timeout
) )
const ( 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 ( var (
@ -503,10 +503,12 @@ func (aic *AudioInputClient) Connect() error {
aic.running = true aic.running = true
return nil return nil
} }
// Exponential backoff starting at 50ms // Exponential backoff starting from config
delay := time.Duration(50*(1<<uint(i/3))) * time.Millisecond backoffStart := GetConfig().BackoffStart
if delay > 500*time.Millisecond { delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
delay = 500 * time.Millisecond maxDelay := GetConfig().MaxRetryDelay
if delay > maxDelay {
delay = maxDelay
} }
time.Sleep(delay) time.Sleep(delay)
} }

View File

@ -422,10 +422,12 @@ func (c *AudioClient) Connect() error {
c.running = true c.running = true
return nil return nil
} }
// Exponential backoff starting at 50ms // Exponential backoff starting from config
delay := time.Duration(50*(1<<uint(i/3))) * time.Millisecond backoffStart := GetConfig().BackoffStart
if delay > 400*time.Millisecond { delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
delay = 400 * time.Millisecond maxDelay := GetConfig().MaxRetryDelay
if delay > maxDelay {
delay = maxDelay
} }
time.Sleep(delay) time.Sleep(delay)
} }

View File

@ -132,7 +132,7 @@ func (r *AudioRelay) relayLoop() {
defer r.wg.Done() defer r.wg.Done()
r.logger.Debug().Msg("Audio relay loop started") r.logger.Debug().Msg("Audio relay loop started")
const maxConsecutiveErrors = 10 var maxConsecutiveErrors = GetConfig().MaxConsecutiveErrors
consecutiveErrors := 0 consecutiveErrors := 0
for { for {

View File

@ -23,6 +23,7 @@ type ZeroCopyFramePool struct {
counter int64 // Frame counter (atomic) counter int64 // Frame counter (atomic)
hitCount int64 // Pool hit counter (atomic) hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic) missCount int64 // Pool miss counter (atomic)
allocationCount int64 // Total allocations counter (atomic)
// Other fields // Other fields
pool sync.Pool pool sync.Pool
@ -36,13 +37,23 @@ type ZeroCopyFramePool struct {
// NewZeroCopyFramePool creates a new zero-copy frame pool // NewZeroCopyFramePool creates a new zero-copy frame pool
func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool { func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
// Pre-allocate 15 frames for immediate availability // Pre-allocate frames for immediate availability
preallocSize := 15 preallocSizeBytes := GetConfig().PreallocSize
maxPoolSize := 50 // Limit total pool size maxPoolSize := GetConfig().MaxPoolSize // Limit total pool size
preallocated := make([]*ZeroCopyAudioFrame, 0, preallocSize)
// 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 // Pre-allocate frames to reduce initial allocation overhead
for i := 0; i < preallocSize; i++ { for i := 0; i < preallocFrameCount; i++ {
frame := &ZeroCopyAudioFrame{ frame := &ZeroCopyAudioFrame{
data: make([]byte, 0, maxFrameSize), data: make([]byte, 0, maxFrameSize),
capacity: maxFrameSize, capacity: maxFrameSize,
@ -54,7 +65,7 @@ func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
return &ZeroCopyFramePool{ return &ZeroCopyFramePool{
maxSize: maxFrameSize, maxSize: maxFrameSize,
preallocated: preallocated, preallocated: preallocated,
preallocSize: preallocSize, preallocSize: preallocFrameCount,
maxPoolSize: maxPoolSize, maxPoolSize: maxPoolSize,
pool: sync.Pool{ pool: sync.Pool{
New: func() interface{} { New: func() interface{} {
@ -70,6 +81,20 @@ func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
// Get retrieves a zero-copy frame from the pool // Get retrieves a zero-copy frame from the pool
func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { 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 // First try pre-allocated frames for fastest access
p.mutex.Lock() p.mutex.Lock()
if len(p.preallocated) > 0 { if len(p.preallocated) > 0 {
@ -88,7 +113,8 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
} }
p.mutex.Unlock() 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 := p.pool.Get().(*ZeroCopyAudioFrame)
frame.mutex.Lock() frame.mutex.Lock()
frame.refCount = 1 frame.refCount = 1
@ -230,6 +256,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
hitCount := atomic.LoadInt64(&p.hitCount) hitCount := atomic.LoadInt64(&p.hitCount)
missCount := atomic.LoadInt64(&p.missCount) missCount := atomic.LoadInt64(&p.missCount)
allocationCount := atomic.LoadInt64(&p.allocationCount)
totalRequests := hitCount + missCount totalRequests := hitCount + missCount
var hitRate float64 var hitRate float64
@ -245,6 +272,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
PreallocatedMax: int64(p.preallocSize), PreallocatedMax: int64(p.preallocSize),
HitCount: hitCount, HitCount: hitCount,
MissCount: missCount, MissCount: missCount,
AllocationCount: allocationCount,
HitRate: hitRate, HitRate: hitRate,
} }
} }
@ -258,6 +286,7 @@ type ZeroCopyFramePoolStats struct {
PreallocatedMax int64 PreallocatedMax int64
HitCount int64 HitCount int64
MissCount int64 MissCount int64
AllocationCount int64
HitRate float64 // Percentage HitRate float64 // Percentage
} }