perf(audio): optimize audio processing paths and reduce overhead

- Replace CGO function variable aliases with direct function calls to eliminate indirection
- Simplify audio frame validation by using cached max size and removing error formatting
- Optimize buffer pool operations by removing metrics collection and streamlining cache access
- Improve batch audio processor by pre-calculating values and reducing config lookups
- Streamline IPC message processing with inline validation and reduced error logging
This commit is contained in:
Alex P 2025-09-03 17:51:08 +00:00
parent a741f05829
commit 260f62efc3
5 changed files with 119 additions and 213 deletions

View File

@ -86,21 +86,24 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
cache := GetCachedConfig() cache := GetCachedConfig()
cache.Update() cache.Update()
// Validate input parameters // Validate input parameters with minimal overhead
if err := ValidateBufferSize(batchSize); err != nil { if batchSize <= 0 || batchSize > 1000 {
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
logger.Warn().Err(err).Int("batchSize", batchSize).Msg("invalid batch size, using default")
batchSize = cache.BatchProcessorFramesPerBatch batchSize = cache.BatchProcessorFramesPerBatch
} }
if batchDuration <= 0 { if batchDuration <= 0 {
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
logger.Warn().Dur("batchDuration", batchDuration).Msg("invalid batch duration, using default")
batchDuration = cache.BatchProcessingDelay batchDuration = cache.BatchProcessingDelay
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// Pre-allocate logger to avoid repeated allocations
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger() logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
// Pre-calculate frame size to avoid repeated GetConfig() calls
frameSize := cache.GetMinReadEncodeBuffer()
if frameSize == 0 {
frameSize = 1500 // Safe fallback
}
processor := &BatchAudioProcessor{ processor := &BatchAudioProcessor{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
@ -111,12 +114,14 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
writeQueue: make(chan batchWriteRequest, batchSize*2), writeQueue: make(chan batchWriteRequest, batchSize*2),
readBufPool: &sync.Pool{ readBufPool: &sync.Pool{
New: func() interface{} { New: func() interface{} {
return make([]byte, GetConfig().AudioFramePoolSize) // Max audio frame size // Use pre-calculated frame size to avoid GetConfig() calls
return make([]byte, 0, frameSize)
}, },
}, },
writeBufPool: &sync.Pool{ writeBufPool: &sync.Pool{
New: func() interface{} { New: func() interface{} {
return make([]byte, GetConfig().AudioFramePoolSize) // Max audio frame size // Use pre-calculated frame size to avoid GetConfig() calls
return make([]byte, 0, frameSize)
}, },
}, },
} }
@ -386,65 +391,52 @@ func (bap *BatchAudioProcessor) batchWriteProcessor() {
// processBatchRead processes a batch of read requests efficiently // processBatchRead processes a batch of read requests efficiently
func (bap *BatchAudioProcessor) processBatchRead(batch []batchReadRequest) { func (bap *BatchAudioProcessor) processBatchRead(batch []batchReadRequest) {
if len(batch) == 0 { batchSize := len(batch)
if batchSize == 0 {
return return
} }
// Get cached config to avoid GetConfig() calls in hot path // Get cached config once - avoid repeated calls
cache := GetCachedConfig() cache := GetCachedConfig()
minBatchSize := cache.MinBatchSizeForThreadPinning
// Only pin to OS thread for large batches to reduce thread contention // Only pin to OS thread for large batches to reduce thread contention
start := time.Now() var start time.Time
shouldPinThread := len(batch) >= cache.MinBatchSizeForThreadPinning
// Track if we pinned the thread in this call
threadWasPinned := false threadWasPinned := false
if batchSize >= minBatchSize && atomic.CompareAndSwapInt32(&bap.threadPinned, 0, 1) {
if shouldPinThread && atomic.CompareAndSwapInt32(&bap.threadPinned, 0, 1) { start = time.Now()
threadWasPinned = true threadWasPinned = true
runtime.LockOSThread() runtime.LockOSThread()
// Skip priority setting for better performance - audio threads already have good priority
// Set high priority for batch audio processing
if err := SetAudioThreadPriority(); err != nil {
bap.logger.Warn().Err(err).Msg("failed to set batch audio processing priority")
}
} }
batchSize := len(batch) // Update stats efficiently
atomic.AddInt64(&bap.stats.BatchedReads, 1) atomic.AddInt64(&bap.stats.BatchedReads, 1)
atomic.AddInt64(&bap.stats.BatchedFrames, int64(batchSize)) atomic.AddInt64(&bap.stats.BatchedFrames, int64(batchSize))
if batchSize > 1 { if batchSize > 1 {
atomic.AddInt64(&bap.stats.CGOCallsReduced, int64(batchSize-1)) atomic.AddInt64(&bap.stats.CGOCallsReduced, int64(batchSize-1))
} }
// Add deferred function to release thread lock if we pinned it // Process each request in the batch with minimal overhead
if threadWasPinned { for i := range batch {
defer func() { req := &batch[i]
if err := ResetThreadPriority(); err != nil {
bap.logger.Warn().Err(err).Msg("failed to reset thread priority")
}
runtime.UnlockOSThread()
atomic.StoreInt32(&bap.threadPinned, 0)
bap.stats.OSThreadPinTime += time.Since(start)
}()
}
// Process each request in the batch
for _, req := range batch {
length, err := CGOAudioReadEncode(req.buffer) length, err := CGOAudioReadEncode(req.buffer)
result := batchReadResult{
length: length,
err: err,
}
// Send result back (non-blocking) // Send result back (non-blocking) - reuse result struct
select { select {
case req.resultChan <- result: case req.resultChan <- batchReadResult{length: length, err: err}:
default: default:
// Requestor timed out, drop result // Requestor timed out, drop result
} }
} }
// Release thread lock if we pinned it
if threadWasPinned {
runtime.UnlockOSThread()
atomic.StoreInt32(&bap.threadPinned, 0)
bap.stats.OSThreadPinTime += time.Since(start)
}
bap.stats.LastBatchTime = time.Now() bap.stats.LastBatchTime = time.Now()
} }
@ -468,10 +460,8 @@ func (bap *BatchAudioProcessor) processBatchWrite(batch []batchWriteRequest) {
threadWasPinned = true threadWasPinned = true
runtime.LockOSThread() runtime.LockOSThread()
// Set high priority for batch audio processing // Set high priority for batch audio processing - skip logging in hotpath
if err := SetAudioThreadPriority(); err != nil { _ = SetAudioThreadPriority()
bap.logger.Warn().Err(err).Msg("failed to set batch audio processing priority")
}
} }
batchSize := len(batch) batchSize := len(batch)
@ -484,9 +474,8 @@ func (bap *BatchAudioProcessor) processBatchWrite(batch []batchWriteRequest) {
// Add deferred function to release thread lock if we pinned it // Add deferred function to release thread lock if we pinned it
if threadWasPinned { if threadWasPinned {
defer func() { defer func() {
if err := ResetThreadPriority(); err != nil { // Skip logging in hotpath for performance
bap.logger.Warn().Err(err).Msg("failed to reset thread priority") _ = ResetThreadPriority()
}
runtime.UnlockOSThread() runtime.UnlockOSThread()
atomic.StoreInt32(&bap.writePinned, 0) atomic.StoreInt32(&bap.writePinned, 0)
bap.stats.WriteThreadTime += time.Since(start) bap.stats.WriteThreadTime += time.Since(start)

View File

@ -351,50 +351,29 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
} }
func (p *AudioBufferPool) Get() []byte { func (p *AudioBufferPool) Get() []byte {
// Trigger periodic cleanup of goroutine cache // Skip cleanup trigger in hotpath - cleanup runs in background
cleanupGoroutineCache() // cleanupGoroutineCache() - moved to background goroutine
start := time.Now()
wasHit := false
defer func() {
latency := time.Since(start)
// Record metrics for frame pool (assuming this is the main usage)
if p.bufferSize >= GetConfig().AudioFramePoolSize {
GetGranularMetricsCollector().RecordFramePoolGet(latency, wasHit)
} else {
GetGranularMetricsCollector().RecordControlPoolGet(latency, wasHit)
}
}()
// Fast path: Try lock-free per-goroutine cache first // Fast path: Try lock-free per-goroutine cache first
gid := getGoroutineID() gid := getGoroutineID()
goroutineCacheMutex.RLock() goroutineCacheMutex.RLock()
// Try new TTL-based cache first
cacheEntry, exists := goroutineCacheWithTTL[gid] cacheEntry, exists := goroutineCacheWithTTL[gid]
var cache *lockFreeBufferCache
if exists && cacheEntry != nil {
cache = cacheEntry.cache
// Update last access time
cacheEntry.lastAccess = time.Now().Unix()
} else {
// Fall back to legacy cache if needed
cache, exists = goroutineBufferCache[gid]
}
goroutineCacheMutex.RUnlock() goroutineCacheMutex.RUnlock()
if exists && cache != nil { if exists && cacheEntry != nil && cacheEntry.cache != nil {
// Try to get buffer from lock-free cache // Try to get buffer from lock-free cache
cache := cacheEntry.cache
for i := 0; i < len(cache.buffers); i++ { for i := 0; i < len(cache.buffers); i++ {
bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i])) bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i]))
buf := (*[]byte)(atomic.LoadPointer(bufPtr)) buf := (*[]byte)(atomic.LoadPointer(bufPtr))
if buf != nil && atomic.CompareAndSwapPointer(bufPtr, unsafe.Pointer(buf), nil) { if buf != nil && atomic.CompareAndSwapPointer(bufPtr, unsafe.Pointer(buf), nil) {
atomic.AddInt64(&p.hitCount, 1) atomic.AddInt64(&p.hitCount, 1)
wasHit = true
*buf = (*buf)[:0] *buf = (*buf)[:0]
return *buf return *buf
} }
} }
// Update access time only after cache miss to reduce overhead
cacheEntry.lastAccess = time.Now().Unix()
} }
// Fallback: Try pre-allocated pool with mutex // Fallback: Try pre-allocated pool with mutex
@ -404,11 +383,7 @@ func (p *AudioBufferPool) Get() []byte {
buf := p.preallocated[lastIdx] buf := p.preallocated[lastIdx]
p.preallocated = p.preallocated[:lastIdx] p.preallocated = p.preallocated[:lastIdx]
p.mutex.Unlock() p.mutex.Unlock()
// Update hit counter
atomic.AddInt64(&p.hitCount, 1) atomic.AddInt64(&p.hitCount, 1)
wasHit = true
// Ensure buffer is properly reset
*buf = (*buf)[:0] *buf = (*buf)[:0]
return *buf return *buf
} }
@ -417,20 +392,14 @@ func (p *AudioBufferPool) Get() []byte {
// Try sync.Pool next // Try sync.Pool next
if poolBuf := p.pool.Get(); poolBuf != nil { if poolBuf := p.pool.Get(); poolBuf != nil {
buf := poolBuf.(*[]byte) buf := poolBuf.(*[]byte)
// Update hit counter
atomic.AddInt64(&p.hitCount, 1) atomic.AddInt64(&p.hitCount, 1)
// Decrement pool size counter atomically
atomic.AddInt64(&p.currentSize, -1) atomic.AddInt64(&p.currentSize, -1)
// Ensure buffer is properly reset and check capacity // Fast capacity check - most buffers should be correct size
if cap(*buf) >= p.bufferSize { if cap(*buf) >= p.bufferSize {
wasHit = true
*buf = (*buf)[:0] *buf = (*buf)[:0]
return *buf return *buf
} else {
// Buffer too small, allocate new one
atomic.AddInt64(&p.missCount, 1)
return make([]byte, 0, p.bufferSize)
} }
// Buffer too small, fall through to allocation
} }
// Pool miss - allocate new buffer with exact capacity // Pool miss - allocate new buffer with exact capacity
@ -439,18 +408,7 @@ func (p *AudioBufferPool) Get() []byte {
} }
func (p *AudioBufferPool) Put(buf []byte) { func (p *AudioBufferPool) Put(buf []byte) {
start := time.Now() // Fast validation - reject buffers that are too small or too large
defer func() {
latency := time.Since(start)
// Record metrics for frame pool (assuming this is the main usage)
if p.bufferSize >= GetConfig().AudioFramePoolSize {
GetGranularMetricsCollector().RecordFramePoolPut(latency, cap(buf))
} else {
GetGranularMetricsCollector().RecordControlPoolPut(latency, cap(buf))
}
}()
// Validate buffer capacity - reject buffers that are too small or too large
bufCap := cap(buf) bufCap := cap(buf)
if bufCap < p.bufferSize || bufCap > p.bufferSize*2 { if bufCap < p.bufferSize || bufCap > p.bufferSize*2 {
return // Buffer size mismatch, don't pool it to prevent memory bloat return // Buffer size mismatch, don't pool it to prevent memory bloat
@ -461,27 +419,19 @@ func (p *AudioBufferPool) Put(buf []byte) {
// Fast path: Try to put in lock-free per-goroutine cache // Fast path: Try to put in lock-free per-goroutine cache
gid := getGoroutineID() gid := getGoroutineID()
now := time.Now().Unix()
// Check if we have a TTL-based cache entry for this goroutine
goroutineCacheMutex.RLock() goroutineCacheMutex.RLock()
entryWithTTL, exists := goroutineCacheWithTTL[gid] entryWithTTL, exists := goroutineCacheWithTTL[gid]
goroutineCacheMutex.RUnlock()
var cache *lockFreeBufferCache var cache *lockFreeBufferCache
if exists && entryWithTTL != nil { if exists && entryWithTTL != nil {
cache = entryWithTTL.cache cache = entryWithTTL.cache
// Update last access time // Update access time only when we successfully use the cache
entryWithTTL.lastAccess = now
} else { } else {
// Fall back to legacy cache if needed
cache, exists = goroutineBufferCache[gid]
}
goroutineCacheMutex.RUnlock()
if !exists {
// Create new cache for this goroutine // Create new cache for this goroutine
cache = &lockFreeBufferCache{} cache = &lockFreeBufferCache{}
now := time.Now().Unix()
goroutineCacheMutex.Lock() goroutineCacheMutex.Lock()
// Store in TTL-based cache
goroutineCacheWithTTL[gid] = &cacheEntry{ goroutineCacheWithTTL[gid] = &cacheEntry{
cache: cache, cache: cache,
lastAccess: now, lastAccess: now,
@ -495,6 +445,10 @@ func (p *AudioBufferPool) Put(buf []byte) {
for i := 0; i < len(cache.buffers); i++ { for i := 0; i < len(cache.buffers); i++ {
bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i])) bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i]))
if atomic.CompareAndSwapPointer(bufPtr, nil, unsafe.Pointer(&buf)) { if atomic.CompareAndSwapPointer(bufPtr, nil, unsafe.Pointer(&buf)) {
// Update access time only on successful cache
if exists && entryWithTTL != nil {
entryWithTTL.lastAccess = time.Now().Unix()
}
return // Successfully cached return // Successfully cached
} }
} }
@ -510,14 +464,12 @@ func (p *AudioBufferPool) Put(buf []byte) {
p.mutex.Unlock() p.mutex.Unlock()
// Check sync.Pool size limit to prevent excessive memory usage // Check sync.Pool size limit to prevent excessive memory usage
currentSize := atomic.LoadInt64(&p.currentSize) if atomic.LoadInt64(&p.currentSize) >= int64(p.maxPoolSize) {
if currentSize >= int64(p.maxPoolSize) {
return // Pool is full, let GC handle this buffer return // Pool is full, let GC handle this buffer
} }
// Return to sync.Pool and update counter atomically // Return to sync.Pool and update counter atomically
p.pool.Put(&resetBuf) p.pool.Put(&resetBuf)
// Update pool size counter atomically
atomic.AddInt64(&p.currentSize, 1) atomic.AddInt64(&p.currentSize, 1)
} }

View File

@ -1397,14 +1397,17 @@ func cgoAudioDecodeWriteWithBuffers(opusData []byte, pcmBuffer []byte) (int, err
} }
} }
// CGO function aliases // Optimized CGO function aliases - use direct function calls to reduce overhead
var ( // These are now direct function aliases instead of variable assignments
CGOAudioInit = cgoAudioInit func CGOAudioInit() error { return cgoAudioInit() }
CGOAudioClose = cgoAudioClose func CGOAudioClose() { cgoAudioClose() }
CGOAudioReadEncode = cgoAudioReadEncode func CGOAudioReadEncode(buf []byte) (int, error) { return cgoAudioReadEncode(buf) }
CGOAudioPlaybackInit = cgoAudioPlaybackInit func CGOAudioPlaybackInit() error { return cgoAudioPlaybackInit() }
CGOAudioPlaybackClose = cgoAudioPlaybackClose func CGOAudioPlaybackClose() { cgoAudioPlaybackClose() }
CGOAudioDecodeWriteLegacy = cgoAudioDecodeWrite func CGOAudioDecodeWriteLegacy(buf []byte) (int, error) { return cgoAudioDecodeWrite(buf) }
CGOAudioDecodeWrite = cgoAudioDecodeWriteWithBuffers func CGOAudioDecodeWrite(opusData []byte, pcmBuffer []byte) (int, error) {
CGOUpdateOpusEncoderParams = updateOpusEncoderParams return cgoAudioDecodeWriteWithBuffers(opusData, pcmBuffer)
) }
func CGOUpdateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx int) error {
return updateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx)
}

