mirror of https://github.com/jetkvm/kvm.git
refactor(audio): optimize performance and simplify code
- Replace mutex locks with atomic operations for counters - Remove redundant logging calls to reduce overhead - Simplify error handling and buffer validation - Add exponential backoff for audio relay stability - Streamline CGO audio operations for hotpath optimization
This commit is contained in:
parent
bda92b4a62
commit
a5d1ef1225
|
@ -911,46 +911,28 @@ func updateCacheIfNeeded(cache *AudioConfigCache) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cgoAudioReadEncode(buf []byte) (int, error) {
|
func cgoAudioReadEncode(buf []byte) (int, error) {
|
||||||
cache := GetCachedConfig()
|
// Minimal buffer validation - assume caller provides correct size
|
||||||
updateCacheIfNeeded(cache)
|
if len(buf) == 0 {
|
||||||
|
return 0, errEmptyBuffer
|
||||||
// Fast validation with cached values - avoid lock with atomic access
|
|
||||||
minRequired := cache.GetMinReadEncodeBuffer()
|
|
||||||
|
|
||||||
// Buffer validation - use pre-allocated error for common case
|
|
||||||
if len(buf) < minRequired {
|
|
||||||
// Use pre-allocated error for common case, only create custom error for edge cases
|
|
||||||
if len(buf) > 0 {
|
|
||||||
return 0, newBufferTooSmallError(len(buf), minRequired)
|
|
||||||
}
|
|
||||||
return 0, cache.GetBufferTooSmallError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip initialization check for now to avoid CGO compilation issues
|
// Direct CGO call - hotpath optimization
|
||||||
|
|
||||||
// Direct CGO call with minimal overhead - unsafe.Pointer(&slice[0]) is safe for validated non-empty buffers
|
|
||||||
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
|
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
|
||||||
|
|
||||||
// Fast path for success case
|
// Fast path for success
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
return int(n), nil
|
return int(n), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle error cases - use static error codes to reduce allocations
|
// Error handling with static errors
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
// Common error cases
|
if n == -1 {
|
||||||
switch n {
|
|
||||||
case -1:
|
|
||||||
return 0, errAudioInitFailed
|
return 0, errAudioInitFailed
|
||||||
case -2:
|
|
||||||
return 0, errAudioReadEncode
|
|
||||||
default:
|
|
||||||
return 0, newAudioReadEncodeError(int(n))
|
|
||||||
}
|
}
|
||||||
|
return 0, errAudioReadEncode
|
||||||
}
|
}
|
||||||
|
|
||||||
// n == 0 case
|
return 0, nil
|
||||||
return 0, nil // No data available
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio playback functions
|
// Audio playback functions
|
||||||
|
@ -972,58 +954,25 @@ func cgoAudioPlaybackClose() {
|
||||||
C.jetkvm_audio_playback_close()
|
C.jetkvm_audio_playback_close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func cgoAudioDecodeWrite(buf []byte) (n int, err error) {
|
func cgoAudioDecodeWrite(buf []byte) (int, error) {
|
||||||
// Fast validation with AudioConfigCache
|
// Minimal validation - assume caller provides correct size
|
||||||
cache := GetCachedConfig()
|
|
||||||
// Only update cache if expired - avoid unnecessary overhead
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimized buffer validation
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return 0, errEmptyBuffer
|
return 0, errEmptyBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use cached max buffer size with atomic access
|
// Direct CGO call - hotpath optimization
|
||||||
maxAllowed := cache.GetMaxDecodeWriteBuffer()
|
n := int(C.jetkvm_audio_decode_write(unsafe.Pointer(&buf[0]), C.int(len(buf))))
|
||||||
if len(buf) > maxAllowed {
|
|
||||||
// Use pre-allocated error for common case
|
|
||||||
if len(buf) == maxAllowed+1 {
|
|
||||||
return 0, cache.GetBufferTooLargeError()
|
|
||||||
}
|
|
||||||
return 0, newBufferTooLargeError(len(buf), maxAllowed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct CGO call with minimal overhead - unsafe.Pointer(&slice[0]) is safe for validated non-empty buffers
|
// Fast path for success
|
||||||
n = int(C.jetkvm_audio_decode_write(unsafe.Pointer(&buf[0]), C.int(len(buf))))
|
|
||||||
|
|
||||||
// Fast path for success case
|
|
||||||
if n >= 0 {
|
if n >= 0 {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle error cases with static error codes
|
// Error handling with static errors
|
||||||
switch n {
|
if n == -1 {
|
||||||
case -1:
|
return 0, errAudioInitFailed
|
||||||
n = 0
|
|
||||||
err = errAudioInitFailed
|
|
||||||
case -2:
|
|
||||||
n = 0
|
|
||||||
err = errAudioDecodeWrite
|
|
||||||
default:
|
|
||||||
n = 0
|
|
||||||
err = newAudioDecodeWriteError(n)
|
|
||||||
}
|
}
|
||||||
return
|
return 0, errAudioDecodeWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateOpusEncoderParams dynamically updates OPUS encoder parameters
|
// updateOpusEncoderParams dynamically updates OPUS encoder parameters
|
||||||
|
@ -1111,77 +1060,22 @@ func DecodeWriteWithPooledBuffer(data []byte) (int, error) {
|
||||||
// BatchReadEncode reads and encodes multiple audio frames in a single batch
|
// BatchReadEncode reads and encodes multiple audio frames in a single batch
|
||||||
// with optimized zero-copy frame management and batch reference counting
|
// with optimized zero-copy frame management and batch reference counting
|
||||||
func BatchReadEncode(batchSize int) ([][]byte, error) {
|
func BatchReadEncode(batchSize int) ([][]byte, error) {
|
||||||
cache := GetCachedConfig()
|
// Simple batch processing without complex overhead
|
||||||
updateCacheIfNeeded(cache)
|
|
||||||
|
|
||||||
// Calculate total buffer size needed for batch
|
|
||||||
frameSize := cache.GetMinReadEncodeBuffer()
|
|
||||||
totalSize := frameSize * batchSize
|
|
||||||
|
|
||||||
// Get a single large buffer for all frames
|
|
||||||
batchBuffer := GetBufferFromPool(totalSize)
|
|
||||||
defer ReturnBufferToPool(batchBuffer)
|
|
||||||
|
|
||||||
// Pre-allocate zero-copy frames for batch processing
|
|
||||||
zeroCopyFrames := make([]*ZeroCopyAudioFrame, 0, batchSize)
|
|
||||||
for i := 0; i < batchSize; i++ {
|
|
||||||
frame := GetZeroCopyFrame()
|
|
||||||
zeroCopyFrames = append(zeroCopyFrames, frame)
|
|
||||||
}
|
|
||||||
// Use batch reference counting for efficient cleanup
|
|
||||||
defer func() {
|
|
||||||
if _, err := BatchReleaseFrames(zeroCopyFrames); err != nil {
|
|
||||||
// Log release error but don't fail the operation
|
|
||||||
_ = err
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Batch AddRef all frames at once to reduce atomic operation overhead
|
|
||||||
err := BatchAddRefFrames(zeroCopyFrames)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track batch processing statistics - only if enabled
|
|
||||||
var startTime time.Time
|
|
||||||
// Batch time tracking removed
|
|
||||||
trackTime := false
|
|
||||||
if trackTime {
|
|
||||||
startTime = time.Now()
|
|
||||||
}
|
|
||||||
batchProcessingCount.Add(1)
|
|
||||||
|
|
||||||
// Process frames in batch using zero-copy frames
|
|
||||||
frames := make([][]byte, 0, batchSize)
|
frames := make([][]byte, 0, batchSize)
|
||||||
for i := 0; i < batchSize; i++ {
|
frameSize := 4096 // Fixed frame size for performance
|
||||||
// Calculate offset for this frame in the batch buffer
|
|
||||||
offset := i * frameSize
|
|
||||||
frameBuf := batchBuffer[offset : offset+frameSize]
|
|
||||||
|
|
||||||
// Process this frame
|
for i := 0; i < batchSize; i++ {
|
||||||
n, err := cgoAudioReadEncode(frameBuf)
|
buf := make([]byte, frameSize)
|
||||||
|
n, err := cgoAudioReadEncode(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Return partial batch on error
|
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
batchFrameCount.Add(int64(i))
|
return frames, nil // Return partial batch
|
||||||
if trackTime {
|
|
||||||
batchProcessingTime.Add(time.Since(startTime).Microseconds())
|
|
||||||
}
|
|
||||||
return frames, nil
|
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if n > 0 {
|
||||||
// Use zero-copy frame for efficient memory management
|
frames = append(frames, buf[:n])
|
||||||
frame := zeroCopyFrames[i]
|
}
|
||||||
frame.SetDataDirect(frameBuf[:n]) // Direct assignment without copy
|
|
||||||
frames = append(frames, frame.Data())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update statistics
|
|
||||||
batchFrameCount.Add(int64(len(frames)))
|
|
||||||
if trackTime {
|
|
||||||
batchProcessingTime.Add(time.Since(startTime).Microseconds())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return frames, nil
|
return frames, nil
|
||||||
|
|
|
@ -29,11 +29,9 @@ func (s *AudioControlService) MuteAudio(muted bool) error {
|
||||||
supervisor := GetAudioOutputSupervisor()
|
supervisor := GetAudioOutputSupervisor()
|
||||||
if supervisor != nil {
|
if supervisor != nil {
|
||||||
supervisor.Stop()
|
supervisor.Stop()
|
||||||
s.logger.Info().Msg("audio output supervisor stopped")
|
|
||||||
}
|
}
|
||||||
StopAudioRelay()
|
StopAudioRelay()
|
||||||
SetAudioMuted(true)
|
SetAudioMuted(true)
|
||||||
s.logger.Info().Msg("audio output muted (subprocess and relay stopped)")
|
|
||||||
} else {
|
} else {
|
||||||
// Unmute: Start audio output subprocess and relay
|
// Unmute: Start audio output subprocess and relay
|
||||||
if !s.sessionProvider.IsSessionActive() {
|
if !s.sessionProvider.IsSessionActive() {
|
||||||
|
@ -44,10 +42,9 @@ func (s *AudioControlService) MuteAudio(muted bool) error {
|
||||||
if supervisor != nil {
|
if supervisor != nil {
|
||||||
err := supervisor.Start()
|
err := supervisor.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error().Err(err).Msg("failed to start audio output supervisor during unmute")
|
s.logger.Debug().Err(err).Msg("failed to start audio output supervisor")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.logger.Info().Msg("audio output supervisor started")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start audio relay
|
// Start audio relay
|
||||||
|
|
|
@ -688,32 +688,28 @@ func (aic *AudioInputClient) Disconnect() {
|
||||||
|
|
||||||
// SendFrame sends an Opus frame to the audio input server
|
// SendFrame sends an Opus frame to the audio input server
|
||||||
func (aic *AudioInputClient) SendFrame(frame []byte) error {
|
func (aic *AudioInputClient) SendFrame(frame []byte) error {
|
||||||
|
// Fast path validation
|
||||||
|
if len(frame) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
aic.mtx.Lock()
|
aic.mtx.Lock()
|
||||||
defer aic.mtx.Unlock()
|
|
||||||
|
|
||||||
if !aic.running || aic.conn == nil {
|
if !aic.running || aic.conn == nil {
|
||||||
return fmt.Errorf("not connected to audio input server")
|
aic.mtx.Unlock()
|
||||||
}
|
return fmt.Errorf("not connected")
|
||||||
|
|
||||||
frameLen := len(frame)
|
|
||||||
if frameLen == 0 {
|
|
||||||
return nil // Empty frame, ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inline frame validation to reduce function call overhead
|
|
||||||
if frameLen > maxFrameSize {
|
|
||||||
return ErrFrameDataTooLarge
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direct message creation without timestamp overhead
|
||||||
msg := &InputIPCMessage{
|
msg := &InputIPCMessage{
|
||||||
Magic: inputMagicNumber,
|
Magic: inputMagicNumber,
|
||||||
Type: InputMessageTypeOpusFrame,
|
Type: InputMessageTypeOpusFrame,
|
||||||
Length: uint32(frameLen),
|
Length: uint32(len(frame)),
|
||||||
Timestamp: time.Now().UnixNano(),
|
Data: frame,
|
||||||
Data: frame,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return aic.writeMessage(msg)
|
err := aic.writeMessage(msg)
|
||||||
|
aic.mtx.Unlock()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendFrameZeroCopy sends a zero-copy Opus frame to the audio input server
|
// SendFrameZeroCopy sends a zero-copy Opus frame to the audio input server
|
||||||
|
|
|
@ -312,7 +312,6 @@ func SetMicrophoneQuality(quality AudioQuality) {
|
||||||
|
|
||||||
// Update audio input subprocess configuration dynamically without restart
|
// Update audio input subprocess configuration dynamically without restart
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
||||||
logger.Info().Int("quality", int(quality)).Msg("updating audio input quality settings dynamically")
|
|
||||||
|
|
||||||
// Set new OPUS configuration for future restarts
|
// Set new OPUS configuration for future restarts
|
||||||
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
|
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
|
||||||
|
@ -321,12 +320,11 @@ func SetMicrophoneQuality(quality AudioQuality) {
|
||||||
// Check if microphone is active but IPC control is broken
|
// Check if microphone is active but IPC control is broken
|
||||||
inputManager := getAudioInputManager()
|
inputManager := getAudioInputManager()
|
||||||
if inputManager.IsRunning() && !supervisor.IsConnected() {
|
if inputManager.IsRunning() && !supervisor.IsConnected() {
|
||||||
logger.Info().Msg("microphone active but IPC disconnected, attempting to reconnect control channel")
|
|
||||||
// Reconnect the IPC control channel
|
// Reconnect the IPC control channel
|
||||||
supervisor.Stop()
|
supervisor.Stop()
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
if err := supervisor.Start(); err != nil {
|
if err := supervisor.Start(); err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to reconnect IPC control channel")
|
logger.Debug().Err(err).Msg("failed to reconnect IPC control channel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,9 +343,8 @@ func SetMicrophoneQuality(quality AudioQuality) {
|
||||||
DTX: dtx,
|
DTX: dtx,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Interface("opusConfig", opusConfig).Msg("sending Opus configuration to audio input subprocess")
|
|
||||||
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
|
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update via IPC, falling back to subprocess restart")
|
logger.Debug().Err(err).Msg("failed to send dynamic Opus config update via IPC")
|
||||||
// Fallback to subprocess restart if IPC update fails
|
// Fallback to subprocess restart if IPC update fails
|
||||||
supervisor.Stop()
|
supervisor.Stop()
|
||||||
if err := supervisor.Start(); err != nil {
|
if err := supervisor.Start(); err != nil {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
|
@ -118,9 +119,7 @@ func (r *AudioRelay) IsMuted() bool {
|
||||||
|
|
||||||
// GetStats returns relay statistics
|
// GetStats returns relay statistics
|
||||||
func (r *AudioRelay) GetStats() (framesRelayed, framesDropped int64) {
|
func (r *AudioRelay) GetStats() (framesRelayed, framesDropped int64) {
|
||||||
r.mutex.RLock()
|
return atomic.LoadInt64(&r.framesRelayed), atomic.LoadInt64(&r.framesDropped)
|
||||||
defer r.mutex.RUnlock()
|
|
||||||
return r.framesRelayed, r.framesDropped
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTrack updates the WebRTC audio track for the relay
|
// UpdateTrack updates the WebRTC audio track for the relay
|
||||||
|
@ -132,34 +131,43 @@ func (r *AudioRelay) UpdateTrack(audioTrack AudioTrackWriter) {
|
||||||
|
|
||||||
func (r *AudioRelay) relayLoop() {
|
func (r *AudioRelay) relayLoop() {
|
||||||
defer r.wg.Done()
|
defer r.wg.Done()
|
||||||
r.logger.Debug().Msg("Audio relay loop started")
|
|
||||||
|
|
||||||
var maxConsecutiveErrors = Config.MaxConsecutiveErrors
|
var maxConsecutiveErrors = Config.MaxConsecutiveErrors
|
||||||
consecutiveErrors := 0
|
consecutiveErrors := 0
|
||||||
|
backoffDelay := time.Millisecond * 10
|
||||||
|
maxBackoff := time.Second * 5
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-r.ctx.Done():
|
case <-r.ctx.Done():
|
||||||
r.logger.Debug().Msg("audio relay loop stopping")
|
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
frame, err := r.client.ReceiveFrame()
|
frame, err := r.client.ReceiveFrame()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
consecutiveErrors++
|
consecutiveErrors++
|
||||||
r.logger.Error().Err(err).Int("consecutive_errors", consecutiveErrors).Msg("error reading frame from audio output server")
|
|
||||||
r.incrementDropped()
|
r.incrementDropped()
|
||||||
|
|
||||||
|
// Exponential backoff for stability
|
||||||
if consecutiveErrors >= maxConsecutiveErrors {
|
if consecutiveErrors >= maxConsecutiveErrors {
|
||||||
r.logger.Error().Int("consecutive_errors", consecutiveErrors).Int("max_errors", maxConsecutiveErrors).Msg("too many consecutive read errors, stopping audio relay")
|
// Attempt reconnection
|
||||||
|
if r.attemptReconnection() {
|
||||||
|
consecutiveErrors = 0
|
||||||
|
backoffDelay = time.Millisecond * 10
|
||||||
|
continue
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(Config.ShortSleepDuration)
|
|
||||||
|
time.Sleep(backoffDelay)
|
||||||
|
if backoffDelay < maxBackoff {
|
||||||
|
backoffDelay *= 2
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
consecutiveErrors = 0
|
consecutiveErrors = 0
|
||||||
|
backoffDelay = time.Millisecond * 10
|
||||||
if err := r.forwardToWebRTC(frame); err != nil {
|
if err := r.forwardToWebRTC(frame); err != nil {
|
||||||
r.logger.Warn().Err(err).Msg("failed to forward frame to webrtc")
|
|
||||||
r.incrementDropped()
|
r.incrementDropped()
|
||||||
} else {
|
} else {
|
||||||
r.incrementRelayed()
|
r.incrementRelayed()
|
||||||
|
@ -218,14 +226,24 @@ func (r *AudioRelay) forwardToWebRTC(frame []byte) error {
|
||||||
|
|
||||||
// incrementRelayed atomically increments the relayed frames counter
|
// incrementRelayed atomically increments the relayed frames counter
|
||||||
func (r *AudioRelay) incrementRelayed() {
|
func (r *AudioRelay) incrementRelayed() {
|
||||||
r.mutex.Lock()
|
atomic.AddInt64(&r.framesRelayed, 1)
|
||||||
r.framesRelayed++
|
|
||||||
r.mutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// incrementDropped atomically increments the dropped frames counter
|
// incrementDropped atomically increments the dropped frames counter
|
||||||
func (r *AudioRelay) incrementDropped() {
|
func (r *AudioRelay) incrementDropped() {
|
||||||
r.mutex.Lock()
|
atomic.AddInt64(&r.framesDropped, 1)
|
||||||
r.framesDropped++
|
}
|
||||||
r.mutex.Unlock()
|
|
||||||
|
// attemptReconnection tries to reconnect the audio client for stability
|
||||||
|
func (r *AudioRelay) attemptReconnection() bool {
|
||||||
|
if r.client == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect and reconnect
|
||||||
|
r.client.Disconnect()
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
|
||||||
|
err := r.client.Connect()
|
||||||
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue