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 {
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

View File

@ -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,

View File

@ -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<<uint(i/3))) * time.Millisecond
if delay > 500*time.Millisecond {
delay = 500 * time.Millisecond
// Exponential backoff starting from config
backoffStart := GetConfig().BackoffStart
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
maxDelay := GetConfig().MaxRetryDelay
if delay > maxDelay {
delay = maxDelay
}
time.Sleep(delay)
}

View File

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

View File

@ -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 {

View File

@ -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
}