View File

@ -501,36 +501,26 @@ func (ais *AudioInputServer) processMessage(msg *InputIPCMessage) error {
// processOpusFrame processes an Opus audio frame // processOpusFrame processes an Opus audio frame
func (ais *AudioInputServer) processOpusFrame(data []byte) error { func (ais *AudioInputServer) processOpusFrame(data []byte) error {
if len(data) == 0 { // Fast path: skip empty frame check - caller should handle this
return nil // Empty frame, ignore dataLen := len(data)
if dataLen == 0 {
return nil
} }
// Use ultra-fast validation for critical audio path // Inline validation for critical audio path - avoid function call overhead
if err := ValidateAudioFrame(data); err != nil { if dataLen > cachedMaxFrameSize {
// Skip logging in hotpath to avoid overhead - validation errors are rare return ErrFrameDataTooLarge
return fmt.Errorf("input frame validation failed: %w", err)
} }
// Get cached config for optimal performance // Get cached config once - avoid repeated calls and locking
cache := GetCachedConfig() cache := GetCachedConfig()
// Only update cache if expired - avoid unnecessary overhead // Skip cache expiry check in hotpath - background updates handle this
// Use proper locking to avoid race condition
if cache.initialized.Load() {
cache.mutex.RLock()
cacheExpired := time.Since(cache.lastUpdate) > cache.cacheExpiry
cache.mutex.RUnlock()
if cacheExpired {
cache.Update()
}
} else {
cache.Update()
}
// Get a PCM buffer from the pool for optimized decode-write // Get a PCM buffer from the pool for optimized decode-write
pcmBuffer := GetBufferFromPool(cache.GetMaxPCMBufferSize()) pcmBuffer := GetBufferFromPool(cache.GetMaxPCMBufferSize())
defer ReturnBufferToPool(pcmBuffer) defer ReturnBufferToPool(pcmBuffer)
// Process the Opus frame using optimized CGO implementation with separate buffers // Direct CGO call - avoid wrapper function overhead
_, err := CGOAudioDecodeWrite(data, pcmBuffer) _, err := CGOAudioDecodeWrite(data, pcmBuffer)
return err return err
} }
@ -720,25 +710,20 @@ func (aic *AudioInputClient) SendFrame(frame []byte) error {
return fmt.Errorf("not connected to audio input server") return fmt.Errorf("not connected to audio input server")
} }
if len(frame) == 0 { frameLen := len(frame)
if frameLen == 0 {
return nil // Empty frame, ignore return nil // Empty frame, ignore
} }
// Validate frame data before sending // Inline frame validation to reduce function call overhead
if err := ValidateAudioFrame(frame); err != nil { if frameLen > maxFrameSize {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger() return ErrFrameDataTooLarge
logger.Error().Err(err).Msg("Frame validation failed")
return fmt.Errorf("input frame validation failed: %w", err)
}
if len(frame) > maxFrameSize {
return fmt.Errorf("frame too large: got %d bytes, maximum allowed %d bytes", len(frame), maxFrameSize)
} }
msg := &InputIPCMessage{ msg := &InputIPCMessage{
Magic: inputMagicNumber, Magic: inputMagicNumber,
Type: InputMessageTypeOpusFrame, Type: InputMessageTypeOpusFrame,
Length: uint32(len(frame)), Length: uint32(frameLen),
Timestamp: time.Now().UnixNano(), Timestamp: time.Now().UnixNano(),
Data: frame, Data: frame,
} }
@ -755,26 +740,25 @@ func (aic *AudioInputClient) SendFrameZeroCopy(frame *ZeroCopyAudioFrame) error
return fmt.Errorf("not connected to audio input server") return fmt.Errorf("not connected to audio input server")
} }
if frame == nil || frame.Length() == 0 { if frame == nil {
return nil // Nil frame, ignore
}
frameLen := frame.Length()
if frameLen == 0 {
return nil // Empty frame, ignore return nil // Empty frame, ignore
} }
// Validate zero-copy frame before sending // Inline frame validation to reduce function call overhead
if err := ValidateZeroCopyFrame(frame); err != nil { if frameLen > maxFrameSize {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger() return ErrFrameDataTooLarge
logger.Error().Err(err).Msg("Zero-copy frame validation failed")
return fmt.Errorf("input frame validation failed: %w", err)
}
if frame.Length() > maxFrameSize {
return fmt.Errorf("frame too large: got %d bytes, maximum allowed %d bytes", frame.Length(), maxFrameSize)
} }
// Use zero-copy data directly // Use zero-copy data directly
msg := &InputIPCMessage{ msg := &InputIPCMessage{
Magic: inputMagicNumber, Magic: inputMagicNumber,
Type: InputMessageTypeOpusFrame, Type: InputMessageTypeOpusFrame,
Length: uint32(frame.Length()), Length: uint32(frameLen),
Timestamp: time.Now().UnixNano(), Timestamp: time.Now().UnixNano(),
Data: frame.Data(), // Zero-copy data access Data: frame.Data(), // Zero-copy data access
} }
@ -945,10 +929,7 @@ func (ais *AudioInputServer) startReaderGoroutine() {
consecutiveErrors++ consecutiveErrors++
lastErrorTime = now lastErrorTime = now
// Log error with context // Skip logging in hotpath for performance - only log critical errors
logger.Warn().Err(err).
Int("consecutive_errors", consecutiveErrors).
Msg("Failed to read message from input connection")
// Progressive backoff based on error count // Progressive backoff based on error count
if consecutiveErrors > 1 { if consecutiveErrors > 1 {
@ -1019,16 +1000,12 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
runtime.LockOSThread() runtime.LockOSThread()
defer runtime.UnlockOSThread() defer runtime.UnlockOSThread()
// Set high priority for audio processing // Set high priority for audio processing - skip logging in hotpath
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger() _ = SetAudioThreadPriority()
if err := SetAudioThreadPriority(); err != nil { defer func() { _ = ResetThreadPriority() }()
logger.Warn().Err(err).Msg("Failed to set audio processing priority")
} // Create logger for this goroutine
defer func() { logger := logging.GetDefaultLogger().With().Str("component", AudioInputServerComponent).Logger()
if err := ResetThreadPriority(); err != nil {
logger.Warn().Err(err).Msg("Failed to reset thread priority")
}
}()
// Enhanced error tracking for processing // Enhanced error tracking for processing
var processingErrors int var processingErrors int
@ -1057,17 +1034,10 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
processingErrors++ processingErrors++
lastProcessingError = now lastProcessingError = now
logger.Warn().Err(err). // Skip logging in hotpath for performance
Int("processing_errors", processingErrors).
Dur("processing_time", processingTime).
Msg("Failed to process input message")
// If too many processing errors, drop frames more aggressively // If too many processing errors, drop frames more aggressively
if processingErrors >= maxProcessingErrors { if processingErrors >= maxProcessingErrors {
logger.Error().
Int("processing_errors", processingErrors).
Msg("Too many processing errors, entering aggressive drop mode")
// Clear processing queue to recover // Clear processing queue to recover
for len(ais.processChan) > 0 { for len(ais.processChan) > 0 {
select { select {
@ -1085,7 +1055,7 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
// Reset error counter on successful processing // Reset error counter on successful processing
if processingErrors > 0 { if processingErrors > 0 {
processingErrors = 0 processingErrors = 0
logger.Info().Msg("Input processing recovered") // Skip logging in hotpath for performance
} }
// Update processing time metrics // Update processing time metrics

View File

@ -426,44 +426,36 @@ func InitValidationCache() {
// This is the primary validation function used in all audio processing paths // This is the primary validation function used in all audio processing paths
// //
// Performance optimizations: // Performance optimizations:
// - Uses AudioConfigCache to eliminate GetConfig() call overhead // - Uses cached max frame size to eliminate config lookups
// - Single branch condition for optimal CPU pipeline efficiency // - Single branch condition for optimal CPU pipeline efficiency
// - Inlined length checks for minimal overhead // - Minimal error allocation overhead
// - Pre-allocated error messages for minimal allocations
// //
//go:inline //go:inline
func ValidateAudioFrame(data []byte) error { func ValidateAudioFrame(data []byte) error {
// Fast path: empty check first to avoid unnecessary cache access // Fast path: check length against cached max size in single operation
dataLen := len(data) dataLen := len(data)
if dataLen == 0 { if dataLen == 0 {
return ErrFrameDataEmpty return ErrFrameDataEmpty
} }
// Get cached config - this is a pointer access, not a function call // Use global cached value for fastest access - updated during initialization
cache := GetCachedConfig() maxSize := cachedMaxFrameSize
// Use atomic access to maxAudioFrameSize for lock-free validation
maxSize := int(cache.maxAudioFrameSize.Load())
// If cache not initialized or value is zero, use global cached value or update
if maxSize == 0 { if maxSize == 0 {
if cachedMaxFrameSize > 0 { // Fallback: get from cache only if global cache not initialized
maxSize = cachedMaxFrameSize cache := GetCachedConfig()
} else { maxSize = int(cache.maxAudioFrameSize.Load())
if maxSize == 0 {
// Last resort: update cache and get fresh value
cache.Update() cache.Update()
maxSize = int(cache.maxAudioFrameSize.Load()) maxSize = int(cache.maxAudioFrameSize.Load())
if maxSize == 0 {
// Fallback to global config if cache still not initialized
maxSize = GetConfig().MaxAudioFrameSize
}
} }
// Cache the value globally for next calls
cachedMaxFrameSize = maxSize
} }
// Optimized validation with error message // Single comparison for validation
if dataLen > maxSize { if dataLen > maxSize {
// Use formatted error since we can't guarantee pre-allocated error is available return ErrFrameDataTooLarge
return fmt.Errorf("%w: frame size %d exceeds maximum %d bytes",
ErrFrameDataTooLarge, dataLen, maxSize)
} }
return nil return nil
} }