mirror of https://github.com/jetkvm/kvm.git
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:
parent
35a666ed31
commit
9e343b3cc7
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue