kvm/internal/audio/ipc_common.go
Alex P 1d1658db15 refactor(audio): replace GetConfig() calls with direct Config access
This change replaces all instances of GetConfig() function calls with direct access to the Config variable throughout the audio package. The modification improves performance by eliminating function call overhead and simplifies the codebase by removing unnecessary indirection.

The commit also includes minor optimizations in validation logic and connection handling, while maintaining all existing functionality. Error handling remains robust with appropriate fallbacks when config values are not available.

Additional improvements include:
- Enhanced connection health monitoring in UnifiedAudioClient
- Optimized validation functions using cached config values
- Reduced memory allocations in hot paths
- Improved error recovery during quality changes
2025-09-08 17:30:49 +00:00

228 lines
5.8 KiB
Go

package audio
import (
"encoding/binary"
"fmt"
"net"
"sync"
"sync/atomic"
"time"
)
// Common IPC message interface
type IPCMessage interface {
GetMagic() uint32
GetType() uint8
GetLength() uint32
GetTimestamp() int64
GetData() []byte
}
// Common optimized message structure
type OptimizedMessage struct {
header [17]byte // Pre-allocated header buffer
data []byte // Reusable data buffer
}
// Generic message pool for both input and output
type GenericMessagePool struct {
// 64-bit fields must be first for proper alignment on ARM
hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic)
pool chan *OptimizedMessage
preallocated []*OptimizedMessage // Pre-allocated messages
preallocSize int
maxPoolSize int
mutex sync.RWMutex
}
// NewGenericMessagePool creates a new generic message pool
func NewGenericMessagePool(size int) *GenericMessagePool {
pool := &GenericMessagePool{
pool: make(chan *OptimizedMessage, size),
preallocSize: size / 4, // 25% pre-allocated for immediate use
maxPoolSize: size,
}
// Pre-allocate some messages for immediate use
pool.preallocated = make([]*OptimizedMessage, pool.preallocSize)
for i := 0; i < pool.preallocSize; i++ {
pool.preallocated[i] = &OptimizedMessage{
data: make([]byte, 0, Config.MaxFrameSize),
}
}
// Fill the channel pool
for i := 0; i < size-pool.preallocSize; i++ {
select {
case pool.pool <- &OptimizedMessage{
data: make([]byte, 0, Config.MaxFrameSize),
}:
default:
break
}
}
return pool
}
// Get retrieves an optimized message from the pool
func (mp *GenericMessagePool) Get() *OptimizedMessage {
// Try pre-allocated first (fastest path)
mp.mutex.Lock()
if len(mp.preallocated) > 0 {
msg := mp.preallocated[len(mp.preallocated)-1]
mp.preallocated = mp.preallocated[:len(mp.preallocated)-1]
mp.mutex.Unlock()
atomic.AddInt64(&mp.hitCount, 1)
return msg
}
mp.mutex.Unlock()
// Try channel pool
select {
case msg := <-mp.pool:
atomic.AddInt64(&mp.hitCount, 1)
return msg
default:
// Pool empty, create new message
atomic.AddInt64(&mp.missCount, 1)
return &OptimizedMessage{
data: make([]byte, 0, Config.MaxFrameSize),
}
}
}
// Put returns an optimized message to the pool
func (mp *GenericMessagePool) Put(msg *OptimizedMessage) {
if msg == nil {
return
}
// Reset the message for reuse
msg.data = msg.data[:0]
// Try to return to pre-allocated slice first
mp.mutex.Lock()
if len(mp.preallocated) < mp.preallocSize {
mp.preallocated = append(mp.preallocated, msg)
mp.mutex.Unlock()
return
}
mp.mutex.Unlock()
// Try to return to channel pool
select {
case mp.pool <- msg:
// Successfully returned to pool
default:
// Pool full, let GC handle it
}
}
// GetStats returns pool statistics
func (mp *GenericMessagePool) GetStats() (hitCount, missCount int64, hitRate float64) {
hits := atomic.LoadInt64(&mp.hitCount)
misses := atomic.LoadInt64(&mp.missCount)
total := hits + misses
if total > 0 {
hitRate = float64(hits) / float64(total) * 100
}
return hits, misses, hitRate
}
// Common write message function
func WriteIPCMessage(conn net.Conn, msg IPCMessage, pool *GenericMessagePool, droppedFramesCounter *int64) error {
if conn == nil {
return fmt.Errorf("connection is nil")
}
// Get optimized message from pool for header preparation
optMsg := pool.Get()
defer pool.Put(optMsg)
// Prepare header in pre-allocated buffer
binary.LittleEndian.PutUint32(optMsg.header[0:4], msg.GetMagic())
optMsg.header[4] = msg.GetType()
binary.LittleEndian.PutUint32(optMsg.header[5:9], msg.GetLength())
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(msg.GetTimestamp()))
// Set write deadline for timeout handling (more efficient than goroutines)
if deadline := time.Now().Add(Config.WriteTimeout); deadline.After(time.Now()) {
if err := conn.SetWriteDeadline(deadline); err != nil {
// If we can't set deadline, proceed without it
// This maintains compatibility with connections that don't support deadlines
_ = err // Explicitly ignore error for linter
}
}
// Write header using pre-allocated buffer (synchronous for better performance)
_, err := conn.Write(optMsg.header[:])
if err != nil {
if droppedFramesCounter != nil {
atomic.AddInt64(droppedFramesCounter, 1)
}
return err
}
// Write data if present
if msg.GetLength() > 0 && msg.GetData() != nil {
_, err = conn.Write(msg.GetData())
if err != nil {
if droppedFramesCounter != nil {
atomic.AddInt64(droppedFramesCounter, 1)
}
return err
}
}
// Clear write deadline after successful write
_ = conn.SetWriteDeadline(time.Time{}) // Ignore error as this is cleanup
return nil
}
// Common connection acceptance with retry logic
func AcceptConnectionWithRetry(listener net.Listener, maxRetries int, retryDelay time.Duration) (net.Conn, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
conn, err := listener.Accept()
if err == nil {
return conn, nil
}
lastErr = err
if i < maxRetries-1 {
time.Sleep(retryDelay)
}
}
return nil, fmt.Errorf("failed to accept connection after %d retries: %w", maxRetries, lastErr)
}
// Common frame statistics structure
type FrameStats struct {
Total int64
Dropped int64
}
// GetFrameStats safely retrieves frame statistics
func GetFrameStats(totalCounter, droppedCounter *int64) FrameStats {
return FrameStats{
Total: atomic.LoadInt64(totalCounter),
Dropped: atomic.LoadInt64(droppedCounter),
}
}
// CalculateDropRate calculates the drop rate percentage
func CalculateDropRate(stats FrameStats) float64 {
if stats.Total == 0 {
return 0.0
}
return float64(stats.Dropped) / float64(stats.Total) * 100.0
}
// ResetFrameStats resets frame counters
func ResetFrameStats(totalCounter, droppedCounter *int64) {
atomic.StoreInt64(totalCounter, 0)
atomic.StoreInt64(droppedCounter, 0)
}