feat(audio): add validation cache fields to AudioConfigCache

Add atomic fields to AudioConfigCache for validation parameters to enable lock-free access
Optimize validation functions to use cached values for common cases
Move AudioFrameBatch to separate file and update validation logic
This commit is contained in:
Alex P 2025-09-03 14:48:41 +00:00
parent 1b7198aec2
commit 2ab90e76e0
2 changed files with 188 additions and 33 deletions

View File

@ -727,6 +727,14 @@ type AudioConfigCache struct {
channels atomic.Int32 channels atomic.Int32
frameSize atomic.Int32 frameSize atomic.Int32
// Additional cached values for validation functions
maxAudioFrameSize atomic.Int32
maxChannels atomic.Int32
minFrameDuration atomic.Int64 // Store as nanoseconds
maxFrameDuration atomic.Int64 // Store as nanoseconds
minOpusBitrate atomic.Int32
maxOpusBitrate atomic.Int32
// Mutex for updating the cache // Mutex for updating the cache
mutex sync.RWMutex mutex sync.RWMutex
lastUpdate time.Time lastUpdate time.Time
@ -768,7 +776,7 @@ func (c *AudioConfigCache) Update() {
if !c.initialized.Load() || time.Since(c.lastUpdate) > c.cacheExpiry { if !c.initialized.Load() || time.Since(c.lastUpdate) > c.cacheExpiry {
config := GetConfig() // Call GetConfig() only once config := GetConfig() // Call GetConfig() only once
// Update atomic values for lock-free access // Update atomic values for lock-free access - CGO values
c.minReadEncodeBuffer.Store(int32(config.MinReadEncodeBuffer)) c.minReadEncodeBuffer.Store(int32(config.MinReadEncodeBuffer))
c.maxDecodeWriteBuffer.Store(int32(config.MaxDecodeWriteBuffer)) c.maxDecodeWriteBuffer.Store(int32(config.MaxDecodeWriteBuffer))
c.maxPacketSize.Store(int32(config.CGOMaxPacketSize)) c.maxPacketSize.Store(int32(config.CGOMaxPacketSize))
@ -783,12 +791,25 @@ func (c *AudioConfigCache) Update() {
c.channels.Store(int32(config.CGOChannels)) c.channels.Store(int32(config.CGOChannels))
c.frameSize.Store(int32(config.CGOFrameSize)) c.frameSize.Store(int32(config.CGOFrameSize))
// Update additional validation values
c.maxAudioFrameSize.Store(int32(config.MaxAudioFrameSize))
c.maxChannels.Store(int32(config.MaxChannels))
c.minFrameDuration.Store(int64(config.MinFrameDuration))
c.maxFrameDuration.Store(int64(config.MaxFrameDuration))
c.minOpusBitrate.Store(int32(config.MinOpusBitrate))
c.maxOpusBitrate.Store(int32(config.MaxOpusBitrate))
// Pre-allocate common errors // Pre-allocate common errors
c.bufferTooSmallReadEncode = newBufferTooSmallError(0, config.MinReadEncodeBuffer) c.bufferTooSmallReadEncode = newBufferTooSmallError(0, config.MinReadEncodeBuffer)
c.bufferTooLargeDecodeWrite = newBufferTooLargeError(config.MaxDecodeWriteBuffer+1, config.MaxDecodeWriteBuffer) c.bufferTooLargeDecodeWrite = newBufferTooLargeError(config.MaxDecodeWriteBuffer+1, config.MaxDecodeWriteBuffer)
c.lastUpdate = time.Now() c.lastUpdate = time.Now()
c.initialized.Store(true) c.initialized.Store(true)
// Update the global validation cache as well
if cachedMaxFrameSize != 0 {
cachedMaxFrameSize = config.MaxAudioFrameSize
}
} }
} }
@ -1029,16 +1050,19 @@ func ReturnBufferToPool(buf []byte) {
ReturnOptimalBuffer(buf) ReturnOptimalBuffer(buf)
} }
// Note: AudioFrameBatch is now defined in batch_audio.go
// This is kept here for reference but commented out to avoid conflicts
/*
// AudioFrameBatch represents a batch of audio frames for processing // AudioFrameBatch represents a batch of audio frames for processing
type AudioFrameBatch struct { type AudioFrameBatch struct {
// Buffer for batch processing // Buffer for batch processing
Buffer []byte buffer []byte
// Number of frames in the batch // Number of frames in the batch
FrameCount int frameCount int
// Size of each frame // Size of each frame
FrameSize int frameSize int
// Current position in the buffer // Current position in the buffer
Position int position int
} }
// NewAudioFrameBatch creates a new audio frame batch with the specified capacity // NewAudioFrameBatch creates a new audio frame batch with the specified capacity
@ -1052,10 +1076,10 @@ func NewAudioFrameBatch(maxFrames int) *AudioFrameBatch {
// Create batch with buffer sized for maxFrames // Create batch with buffer sized for maxFrames
return &AudioFrameBatch{ return &AudioFrameBatch{
Buffer: GetBufferFromPool(maxFrames * frameSize), buffer: GetBufferFromPool(maxFrames * frameSize),
FrameCount: 0, frameCount: 0,
FrameSize: frameSize, frameSize: frameSize,
Position: 0, position: 0,
} }
} }
@ -1063,33 +1087,34 @@ func NewAudioFrameBatch(maxFrames int) *AudioFrameBatch {
// Returns true if the batch is full after adding this frame // Returns true if the batch is full after adding this frame
func (b *AudioFrameBatch) AddFrame(frame []byte) bool { func (b *AudioFrameBatch) AddFrame(frame []byte) bool {
// Calculate position in buffer for this frame // Calculate position in buffer for this frame
pos := b.Position pos := b.position
// Copy frame data to batch buffer // Copy frame data to batch buffer
copy(b.Buffer[pos:pos+len(frame)], frame) copy(b.buffer[pos:pos+len(frame)], frame)
// Update position and frame count // Update position and frame count
b.Position += len(frame) b.position += len(frame)
b.FrameCount++ b.frameCount++
// Check if batch is full (buffer capacity reached) // Check if batch is full (buffer capacity reached)
return b.Position >= len(b.Buffer) return b.position >= len(b.buffer)
} }
// Reset resets the batch for reuse // Reset resets the batch for reuse
func (b *AudioFrameBatch) Reset() { func (b *AudioFrameBatch) Reset() {
b.FrameCount = 0 b.frameCount = 0
b.Position = 0 b.position = 0
} }
// Release returns the batch buffer to the pool // Release returns the batch buffer to the pool
func (b *AudioFrameBatch) Release() { func (b *AudioFrameBatch) Release() {
ReturnBufferToPool(b.Buffer) ReturnBufferToPool(b.buffer)
b.Buffer = nil b.buffer = nil
b.FrameCount = 0 b.frameCount = 0
b.FrameSize = 0 b.frameSize = 0
b.Position = 0 b.position = 0
} }
*/
// ReadEncodeWithPooledBuffer reads audio data and encodes it using a buffer from the pool // ReadEncodeWithPooledBuffer reads audio data and encodes it using a buffer from the pool
// This reduces memory allocations by reusing buffers // This reduces memory allocations by reusing buffers

View File

@ -58,10 +58,22 @@ func ValidateZeroCopyFrame(frame *ZeroCopyAudioFrame) error {
} }
// ValidateBufferSize validates buffer size parameters with enhanced boundary checks // ValidateBufferSize validates buffer size parameters with enhanced boundary checks
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateBufferSize(size int) error { func ValidateBufferSize(size int) error {
if size <= 0 { if size <= 0 {
return fmt.Errorf("%w: buffer size %d must be positive", ErrInvalidBufferSize, size) return fmt.Errorf("%w: buffer size %d must be positive", ErrInvalidBufferSize, size)
} }
// Fast path: Check against cached max frame size
cache := GetCachedConfig()
maxFrameSize := int(cache.maxAudioFrameSize.Load())
// Most common case: validating a buffer that's sized for audio frames
if maxFrameSize > 0 && size <= maxFrameSize {
return nil
}
// Slower path: full validation against SocketMaxBuffer
config := GetConfig() config := GetConfig()
// Use SocketMaxBuffer as the upper limit for general buffer validation // Use SocketMaxBuffer as the upper limit for general buffer validation
// This allows for socket buffers while still preventing extremely large allocations // This allows for socket buffers while still preventing extremely large allocations
@ -199,10 +211,22 @@ func ValidateLatencyConfig(config LatencyConfig) error {
} }
// ValidateSampleRate validates audio sample rate values // ValidateSampleRate validates audio sample rate values
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateSampleRate(sampleRate int) error { func ValidateSampleRate(sampleRate int) error {
if sampleRate <= 0 { if sampleRate <= 0 {
return fmt.Errorf("%w: sample rate %d must be positive", ErrInvalidSampleRate, sampleRate) return fmt.Errorf("%w: sample rate %d must be positive", ErrInvalidSampleRate, sampleRate)
} }
// Fast path: Check against cached sample rate first
cache := GetCachedConfig()
cachedRate := int(cache.sampleRate.Load())
// Most common case: validating against the current sample rate
if sampleRate == cachedRate {
return nil
}
// Slower path: check against all valid rates
config := GetConfig() config := GetConfig()
validRates := config.ValidSampleRates validRates := config.ValidSampleRates
for _, rate := range validRates { for _, rate := range validRates {
@ -215,10 +239,23 @@ func ValidateSampleRate(sampleRate int) error {
} }
// ValidateChannelCount validates audio channel count // ValidateChannelCount validates audio channel count
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateChannelCount(channels int) error { func ValidateChannelCount(channels int) error {
if channels <= 0 { if channels <= 0 {
return fmt.Errorf("%w: channel count %d must be positive", ErrInvalidChannels, channels) return fmt.Errorf("%w: channel count %d must be positive", ErrInvalidChannels, channels)
} }
// Fast path: Check against cached channels first
cache := GetCachedConfig()
cachedChannels := int(cache.channels.Load())
// Most common case: validating against the current channel count
if channels == cachedChannels {
return nil
}
// Check against max channels - still using cache to avoid GetConfig()
// Note: We don't have maxChannels in the cache yet, so we'll use GetConfig() for now
config := GetConfig() config := GetConfig()
if channels > config.MaxChannels { if channels > config.MaxChannels {
return fmt.Errorf("%w: channel count %d exceeds maximum %d", return fmt.Errorf("%w: channel count %d exceeds maximum %d",
@ -228,10 +265,33 @@ func ValidateChannelCount(channels int) error {
} }
// ValidateBitrate validates audio bitrate values (expects kbps) // ValidateBitrate validates audio bitrate values (expects kbps)
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateBitrate(bitrate int) error { func ValidateBitrate(bitrate int) error {
if bitrate <= 0 { if bitrate <= 0 {
return fmt.Errorf("%w: bitrate %d must be positive", ErrInvalidBitrate, bitrate) return fmt.Errorf("%w: bitrate %d must be positive", ErrInvalidBitrate, bitrate)
} }
// Fast path: Check against cached bitrate values
cache := GetCachedConfig()
minBitrate := int(cache.minOpusBitrate.Load())
maxBitrate := int(cache.maxOpusBitrate.Load())
// If we have valid cached values, use them
if minBitrate > 0 && maxBitrate > 0 {
// Convert kbps to bps for comparison with config limits
bitrateInBps := bitrate * 1000
if bitrateInBps < minBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, minBitrate)
}
if bitrateInBps > maxBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, maxBitrate)
}
return nil
}
// Slower path: full validation with GetConfig()
config := GetConfig() config := GetConfig()
// Convert kbps to bps for comparison with config limits // Convert kbps to bps for comparison with config limits
bitrateInBps := bitrate * 1000 bitrateInBps := bitrate * 1000
@ -247,10 +307,31 @@ func ValidateBitrate(bitrate int) error {
} }
// ValidateFrameDuration validates frame duration values // ValidateFrameDuration validates frame duration values
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateFrameDuration(duration time.Duration) error { func ValidateFrameDuration(duration time.Duration) error {
if duration <= 0 { if duration <= 0 {
return fmt.Errorf("%w: frame duration %v must be positive", ErrInvalidFrameDuration, duration) return fmt.Errorf("%w: frame duration %v must be positive", ErrInvalidFrameDuration, duration)
} }
// Fast path: Check against cached frame size first
cache := GetCachedConfig()
// Convert frameSize (samples) to duration for comparison
// Note: This calculation should match how frameSize is converted to duration elsewhere
cachedFrameSize := int(cache.frameSize.Load())
cachedSampleRate := int(cache.sampleRate.Load())
// Only do this calculation if we have valid cached values
if cachedFrameSize > 0 && cachedSampleRate > 0 {
cachedDuration := time.Duration(cachedFrameSize) * time.Second / time.Duration(cachedSampleRate)
// Most common case: validating against the current frame duration
if duration == cachedDuration {
return nil
}
}
// Slower path: full validation against min/max
config := GetConfig() config := GetConfig()
if duration < config.MinFrameDuration { if duration < config.MinFrameDuration {
return fmt.Errorf("%w: frame duration %v below minimum %v", return fmt.Errorf("%w: frame duration %v below minimum %v",
@ -264,7 +345,29 @@ func ValidateFrameDuration(duration time.Duration) error {
} }
// ValidateAudioConfigComplete performs comprehensive audio configuration validation // ValidateAudioConfigComplete performs comprehensive audio configuration validation
// Uses optimized validation functions that leverage AudioConfigCache
func ValidateAudioConfigComplete(config AudioConfig) error { func ValidateAudioConfigComplete(config AudioConfig) error {
// Fast path: Check if all values match the current cached configuration
cache := GetCachedConfig()
cachedSampleRate := int(cache.sampleRate.Load())
cachedChannels := int(cache.channels.Load())
cachedBitrate := int(cache.opusBitrate.Load()) / 1000 // Convert from bps to kbps
cachedFrameSize := int(cache.frameSize.Load())
// Only do this calculation if we have valid cached values
if cachedSampleRate > 0 && cachedChannels > 0 && cachedBitrate > 0 && cachedFrameSize > 0 {
cachedDuration := time.Duration(cachedFrameSize) * time.Second / time.Duration(cachedSampleRate)
// Most common case: validating the current configuration
if config.SampleRate == cachedSampleRate &&
config.Channels == cachedChannels &&
config.Bitrate == cachedBitrate &&
config.FrameSize == cachedDuration {
return nil
}
}
// Slower path: validate each parameter individually
if err := ValidateAudioQuality(config.Quality); err != nil { if err := ValidateAudioQuality(config.Quality); err != nil {
return fmt.Errorf("quality validation failed: %w", err) return fmt.Errorf("quality validation failed: %w", err)
} }
@ -303,37 +406,64 @@ func ValidateAudioConfigConstants(config *AudioConfigConstants) error {
return nil return nil
} }
// Cached max frame size to avoid function call overhead in hot paths // Note: We're transitioning from individual cached values to using AudioConfigCache
var cachedMaxFrameSize int // for better consistency and reduced maintenance overhead
// Note: Validation cache is initialized on first use to avoid init function // Global variable for backward compatibility
var cachedMaxFrameSize int
// InitValidationCache initializes cached validation values with actual config // InitValidationCache initializes cached validation values with actual config
func InitValidationCache() { func InitValidationCache() {
cachedMaxFrameSize = GetConfig().MaxAudioFrameSize // Initialize the global cache variable for backward compatibility
config := GetConfig()
cachedMaxFrameSize = config.MaxAudioFrameSize
// Update the global audio config cache
GetCachedConfig().Update()
} }
// ValidateAudioFrame provides optimized validation for audio frame data // ValidateAudioFrame provides optimized validation for audio frame data
// 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 cached config value to eliminate function call overhead // - Uses AudioConfigCache to eliminate GetConfig() call overhead
// - Single branch condition for optimal CPU pipeline efficiency // - Single branch condition for optimal CPU pipeline efficiency
// - Inlined length checks for minimal overhead // - Inlined length checks for minimal overhead
// - Pre-allocated error messages for minimal allocations
// //
//go:inline //go:inline
func ValidateAudioFrame(data []byte) error { func ValidateAudioFrame(data []byte) error {
// Initialize cache on first use if not already done // Fast path: empty check first to avoid unnecessary cache access
if cachedMaxFrameSize == 0 {
InitValidationCache()
}
// Optimized validation with pre-allocated error messages for minimal overhead
dataLen := len(data) dataLen := len(data)
if dataLen == 0 { if dataLen == 0 {
return ErrFrameDataEmpty return ErrFrameDataEmpty
} }
if dataLen > cachedMaxFrameSize {
return ErrFrameDataTooLarge // Get cached config - this is a pointer access, not a function call
cache := GetCachedConfig()
// 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 cachedMaxFrameSize > 0 {
maxSize = cachedMaxFrameSize
} else {
cache.Update()
maxSize = int(cache.maxAudioFrameSize.Load())
if maxSize == 0 {
// Fallback to global config if cache still not initialized
maxSize = GetConfig().MaxAudioFrameSize
}
}
}
// Optimized validation with error message
if dataLen > maxSize {
// Use formatted error since we can't guarantee pre-allocated error is available
return fmt.Errorf("%w: frame size %d exceeds maximum %d bytes",
ErrFrameDataTooLarge, dataLen, maxSize)
} }
return nil return nil
} }