mirror of https://github.com/jetkvm/kvm.git
refactor(audio): centralize configuration constants in audio module
Replace hardcoded values with centralized config constants for better maintainability and flexibility. This includes sleep durations, buffer sizes, thresholds, and various audio processing parameters. The changes affect multiple components including buffer pools, latency monitoring, IPC, and audio processing. This refactoring makes it easier to adjust parameters without modifying individual files. Key changes: - Replace hardcoded sleep durations with config values - Centralize buffer sizes and pool configurations - Move thresholds and limits to config - Update audio quality presets to use config values
This commit is contained in:
parent
7ec583ed6a
commit
35a666ed31
|
@ -42,20 +42,20 @@ func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
|
||||||
DefaultBufferSize: 6, // Default 6 frames (increased for better stability)
|
DefaultBufferSize: 6, // Default 6 frames (increased for better stability)
|
||||||
|
|
||||||
// CPU thresholds optimized for single-core ARM Cortex A7 under load
|
// CPU thresholds optimized for single-core ARM Cortex A7 under load
|
||||||
LowCPUThreshold: 20.0, // Below 20% CPU
|
LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU
|
||||||
HighCPUThreshold: 60.0, // Above 60% CPU (lowered to be more responsive)
|
HighCPUThreshold: GetConfig().HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive)
|
||||||
|
|
||||||
// Memory thresholds for 256MB total RAM
|
// Memory thresholds for 256MB total RAM
|
||||||
LowMemoryThreshold: 35.0, // Below 35% memory usage
|
LowMemoryThreshold: GetConfig().LowMemoryThreshold * 100, // Below 35% memory usage
|
||||||
HighMemoryThreshold: 75.0, // Above 75% memory usage (lowered for earlier response)
|
HighMemoryThreshold: GetConfig().HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response)
|
||||||
|
|
||||||
// Latency targets
|
// Latency targets
|
||||||
TargetLatency: 20 * time.Millisecond, // Target 20ms latency
|
TargetLatency: GetConfig().TargetLatency, // Target 20ms latency
|
||||||
MaxLatency: 50 * time.Millisecond, // Max acceptable 50ms
|
MaxLatency: GetConfig().MaxLatencyTarget, // Max acceptable latency
|
||||||
|
|
||||||
// Adaptation settings
|
// Adaptation settings
|
||||||
AdaptationInterval: 500 * time.Millisecond, // Check every 500ms
|
AdaptationInterval: GetConfig().BufferUpdateInterval, // Check every 500ms
|
||||||
SmoothingFactor: 0.3, // Moderate responsiveness
|
SmoothingFactor: GetConfig().SmoothingFactor, // Moderate responsiveness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) {
|
||||||
atomic.StoreInt64(&abm.averageLatency, newLatency)
|
atomic.StoreInt64(&abm.averageLatency, newLatency)
|
||||||
} else {
|
} else {
|
||||||
// Exponential moving average: 70% historical, 30% current
|
// Exponential moving average: 70% historical, 30% current
|
||||||
newAvg := int64(float64(currentAvg)*0.7 + float64(newLatency)*0.3)
|
newAvg := int64(float64(currentAvg)*GetConfig().HistoricalWeight + float64(newLatency)*GetConfig().CurrentWeight)
|
||||||
atomic.StoreInt64(&abm.averageLatency, newAvg)
|
atomic.StoreInt64(&abm.averageLatency, newAvg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,7 @@ func (abm *AdaptiveBufferManager) adaptBufferSizes() {
|
||||||
latencyFactor := abm.calculateLatencyFactor(currentLatency)
|
latencyFactor := abm.calculateLatencyFactor(currentLatency)
|
||||||
|
|
||||||
// Combine factors with weights (CPU has highest priority for KVM coexistence)
|
// Combine factors with weights (CPU has highest priority for KVM coexistence)
|
||||||
combinedFactor := 0.5*cpuFactor + 0.3*memoryFactor + 0.2*latencyFactor
|
combinedFactor := GetConfig().CPUMemoryWeight*cpuFactor + GetConfig().MemoryWeight*memoryFactor + GetConfig().LatencyWeight*latencyFactor
|
||||||
|
|
||||||
// Apply adaptation with smoothing
|
// Apply adaptation with smoothing
|
||||||
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
|
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
|
||||||
|
@ -306,8 +306,8 @@ func (abm *AdaptiveBufferManager) GetStats() map[string]interface{} {
|
||||||
"input_buffer_size": abm.GetInputBufferSize(),
|
"input_buffer_size": abm.GetInputBufferSize(),
|
||||||
"output_buffer_size": abm.GetOutputBufferSize(),
|
"output_buffer_size": abm.GetOutputBufferSize(),
|
||||||
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
|
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
|
||||||
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / 100,
|
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / GetConfig().PercentageMultiplier,
|
||||||
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / 100,
|
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / GetConfig().PercentageMultiplier,
|
||||||
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
|
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
|
||||||
"last_adaptation": lastAdaptation,
|
"last_adaptation": lastAdaptation,
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,9 +42,9 @@ type OptimizerConfig struct {
|
||||||
func DefaultOptimizerConfig() OptimizerConfig {
|
func DefaultOptimizerConfig() OptimizerConfig {
|
||||||
return OptimizerConfig{
|
return OptimizerConfig{
|
||||||
MaxOptimizationLevel: 8,
|
MaxOptimizationLevel: 8,
|
||||||
CooldownPeriod: 30 * time.Second,
|
CooldownPeriod: GetConfig().CooldownPeriod,
|
||||||
Aggressiveness: 0.7,
|
Aggressiveness: GetConfig().OptimizerAggressiveness,
|
||||||
RollbackThreshold: 300 * time.Millisecond,
|
RollbackThreshold: GetConfig().RollbackThreshold,
|
||||||
StabilityPeriod: 10 * time.Second,
|
StabilityPeriod: 10 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ func (ao *AdaptiveOptimizer) handleLatencyOptimization(metrics LatencyMetrics) e
|
||||||
// calculateTargetOptimizationLevel determines the appropriate optimization level
|
// calculateTargetOptimizationLevel determines the appropriate optimization level
|
||||||
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
|
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
|
||||||
// Base calculation on current latency vs target
|
// Base calculation on current latency vs target
|
||||||
latencyRatio := float64(metrics.Current) / float64(50*time.Millisecond) // 50ms target
|
latencyRatio := float64(metrics.Current) / float64(GetConfig().LatencyTarget) // 50ms target
|
||||||
|
|
||||||
// Adjust based on trend
|
// Adjust based on trend
|
||||||
switch metrics.Trend {
|
switch metrics.Trend {
|
||||||
|
@ -125,7 +125,7 @@ func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMet
|
||||||
latencyRatio *= ao.config.Aggressiveness
|
latencyRatio *= ao.config.Aggressiveness
|
||||||
|
|
||||||
// Convert to optimization level
|
// Convert to optimization level
|
||||||
targetLevel := int64(latencyRatio * 2) // Scale to 0-10 range
|
targetLevel := int64(latencyRatio * GetConfig().LatencyScalingFactor) // Scale to 0-10 range
|
||||||
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
|
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
|
||||||
targetLevel = int64(ao.config.MaxOptimizationLevel)
|
targetLevel = int64(ao.config.MaxOptimizationLevel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,17 +47,17 @@ type AudioMetrics struct {
|
||||||
var (
|
var (
|
||||||
currentConfig = AudioConfig{
|
currentConfig = AudioConfig{
|
||||||
Quality: AudioQualityMedium,
|
Quality: AudioQualityMedium,
|
||||||
Bitrate: 64,
|
Bitrate: GetConfig().AudioQualityMediumOutputBitrate,
|
||||||
SampleRate: GetConfig().SampleRate,
|
SampleRate: GetConfig().SampleRate,
|
||||||
Channels: GetConfig().Channels,
|
Channels: GetConfig().Channels,
|
||||||
FrameSize: 20 * time.Millisecond,
|
FrameSize: GetConfig().AudioQualityMediumFrameSize,
|
||||||
}
|
}
|
||||||
currentMicrophoneConfig = AudioConfig{
|
currentMicrophoneConfig = AudioConfig{
|
||||||
Quality: AudioQualityMedium,
|
Quality: AudioQualityMedium,
|
||||||
Bitrate: 32,
|
Bitrate: GetConfig().AudioQualityMediumInputBitrate,
|
||||||
SampleRate: GetConfig().SampleRate,
|
SampleRate: GetConfig().SampleRate,
|
||||||
Channels: 1,
|
Channels: 1,
|
||||||
FrameSize: 20 * time.Millisecond,
|
FrameSize: GetConfig().AudioQualityMediumFrameSize,
|
||||||
}
|
}
|
||||||
metrics AudioMetrics
|
metrics AudioMetrics
|
||||||
)
|
)
|
||||||
|
@ -69,24 +69,24 @@ var qualityPresets = map[AudioQuality]struct {
|
||||||
frameSize time.Duration
|
frameSize time.Duration
|
||||||
}{
|
}{
|
||||||
AudioQualityLow: {
|
AudioQualityLow: {
|
||||||
outputBitrate: 32, inputBitrate: 16,
|
outputBitrate: GetConfig().AudioQualityLowOutputBitrate, inputBitrate: GetConfig().AudioQualityLowInputBitrate,
|
||||||
sampleRate: 22050, channels: 1,
|
sampleRate: GetConfig().AudioQualityLowSampleRate, channels: GetConfig().AudioQualityLowChannels,
|
||||||
frameSize: 40 * time.Millisecond,
|
frameSize: GetConfig().AudioQualityLowFrameSize,
|
||||||
},
|
},
|
||||||
AudioQualityMedium: {
|
AudioQualityMedium: {
|
||||||
outputBitrate: 64, inputBitrate: 32,
|
outputBitrate: GetConfig().AudioQualityMediumOutputBitrate, inputBitrate: GetConfig().AudioQualityMediumInputBitrate,
|
||||||
sampleRate: 44100, channels: 2,
|
sampleRate: GetConfig().AudioQualityMediumSampleRate, channels: GetConfig().AudioQualityMediumChannels,
|
||||||
frameSize: 20 * time.Millisecond,
|
frameSize: GetConfig().AudioQualityMediumFrameSize,
|
||||||
},
|
},
|
||||||
AudioQualityHigh: {
|
AudioQualityHigh: {
|
||||||
outputBitrate: 128, inputBitrate: 64,
|
outputBitrate: GetConfig().AudioQualityHighOutputBitrate, inputBitrate: GetConfig().AudioQualityHighInputBitrate,
|
||||||
sampleRate: GetConfig().SampleRate, channels: GetConfig().Channels,
|
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityHighChannels,
|
||||||
frameSize: 20 * time.Millisecond,
|
frameSize: GetConfig().AudioQualityHighFrameSize,
|
||||||
},
|
},
|
||||||
AudioQualityUltra: {
|
AudioQualityUltra: {
|
||||||
outputBitrate: 192, inputBitrate: 96,
|
outputBitrate: GetConfig().AudioQualityUltraOutputBitrate, inputBitrate: GetConfig().AudioQualityUltraInputBitrate,
|
||||||
sampleRate: GetConfig().SampleRate, channels: GetConfig().Channels,
|
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityUltraChannels,
|
||||||
frameSize: 10 * time.Millisecond,
|
frameSize: GetConfig().AudioQualityUltraFrameSize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
|
||||||
Bitrate: preset.inputBitrate,
|
Bitrate: preset.inputBitrate,
|
||||||
SampleRate: func() int {
|
SampleRate: func() int {
|
||||||
if quality == AudioQualityLow {
|
if quality == AudioQualityLow {
|
||||||
return 16000
|
return GetConfig().AudioQualityMicLowSampleRate
|
||||||
}
|
}
|
||||||
return preset.sampleRate
|
return preset.sampleRate
|
||||||
}(),
|
}(),
|
||||||
|
|
|
@ -72,7 +72,7 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
|
||||||
readQueue: make(chan batchReadRequest, batchSize*2),
|
readQueue: make(chan batchReadRequest, batchSize*2),
|
||||||
readBufPool: &sync.Pool{
|
readBufPool: &sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
return make([]byte, 1500) // Max audio frame size
|
return make([]byte, GetConfig().AudioFramePoolSize) // Max audio frame size
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ func (bap *BatchAudioProcessor) Stop() {
|
||||||
bap.cancel()
|
bap.cancel()
|
||||||
|
|
||||||
// Wait for processing to complete
|
// Wait for processing to complete
|
||||||
time.Sleep(bap.batchDuration + 10*time.Millisecond)
|
time.Sleep(bap.batchDuration + GetConfig().BatchProcessingDelay)
|
||||||
|
|
||||||
bap.logger.Info().Msg("batch audio processor stopped")
|
bap.logger.Info().Msg("batch audio processor stopped")
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
|
||||||
select {
|
select {
|
||||||
case bap.readQueue <- request:
|
case bap.readQueue <- request:
|
||||||
// Successfully queued
|
// Successfully queued
|
||||||
case <-time.After(5 * time.Millisecond):
|
case <-time.After(GetConfig().ShortTimeout):
|
||||||
// Queue is full or blocked, fallback to single operation
|
// Queue is full or blocked, fallback to single operation
|
||||||
atomic.AddInt64(&bap.stats.SingleReads, 1)
|
atomic.AddInt64(&bap.stats.SingleReads, 1)
|
||||||
atomic.AddInt64(&bap.stats.SingleFrames, 1)
|
atomic.AddInt64(&bap.stats.SingleFrames, 1)
|
||||||
|
@ -145,7 +145,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
|
||||||
select {
|
select {
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
return result.length, result.err
|
return result.length, result.err
|
||||||
case <-time.After(50 * time.Millisecond):
|
case <-time.After(GetConfig().MediumTimeout):
|
||||||
// Timeout, fallback to single operation
|
// Timeout, fallback to single operation
|
||||||
atomic.AddInt64(&bap.stats.SingleReads, 1)
|
atomic.AddInt64(&bap.stats.SingleReads, 1)
|
||||||
atomic.AddInt64(&bap.stats.SingleFrames, 1)
|
atomic.AddInt64(&bap.stats.SingleFrames, 1)
|
||||||
|
|
|
@ -23,7 +23,7 @@ type AudioBufferPool struct {
|
||||||
|
|
||||||
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||||
// Pre-allocate 20% of max pool size for immediate availability
|
// Pre-allocate 20% of max pool size for immediate availability
|
||||||
preallocSize := 20
|
preallocSize := GetConfig().PreallocPercentage
|
||||||
preallocated := make([]*[]byte, 0, preallocSize)
|
preallocated := make([]*[]byte, 0, preallocSize)
|
||||||
|
|
||||||
// Pre-allocate buffers to reduce initial allocation overhead
|
// Pre-allocate buffers to reduce initial allocation overhead
|
||||||
|
@ -34,7 +34,7 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||||
|
|
||||||
return &AudioBufferPool{
|
return &AudioBufferPool{
|
||||||
bufferSize: bufferSize,
|
bufferSize: bufferSize,
|
||||||
maxPoolSize: 100, // Limit pool size to prevent excessive memory usage
|
maxPoolSize: GetConfig().MaxPoolSize, // Limit pool size to prevent excessive memory usage
|
||||||
preallocated: preallocated,
|
preallocated: preallocated,
|
||||||
preallocSize: preallocSize,
|
preallocSize: preallocSize,
|
||||||
pool: sync.Pool{
|
pool: sync.Pool{
|
||||||
|
@ -111,8 +111,8 @@ func (p *AudioBufferPool) Put(buf []byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
audioFramePool = NewAudioBufferPool(1500)
|
audioFramePool = NewAudioBufferPool(GetConfig().AudioFramePoolSize)
|
||||||
audioControlPool = NewAudioBufferPool(64)
|
audioControlPool = NewAudioBufferPool(GetConfig().OutputHeaderSize)
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAudioFrameBuffer() []byte {
|
func GetAudioFrameBuffer() []byte {
|
||||||
|
@ -144,7 +144,7 @@ func (p *AudioBufferPool) GetPoolStats() AudioBufferPoolDetailedStats {
|
||||||
|
|
||||||
var hitRate float64
|
var hitRate float64
|
||||||
if totalRequests > 0 {
|
if totalRequests > 0 {
|
||||||
hitRate = float64(hitCount) / float64(totalRequests) * 100
|
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
return AudioBufferPoolDetailedStats{
|
return AudioBufferPoolDetailedStats{
|
||||||
|
|
|
@ -22,18 +22,37 @@ static snd_pcm_t *pcm_handle = NULL;
|
||||||
static snd_pcm_t *pcm_playback_handle = NULL;
|
static snd_pcm_t *pcm_playback_handle = NULL;
|
||||||
static OpusEncoder *encoder = NULL;
|
static OpusEncoder *encoder = NULL;
|
||||||
static OpusDecoder *decoder = NULL;
|
static OpusDecoder *decoder = NULL;
|
||||||
// Optimized Opus encoder settings for ARM Cortex-A7
|
// Opus encoder settings - initialized from Go configuration
|
||||||
static int opus_bitrate = 96000; // Increased for better quality
|
static int opus_bitrate = 96000; // Will be set from GetConfig().CGOOpusBitrate
|
||||||
static int opus_complexity = 3; // Reduced for ARM performance
|
static int opus_complexity = 3; // Will be set from GetConfig().CGOOpusComplexity
|
||||||
static int opus_vbr = 1; // Variable bitrate enabled
|
static int opus_vbr = 1; // Will be set from GetConfig().CGOOpusVBR
|
||||||
static int opus_vbr_constraint = 1; // Constrained VBR for consistent latency
|
static int opus_vbr_constraint = 1; // Will be set from GetConfig().CGOOpusVBRConstraint
|
||||||
static int opus_signal_type = OPUS_SIGNAL_MUSIC; // Optimized for general audio
|
static int opus_signal_type = 3; // Will be set from GetConfig().CGOOpusSignalType
|
||||||
static int opus_bandwidth = OPUS_BANDWIDTH_FULLBAND; // Full bandwidth
|
static int opus_bandwidth = 1105; // Will be set from GetConfig().CGOOpusBandwidth
|
||||||
static int opus_dtx = 0; // Disable DTX for real-time audio
|
static int opus_dtx = 0; // Will be set from GetConfig().CGOOpusDTX
|
||||||
static int sample_rate = 48000;
|
static int sample_rate = 48000; // Will be set from GetConfig().CGOSampleRate
|
||||||
static int channels = 2;
|
static int channels = 2; // Will be set from GetConfig().CGOChannels
|
||||||
static int frame_size = 960; // 20ms for 48kHz
|
static int frame_size = 960; // Will be set from GetConfig().CGOFrameSize
|
||||||
static int max_packet_size = 1500;
|
static int max_packet_size = 1500; // Will be set from GetConfig().CGOMaxPacketSize
|
||||||
|
static int sleep_microseconds = 50000; // Will be set from GetConfig().CGOSleepMicroseconds
|
||||||
|
|
||||||
|
// Function to update constants from Go configuration
|
||||||
|
void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint,
|
||||||
|
int signal_type, int bandwidth, int dtx, int sr, int ch,
|
||||||
|
int fs, int max_pkt, int sleep_us) {
|
||||||
|
opus_bitrate = bitrate;
|
||||||
|
opus_complexity = complexity;
|
||||||
|
opus_vbr = vbr;
|
||||||
|
opus_vbr_constraint = vbr_constraint;
|
||||||
|
opus_signal_type = signal_type;
|
||||||
|
opus_bandwidth = bandwidth;
|
||||||
|
opus_dtx = dtx;
|
||||||
|
sample_rate = sr;
|
||||||
|
channels = ch;
|
||||||
|
frame_size = fs;
|
||||||
|
max_packet_size = max_pkt;
|
||||||
|
sleep_microseconds = sleep_us;
|
||||||
|
}
|
||||||
|
|
||||||
// State tracking to prevent race conditions during rapid start/stop
|
// State tracking to prevent race conditions during rapid start/stop
|
||||||
static volatile int capture_initializing = 0;
|
static volatile int capture_initializing = 0;
|
||||||
|
@ -56,7 +75,7 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
|
||||||
|
|
||||||
if (err == -EBUSY && attempts > 0) {
|
if (err == -EBUSY && attempts > 0) {
|
||||||
// Device busy, wait and retry
|
// Device busy, wait and retry
|
||||||
usleep(50000); // 50ms
|
usleep(sleep_microseconds); // 50ms
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -415,8 +434,25 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func cgoAudioInit() error {
|
func cgoAudioInit() error {
|
||||||
ret := C.jetkvm_audio_init()
|
// Update C constants from Go configuration
|
||||||
if ret != 0 {
|
config := GetConfig()
|
||||||
|
C.update_audio_constants(
|
||||||
|
C.int(config.CGOOpusBitrate),
|
||||||
|
C.int(config.CGOOpusComplexity),
|
||||||
|
C.int(config.CGOOpusVBR),
|
||||||
|
C.int(config.CGOOpusVBRConstraint),
|
||||||
|
C.int(config.CGOOpusSignalType),
|
||||||
|
C.int(config.CGOOpusBandwidth),
|
||||||
|
C.int(config.CGOOpusDTX),
|
||||||
|
C.int(config.CGOSampleRate),
|
||||||
|
C.int(config.CGOChannels),
|
||||||
|
C.int(config.CGOFrameSize),
|
||||||
|
C.int(config.CGOMaxPacketSize),
|
||||||
|
C.int(config.CGOSleepMicroseconds),
|
||||||
|
)
|
||||||
|
|
||||||
|
result := C.jetkvm_audio_init()
|
||||||
|
if result != 0 {
|
||||||
return errAudioInitFailed
|
return errAudioInitFailed
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -427,7 +463,7 @@ func cgoAudioClose() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cgoAudioReadEncode(buf []byte) (int, error) {
|
func cgoAudioReadEncode(buf []byte) (int, error) {
|
||||||
if len(buf) < 1276 {
|
if len(buf) < GetConfig().MinReadEncodeBuffer {
|
||||||
return 0, errBufferTooSmall
|
return 0, errBufferTooSmall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,7 +497,7 @@ func cgoAudioDecodeWrite(buf []byte) (int, error) {
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
return 0, errNilBuffer
|
return 0, errNilBuffer
|
||||||
}
|
}
|
||||||
if len(buf) > 4096 {
|
if len(buf) > GetConfig().MaxDecodeWriteBuffer {
|
||||||
return 0, errBufferTooLarge
|
return 0, errBufferTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -16,14 +16,19 @@ import (
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
inputMagicNumber uint32 = 0x4A4B4D49 // "JKMI" (JetKVM Microphone Input)
|
inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input)
|
||||||
inputSocketName = "audio_input.sock"
|
inputSocketName = "audio_input.sock"
|
||||||
maxFrameSize = 4096 // Maximum Opus frame size
|
|
||||||
writeTimeout = 15 * time.Millisecond // Non-blocking write timeout (increased for high load)
|
writeTimeout = 15 * time.Millisecond // Non-blocking write timeout (increased for high load)
|
||||||
maxDroppedFrames = 100 // Maximum consecutive dropped frames before reconnect
|
)
|
||||||
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
|
|
||||||
messagePoolSize = 256 // Pre-allocated message pool size
|
const (
|
||||||
|
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
maxFrameSize = GetConfig().MaxFrameSize // Maximum Opus frame size
|
||||||
|
messagePoolSize = GetConfig().MessagePoolSize // Pre-allocated message pool size
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputMessageType represents the type of IPC message
|
// InputMessageType represents the type of IPC message
|
||||||
|
@ -79,9 +84,9 @@ var messagePoolInitOnce sync.Once
|
||||||
func initializeMessagePool() {
|
func initializeMessagePool() {
|
||||||
messagePoolInitOnce.Do(func() {
|
messagePoolInitOnce.Do(func() {
|
||||||
// Pre-allocate 30% of pool size for immediate availability
|
// Pre-allocate 30% of pool size for immediate availability
|
||||||
preallocSize := messagePoolSize * 30 / 100
|
preallocSize := messagePoolSize * GetConfig().InputPreallocPercentage / 100
|
||||||
globalMessagePool.preallocSize = preallocSize
|
globalMessagePool.preallocSize = preallocSize
|
||||||
globalMessagePool.maxPoolSize = messagePoolSize * 2 // Allow growth up to 2x
|
globalMessagePool.maxPoolSize = messagePoolSize * GetConfig().PoolGrowthMultiplier // Allow growth up to 2x
|
||||||
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
|
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
|
||||||
|
|
||||||
// Pre-allocate messages to reduce initial allocation overhead
|
// Pre-allocate messages to reduce initial allocation overhead
|
||||||
|
@ -315,7 +320,7 @@ func (ais *AudioInputServer) handleConnection(conn net.Conn) {
|
||||||
if ais.conn == nil {
|
if ais.conn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(GetConfig().DefaultSleepDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -345,7 +350,7 @@ func (ais *AudioInputServer) readMessage(conn net.Conn) (*InputIPCMessage, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate message length
|
// Validate message length
|
||||||
if msg.Length > maxFrameSize {
|
if msg.Length > uint32(maxFrameSize) {
|
||||||
return nil, fmt.Errorf("message too large: %d bytes", msg.Length)
|
return nil, fmt.Errorf("message too large: %d bytes", msg.Length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -711,7 +716,7 @@ func (aic *AudioInputClient) GetDropRate() float64 {
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
return float64(dropped) / float64(total) * 100.0
|
return float64(dropped) / float64(total) * GetConfig().PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetStats resets frame statistics
|
// ResetStats resets frame statistics
|
||||||
|
@ -820,11 +825,11 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
defer ais.wg.Done()
|
defer ais.wg.Done()
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
ticker := time.NewTicker(GetConfig().DefaultTickerInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// Buffer size update ticker (less frequent)
|
// Buffer size update ticker (less frequent)
|
||||||
bufferUpdateTicker := time.NewTicker(500 * time.Millisecond)
|
bufferUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
|
||||||
defer bufferUpdateTicker.Stop()
|
defer bufferUpdateTicker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -917,7 +922,7 @@ func (mp *MessagePool) GetMessagePoolStats() MessagePoolStats {
|
||||||
|
|
||||||
var hitRate float64
|
var hitRate float64
|
||||||
if totalRequests > 0 {
|
if totalRequests > 0 {
|
||||||
hitRate = float64(hitCount) / float64(totalRequests) * 100
|
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate channel pool size
|
// Calculate channel pool size
|
||||||
|
|
|
@ -41,13 +41,13 @@ func (aim *AudioInputIPCManager) Start() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
config := InputIPCConfig{
|
config := InputIPCConfig{
|
||||||
SampleRate: 48000,
|
SampleRate: GetConfig().InputIPCSampleRate,
|
||||||
Channels: 2,
|
Channels: GetConfig().InputIPCChannels,
|
||||||
FrameSize: 960,
|
FrameSize: GetConfig().InputIPCFrameSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for subprocess readiness
|
// Wait for subprocess readiness
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(GetConfig().LongSleepDuration)
|
||||||
|
|
||||||
err = aim.supervisor.SendConfig(config)
|
err = aim.supervisor.SendConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -64,7 +64,7 @@ func RunAudioInputServer() error {
|
||||||
server.Stop()
|
server.Stop()
|
||||||
|
|
||||||
// Give some time for cleanup
|
// Give some time for cleanup
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(GetConfig().DefaultSleepDuration)
|
||||||
|
|
||||||
logger.Info().Msg("Audio input server subprocess stopped")
|
logger.Info().Msg("Audio input server subprocess stopped")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -128,7 +128,7 @@ func (ais *AudioInputSupervisor) Stop() {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
|
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(GetConfig().InputSupervisorTimeout):
|
||||||
// Force kill if graceful shutdown failed
|
// Force kill if graceful shutdown failed
|
||||||
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
|
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
|
||||||
err := ais.cmd.Process.Kill()
|
err := ais.cmd.Process.Kill()
|
||||||
|
@ -220,7 +220,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
|
||||||
// connectClient attempts to connect the client to the server
|
// connectClient attempts to connect the client to the server
|
||||||
func (ais *AudioInputSupervisor) connectClient() {
|
func (ais *AudioInputSupervisor) connectClient() {
|
||||||
// Wait briefly for the server to start (reduced from 500ms)
|
// Wait briefly for the server to start (reduced from 500ms)
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(GetConfig().DefaultSleepDuration)
|
||||||
|
|
||||||
err := ais.client.Connect()
|
err := ais.client.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -16,16 +16,14 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
outputMagicNumber uint32 = 0x4A4B4F55 // "JKOU" (JetKVM Output)
|
outputMagicNumber uint32 = GetConfig().OutputMagicNumber // "JKOU" (JetKVM Output)
|
||||||
outputSocketName = "audio_output.sock"
|
outputSocketName = "audio_output.sock"
|
||||||
outputMaxFrameSize = 4096 // Maximum Opus frame size
|
|
||||||
outputWriteTimeout = 10 * time.Millisecond // Non-blocking write timeout (increased for high load)
|
|
||||||
outputMaxDroppedFrames = 50 // Maximum consecutive dropped frames
|
|
||||||
outputHeaderSize = 17 // Fixed header size: 4+1+4+8 bytes
|
|
||||||
outputMessagePoolSize = 128 // Pre-allocated message pool size
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Output IPC constants are now centralized in config_constants.go
|
||||||
|
// outputMaxFrameSize, outputWriteTimeout, outputMaxDroppedFrames, outputHeaderSize, outputMessagePoolSize
|
||||||
|
|
||||||
// OutputMessageType represents the type of IPC message
|
// OutputMessageType represents the type of IPC message
|
||||||
type OutputMessageType uint8
|
type OutputMessageType uint8
|
||||||
|
|
||||||
|
@ -48,8 +46,8 @@ type OutputIPCMessage struct {
|
||||||
|
|
||||||
// OutputOptimizedMessage represents a pre-allocated message for zero-allocation operations
|
// OutputOptimizedMessage represents a pre-allocated message for zero-allocation operations
|
||||||
type OutputOptimizedMessage struct {
|
type OutputOptimizedMessage struct {
|
||||||
header [outputHeaderSize]byte // Pre-allocated header buffer
|
header [17]byte // Pre-allocated header buffer (using constant value since array size must be compile-time constant)
|
||||||
data []byte // Reusable data buffer
|
data []byte // Reusable data buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutputMessagePool manages pre-allocated messages for zero-allocation IPC
|
// OutputMessagePool manages pre-allocated messages for zero-allocation IPC
|
||||||
|
@ -66,7 +64,7 @@ func NewOutputMessagePool(size int) *OutputMessagePool {
|
||||||
// Pre-allocate messages
|
// Pre-allocate messages
|
||||||
for i := 0; i < size; i++ {
|
for i := 0; i < size; i++ {
|
||||||
msg := &OutputOptimizedMessage{
|
msg := &OutputOptimizedMessage{
|
||||||
data: make([]byte, outputMaxFrameSize),
|
data: make([]byte, GetConfig().OutputMaxFrameSize),
|
||||||
}
|
}
|
||||||
pool.pool <- msg
|
pool.pool <- msg
|
||||||
}
|
}
|
||||||
|
@ -82,7 +80,7 @@ func (p *OutputMessagePool) Get() *OutputOptimizedMessage {
|
||||||
default:
|
default:
|
||||||
// Pool exhausted, create new message
|
// Pool exhausted, create new message
|
||||||
return &OutputOptimizedMessage{
|
return &OutputOptimizedMessage{
|
||||||
data: make([]byte, outputMaxFrameSize),
|
data: make([]byte, GetConfig().OutputMaxFrameSize),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,7 +96,7 @@ func (p *OutputMessagePool) Put(msg *OutputOptimizedMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global message pool for output IPC
|
// Global message pool for output IPC
|
||||||
var globalOutputMessagePool = NewOutputMessagePool(outputMessagePoolSize)
|
var globalOutputMessagePool = NewOutputMessagePool(GetConfig().OutputMessagePoolSize)
|
||||||
|
|
||||||
type AudioServer struct {
|
type AudioServer struct {
|
||||||
// Atomic fields must be first for proper alignment on ARM
|
// Atomic fields must be first for proper alignment on ARM
|
||||||
|
@ -135,7 +133,7 @@ func NewAudioServer() (*AudioServer, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with adaptive buffer size (start with 500 frames)
|
// Initialize with adaptive buffer size (start with 500 frames)
|
||||||
initialBufferSize := int64(500)
|
initialBufferSize := int64(GetConfig().InitialBufferFrames)
|
||||||
|
|
||||||
// Initialize latency monitoring
|
// Initialize latency monitoring
|
||||||
latencyConfig := DefaultLatencyConfig()
|
latencyConfig := DefaultLatencyConfig()
|
||||||
|
@ -284,8 +282,8 @@ func (s *AudioServer) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AudioServer) SendFrame(frame []byte) error {
|
func (s *AudioServer) SendFrame(frame []byte) error {
|
||||||
if len(frame) > outputMaxFrameSize {
|
if len(frame) > GetConfig().OutputMaxFrameSize {
|
||||||
return fmt.Errorf("frame size %d exceeds maximum %d", len(frame), outputMaxFrameSize)
|
return fmt.Errorf("frame size %d exceeds maximum %d", len(frame), GetConfig().OutputMaxFrameSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
@ -340,7 +338,7 @@ func (s *AudioServer) sendFrameToClient(frame []byte) error {
|
||||||
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(start.UnixNano()))
|
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(start.UnixNano()))
|
||||||
|
|
||||||
// Use non-blocking write with timeout
|
// Use non-blocking write with timeout
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), outputWriteTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), GetConfig().OutputWriteTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Create a channel to signal write completion
|
// Create a channel to signal write completion
|
||||||
|
@ -492,8 +490,8 @@ func (c *AudioClient) ReceiveFrame() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
size := binary.LittleEndian.Uint32(optMsg.header[5:9])
|
size := binary.LittleEndian.Uint32(optMsg.header[5:9])
|
||||||
if size > outputMaxFrameSize {
|
if int(size) > GetConfig().OutputMaxFrameSize {
|
||||||
return nil, fmt.Errorf("frame size %d exceeds maximum %d", size, outputMaxFrameSize)
|
return nil, fmt.Errorf("frame size %d exceeds maximum %d", size, GetConfig().OutputMaxFrameSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read frame data
|
// Read frame data
|
||||||
|
|
|
@ -83,10 +83,10 @@ const (
|
||||||
func DefaultLatencyConfig() LatencyConfig {
|
func DefaultLatencyConfig() LatencyConfig {
|
||||||
return LatencyConfig{
|
return LatencyConfig{
|
||||||
TargetLatency: 50 * time.Millisecond,
|
TargetLatency: 50 * time.Millisecond,
|
||||||
MaxLatency: 200 * time.Millisecond,
|
MaxLatency: GetConfig().MaxLatencyThreshold,
|
||||||
OptimizationInterval: 5 * time.Second,
|
OptimizationInterval: 5 * time.Second,
|
||||||
HistorySize: 100,
|
HistorySize: GetConfig().LatencyHistorySize,
|
||||||
JitterThreshold: 20 * time.Millisecond,
|
JitterThreshold: GetConfig().JitterThreshold,
|
||||||
AdaptiveThreshold: 0.8, // Trigger optimization when 80% above target
|
AdaptiveThreshold: 0.8, // Trigger optimization when 80% above target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,8 +171,8 @@ func LogMemoryMetrics() {
|
||||||
metrics := CollectMemoryMetrics()
|
metrics := CollectMemoryMetrics()
|
||||||
|
|
||||||
logger.Info().
|
logger.Info().
|
||||||
Uint64("heap_alloc_mb", metrics.RuntimeStats.HeapAlloc/1024/1024).
|
Uint64("heap_alloc_mb", metrics.RuntimeStats.HeapAlloc/uint64(GetConfig().BytesToMBDivisor)).
|
||||||
Uint64("heap_sys_mb", metrics.RuntimeStats.HeapSys/1024/1024).
|
Uint64("heap_sys_mb", metrics.RuntimeStats.HeapSys/uint64(GetConfig().BytesToMBDivisor)).
|
||||||
Uint64("heap_objects", metrics.RuntimeStats.HeapObjects).
|
Uint64("heap_objects", metrics.RuntimeStats.HeapObjects).
|
||||||
Uint32("num_gc", metrics.RuntimeStats.NumGC).
|
Uint32("num_gc", metrics.RuntimeStats.NumGC).
|
||||||
Float64("gc_cpu_fraction", metrics.RuntimeStats.GCCPUFraction).
|
Float64("gc_cpu_fraction", metrics.RuntimeStats.GCCPUFraction).
|
||||||
|
|
|
@ -451,7 +451,7 @@ func GetLastMetricsUpdate() time.Time {
|
||||||
// StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics
|
// StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics
|
||||||
func StartMetricsUpdater() {
|
func StartMetricsUpdater() {
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(5 * time.Second) // Update every 5 seconds
|
ticker := time.NewTicker(GetConfig().StatsUpdateInterval) // Update every 5 seconds
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
|
|
|
@ -105,7 +105,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
|
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
|
||||||
manager := NewMicrophoneContentionManager(200 * time.Millisecond)
|
manager := NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
|
||||||
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
|
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
|
||||||
return (*MicrophoneContentionManager)(ptr)
|
return (*MicrophoneContentionManager)(ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewMicrophoneContentionManager(200 * time.Millisecond)
|
return NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TryMicrophoneOperation() OperationResult {
|
func TryMicrophoneOperation() OperationResult {
|
||||||
|
|
|
@ -64,7 +64,7 @@ func RunAudioOutputServer() error {
|
||||||
StopNonBlockingAudioStreaming()
|
StopNonBlockingAudioStreaming()
|
||||||
|
|
||||||
// Give some time for cleanup
|
// Give some time for cleanup
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(GetConfig().DefaultSleepDuration)
|
||||||
|
|
||||||
logger.Info().Msg("Audio output server subprocess stopped")
|
logger.Info().Msg("Audio output server subprocess stopped")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -61,9 +61,9 @@ func NewOutputStreamer() (*OutputStreamer, error) {
|
||||||
bufferPool: NewAudioBufferPool(GetMaxAudioFrameSize()), // Use existing buffer pool
|
bufferPool: NewAudioBufferPool(GetMaxAudioFrameSize()), // Use existing buffer pool
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
batchSize: initialBatchSize, // Use adaptive batch size
|
batchSize: initialBatchSize, // Use adaptive batch size
|
||||||
processingChan: make(chan []byte, 500), // Large buffer for smooth processing
|
processingChan: make(chan []byte, GetConfig().ChannelBufferSize), // Large buffer for smooth processing
|
||||||
statsInterval: 5 * time.Second, // Statistics every 5 seconds
|
statsInterval: 5 * time.Second, // Statistics every 5 seconds
|
||||||
lastStatsTime: time.Now().UnixNano(),
|
lastStatsTime: time.Now().UnixNano(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ func (s *OutputStreamer) streamLoop() {
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// Batch size update ticker
|
// Batch size update ticker
|
||||||
batchUpdateTicker := time.NewTicker(500 * time.Millisecond)
|
batchUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
|
||||||
defer batchUpdateTicker.Stop()
|
defer batchUpdateTicker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -233,7 +233,7 @@ func (s *OutputStreamer) reportStatistics() {
|
||||||
processingTime := atomic.LoadInt64(&s.processingTime)
|
processingTime := atomic.LoadInt64(&s.processingTime)
|
||||||
|
|
||||||
if processed > 0 {
|
if processed > 0 {
|
||||||
dropRate := float64(dropped) / float64(processed+dropped) * 100
|
dropRate := float64(dropped) / float64(processed+dropped) * GetConfig().PercentageMultiplier
|
||||||
avgProcessingTime := time.Duration(processingTime)
|
avgProcessingTime := time.Duration(processingTime)
|
||||||
|
|
||||||
getOutputStreamingLogger().Info().Int64("processed", processed).Int64("dropped", dropped).Float64("drop_rate", dropRate).Dur("avg_processing", avgProcessingTime).Msg("Output Audio Stats")
|
getOutputStreamingLogger().Info().Int64("processed", processed).Int64("dropped", dropped).Float64("drop_rate", dropRate).Dur("avg_processing", avgProcessingTime).Msg("Output Audio Stats")
|
||||||
|
@ -270,7 +270,7 @@ func (s *OutputStreamer) GetDetailedStats() map[string]interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
if processed+dropped > 0 {
|
if processed+dropped > 0 {
|
||||||
stats["drop_rate_percent"] = float64(dropped) / float64(processed+dropped) * 100
|
stats["drop_rate_percent"] = float64(dropped) / float64(processed+dropped) * GetConfig().PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add client statistics
|
// Add client statistics
|
||||||
|
@ -343,7 +343,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
RecordFrameReceived(n)
|
RecordFrameReceived(n)
|
||||||
}
|
}
|
||||||
// Small delay to prevent busy waiting
|
// Small delay to prevent busy waiting
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(GetConfig().ShortSleepDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -364,6 +364,6 @@ func StopAudioOutputStreaming() {
|
||||||
|
|
||||||
// Wait for streaming to stop
|
// Wait for streaming to stop
|
||||||
for atomic.LoadInt32(&outputStreamingRunning) == 1 {
|
for atomic.LoadInt32(&outputStreamingRunning) == 1 {
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(GetConfig().ShortSleepDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,23 +16,17 @@ type SchedParam struct {
|
||||||
Priority int32
|
Priority int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority levels for audio processing
|
// getPriorityConstants returns priority levels from centralized config
|
||||||
const (
|
func getPriorityConstants() (audioHigh, audioMedium, audioLow, normal int) {
|
||||||
// SCHED_FIFO priorities (1-99, higher = more priority)
|
config := GetConfig()
|
||||||
AudioHighPriority = 80 // High priority for critical audio processing
|
return config.AudioHighPriority, config.AudioMediumPriority, config.AudioLowPriority, config.NormalPriority
|
||||||
AudioMediumPriority = 60 // Medium priority for regular audio processing
|
}
|
||||||
AudioLowPriority = 40 // Low priority for background audio tasks
|
|
||||||
|
|
||||||
// SCHED_NORMAL is the default (priority 0)
|
// getSchedulingPolicies returns scheduling policies from centralized config
|
||||||
NormalPriority = 0
|
func getSchedulingPolicies() (schedNormal, schedFIFO, schedRR int) {
|
||||||
)
|
config := GetConfig()
|
||||||
|
return config.SchedNormal, config.SchedFIFO, config.SchedRR
|
||||||
// Scheduling policies
|
}
|
||||||
const (
|
|
||||||
SCHED_NORMAL = 0
|
|
||||||
SCHED_FIFO = 1
|
|
||||||
SCHED_RR = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// PriorityScheduler manages thread priorities for audio processing
|
// PriorityScheduler manages thread priorities for audio processing
|
||||||
type PriorityScheduler struct {
|
type PriorityScheduler struct {
|
||||||
|
@ -73,7 +67,8 @@ func (ps *PriorityScheduler) SetThreadPriority(priority int, policy int) error {
|
||||||
|
|
||||||
if errno != 0 {
|
if errno != 0 {
|
||||||
// If we can't set real-time priority, try nice value instead
|
// If we can't set real-time priority, try nice value instead
|
||||||
if policy != SCHED_NORMAL {
|
schedNormal, _, _ := getSchedulingPolicies()
|
||||||
|
if policy != schedNormal {
|
||||||
ps.logger.Warn().Int("errno", int(errno)).Msg("Failed to set real-time priority, falling back to nice")
|
ps.logger.Warn().Int("errno", int(errno)).Msg("Failed to set real-time priority, falling back to nice")
|
||||||
return ps.setNicePriority(priority)
|
return ps.setNicePriority(priority)
|
||||||
}
|
}
|
||||||
|
@ -89,11 +84,11 @@ func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
|
||||||
// Convert real-time priority to nice value (inverse relationship)
|
// Convert real-time priority to nice value (inverse relationship)
|
||||||
// RT priority 80 -> nice -10, RT priority 40 -> nice 0
|
// RT priority 80 -> nice -10, RT priority 40 -> nice 0
|
||||||
niceValue := (40 - rtPriority) / 4
|
niceValue := (40 - rtPriority) / 4
|
||||||
if niceValue < -20 {
|
if niceValue < GetConfig().MinNiceValue {
|
||||||
niceValue = -20
|
niceValue = GetConfig().MinNiceValue
|
||||||
}
|
}
|
||||||
if niceValue > 19 {
|
if niceValue > GetConfig().MaxNiceValue {
|
||||||
niceValue = 19
|
niceValue = GetConfig().MaxNiceValue
|
||||||
}
|
}
|
||||||
|
|
||||||
err := syscall.Setpriority(syscall.PRIO_PROCESS, 0, niceValue)
|
err := syscall.Setpriority(syscall.PRIO_PROCESS, 0, niceValue)
|
||||||
|
@ -108,22 +103,30 @@ func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
|
||||||
|
|
||||||
// SetAudioProcessingPriority sets high priority for audio processing threads
|
// SetAudioProcessingPriority sets high priority for audio processing threads
|
||||||
func (ps *PriorityScheduler) SetAudioProcessingPriority() error {
|
func (ps *PriorityScheduler) SetAudioProcessingPriority() error {
|
||||||
return ps.SetThreadPriority(AudioHighPriority, SCHED_FIFO)
|
audioHigh, _, _, _ := getPriorityConstants()
|
||||||
|
_, schedFIFO, _ := getSchedulingPolicies()
|
||||||
|
return ps.SetThreadPriority(audioHigh, schedFIFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAudioIOPriority sets medium priority for audio I/O threads
|
// SetAudioIOPriority sets medium priority for audio I/O threads
|
||||||
func (ps *PriorityScheduler) SetAudioIOPriority() error {
|
func (ps *PriorityScheduler) SetAudioIOPriority() error {
|
||||||
return ps.SetThreadPriority(AudioMediumPriority, SCHED_FIFO)
|
_, audioMedium, _, _ := getPriorityConstants()
|
||||||
|
_, schedFIFO, _ := getSchedulingPolicies()
|
||||||
|
return ps.SetThreadPriority(audioMedium, schedFIFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAudioBackgroundPriority sets low priority for background audio tasks
|
// SetAudioBackgroundPriority sets low priority for background audio tasks
|
||||||
func (ps *PriorityScheduler) SetAudioBackgroundPriority() error {
|
func (ps *PriorityScheduler) SetAudioBackgroundPriority() error {
|
||||||
return ps.SetThreadPriority(AudioLowPriority, SCHED_FIFO)
|
_, _, audioLow, _ := getPriorityConstants()
|
||||||
|
_, schedFIFO, _ := getSchedulingPolicies()
|
||||||
|
return ps.SetThreadPriority(audioLow, schedFIFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetPriority resets thread to normal scheduling
|
// ResetPriority resets thread to normal scheduling
|
||||||
func (ps *PriorityScheduler) ResetPriority() error {
|
func (ps *PriorityScheduler) ResetPriority() error {
|
||||||
return ps.SetThreadPriority(NormalPriority, SCHED_NORMAL)
|
_, _, _, normal := getPriorityConstants()
|
||||||
|
schedNormal, _, _ := getSchedulingPolicies()
|
||||||
|
return ps.SetThreadPriority(normal, schedNormal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable disables priority scheduling (useful for testing or fallback)
|
// Disable disables priority scheduling (useful for testing or fallback)
|
||||||
|
|
|
@ -13,26 +13,29 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants for process monitoring
|
// Variables for process monitoring (using configuration)
|
||||||
const (
|
var (
|
||||||
// System constants
|
// System constants
|
||||||
pageSize = 4096
|
maxCPUPercent = GetConfig().MaxCPUPercent
|
||||||
maxCPUPercent = 100.0
|
minCPUPercent = GetConfig().MinCPUPercent
|
||||||
minCPUPercent = 0.01
|
defaultClockTicks = GetConfig().DefaultClockTicks
|
||||||
defaultClockTicks = 250.0 // Common for embedded ARM systems
|
defaultMemoryGB = GetConfig().DefaultMemoryGB
|
||||||
defaultMemoryGB = 8
|
|
||||||
|
|
||||||
// Monitoring thresholds
|
// Monitoring thresholds
|
||||||
maxWarmupSamples = 3
|
maxWarmupSamples = GetConfig().MaxWarmupSamples
|
||||||
warmupCPUSamples = 2
|
warmupCPUSamples = GetConfig().WarmupCPUSamples
|
||||||
logThrottleInterval = 10
|
|
||||||
|
|
||||||
// Channel buffer size
|
// Channel buffer size
|
||||||
metricsChannelBuffer = 100
|
metricsChannelBuffer = GetConfig().MetricsChannelBuffer
|
||||||
|
|
||||||
// Clock tick detection ranges
|
// Clock tick detection ranges
|
||||||
minValidClockTicks = 50
|
minValidClockTicks = float64(GetConfig().MinValidClockTicks)
|
||||||
maxValidClockTicks = 1000
|
maxValidClockTicks = float64(GetConfig().MaxValidClockTicks)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Variables for process monitoring
|
||||||
|
var (
|
||||||
|
pageSize = GetConfig().PageSize
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessMetrics represents CPU and memory usage metrics for a process
|
// ProcessMetrics represents CPU and memory usage metrics for a process
|
||||||
|
@ -217,7 +220,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
|
||||||
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
|
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
|
||||||
rss, _ := strconv.ParseInt(fields[23], 10, 64)
|
rss, _ := strconv.ParseInt(fields[23], 10, 64)
|
||||||
|
|
||||||
metric.MemoryRSS = rss * pageSize
|
metric.MemoryRSS = rss * int64(pageSize)
|
||||||
metric.MemoryVMS = vsize
|
metric.MemoryVMS = vsize
|
||||||
|
|
||||||
// Calculate CPU percentage
|
// Calculate CPU percentage
|
||||||
|
@ -230,7 +233,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
|
||||||
|
|
||||||
// Calculate memory percentage (RSS / total system memory)
|
// Calculate memory percentage (RSS / total system memory)
|
||||||
if totalMem := pm.getTotalMemory(); totalMem > 0 {
|
if totalMem := pm.getTotalMemory(); totalMem > 0 {
|
||||||
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * 100.0
|
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * GetConfig().PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state for next calculation
|
// Update state for next calculation
|
||||||
|
@ -261,7 +264,7 @@ func (pm *ProcessMonitor) calculateCPUPercent(totalCPUTime int64, state *process
|
||||||
// Convert from clock ticks to seconds using actual system clock ticks
|
// Convert from clock ticks to seconds using actual system clock ticks
|
||||||
clockTicks := pm.getClockTicks()
|
clockTicks := pm.getClockTicks()
|
||||||
cpuSeconds := cpuDelta / clockTicks
|
cpuSeconds := cpuDelta / clockTicks
|
||||||
cpuPercent := (cpuSeconds / timeDelta) * 100.0
|
cpuPercent := (cpuSeconds / timeDelta) * GetConfig().PercentageMultiplier
|
||||||
|
|
||||||
// Apply bounds
|
// Apply bounds
|
||||||
if cpuPercent > maxCPUPercent {
|
if cpuPercent > maxCPUPercent {
|
||||||
|
@ -341,7 +344,7 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
|
||||||
pm.memoryOnce.Do(func() {
|
pm.memoryOnce.Do(func() {
|
||||||
file, err := os.Open("/proc/meminfo")
|
file, err := os.Open("/proc/meminfo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024
|
pm.totalMemory = int64(defaultMemoryGB) * 1024 * 1024 * 1024
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
@ -360,7 +363,7 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024 // Fallback
|
pm.totalMemory = int64(defaultMemoryGB) * 1024 * 1024 * 1024 // Fallback
|
||||||
})
|
})
|
||||||
return pm.totalMemory
|
return pm.totalMemory
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,7 +151,7 @@ func (r *AudioRelay) relayLoop() {
|
||||||
r.logger.Error().Msg("Too many consecutive errors, stopping relay")
|
r.logger.Error().Msg("Too many consecutive errors, stopping relay")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(GetConfig().ShortSleepDuration)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// Socket buffer sizes are now centralized in config_constants.go
|
||||||
// Socket buffer sizes optimized for JetKVM's audio workload
|
|
||||||
OptimalSocketBuffer = 128 * 1024 // 128KB (32 frames @ 4KB each)
|
|
||||||
MaxSocketBuffer = 256 * 1024 // 256KB for high-load scenarios
|
|
||||||
MinSocketBuffer = 32 * 1024 // 32KB minimum for basic functionality
|
|
||||||
)
|
|
||||||
|
|
||||||
// SocketBufferConfig holds socket buffer configuration
|
// SocketBufferConfig holds socket buffer configuration
|
||||||
type SocketBufferConfig struct {
|
type SocketBufferConfig struct {
|
||||||
|
@ -23,8 +18,8 @@ type SocketBufferConfig struct {
|
||||||
// DefaultSocketBufferConfig returns the default socket buffer configuration
|
// DefaultSocketBufferConfig returns the default socket buffer configuration
|
||||||
func DefaultSocketBufferConfig() SocketBufferConfig {
|
func DefaultSocketBufferConfig() SocketBufferConfig {
|
||||||
return SocketBufferConfig{
|
return SocketBufferConfig{
|
||||||
SendBufferSize: OptimalSocketBuffer,
|
SendBufferSize: GetConfig().SocketOptimalBuffer,
|
||||||
RecvBufferSize: OptimalSocketBuffer,
|
RecvBufferSize: GetConfig().SocketOptimalBuffer,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,8 +27,8 @@ func DefaultSocketBufferConfig() SocketBufferConfig {
|
||||||
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
|
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
|
||||||
func HighLoadSocketBufferConfig() SocketBufferConfig {
|
func HighLoadSocketBufferConfig() SocketBufferConfig {
|
||||||
return SocketBufferConfig{
|
return SocketBufferConfig{
|
||||||
SendBufferSize: MaxSocketBuffer,
|
SendBufferSize: GetConfig().SocketMaxBuffer,
|
||||||
RecvBufferSize: MaxSocketBuffer,
|
RecvBufferSize: GetConfig().SocketMaxBuffer,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,20 +107,20 @@ func ValidateSocketBufferConfig(config SocketBufferConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SendBufferSize < MinSocketBuffer {
|
if config.SendBufferSize < GetConfig().SocketMinBuffer {
|
||||||
return fmt.Errorf("send buffer size %d is below minimum %d", config.SendBufferSize, MinSocketBuffer)
|
return fmt.Errorf("send buffer size %d is below minimum %d", config.SendBufferSize, GetConfig().SocketMinBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.RecvBufferSize < MinSocketBuffer {
|
if config.RecvBufferSize < GetConfig().SocketMinBuffer {
|
||||||
return fmt.Errorf("receive buffer size %d is below minimum %d", config.RecvBufferSize, MinSocketBuffer)
|
return fmt.Errorf("receive buffer size %d is below minimum %d", config.RecvBufferSize, GetConfig().SocketMinBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SendBufferSize > MaxSocketBuffer {
|
if config.SendBufferSize > GetConfig().SocketMaxBuffer {
|
||||||
return fmt.Errorf("send buffer size %d exceeds maximum %d", config.SendBufferSize, MaxSocketBuffer)
|
return fmt.Errorf("send buffer size %d exceeds maximum %d", config.SendBufferSize, GetConfig().SocketMaxBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.RecvBufferSize > MaxSocketBuffer {
|
if config.RecvBufferSize > GetConfig().SocketMaxBuffer {
|
||||||
return fmt.Errorf("receive buffer size %d exceeds maximum %d", config.RecvBufferSize, MaxSocketBuffer)
|
return fmt.Errorf("receive buffer size %d exceeds maximum %d", config.RecvBufferSize, GetConfig().SocketMaxBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -131,7 +131,7 @@ func (s *AudioServerSupervisor) Stop() error {
|
||||||
select {
|
select {
|
||||||
case <-s.processDone:
|
case <-s.processDone:
|
||||||
s.logger.Info().Msg("audio server process stopped gracefully")
|
s.logger.Info().Msg("audio server process stopped gracefully")
|
||||||
case <-time.After(10 * time.Second):
|
case <-time.After(GetConfig().SupervisorTimeout):
|
||||||
s.logger.Warn().Msg("audio server process did not stop gracefully, forcing termination")
|
s.logger.Warn().Msg("audio server process did not stop gracefully, forcing termination")
|
||||||
s.forceKillProcess()
|
s.forceKillProcess()
|
||||||
}
|
}
|
||||||
|
@ -365,7 +365,7 @@ func (s *AudioServerSupervisor) terminateProcess() {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
s.logger.Info().Int("pid", pid).Msg("audio server process terminated gracefully")
|
s.logger.Info().Int("pid", pid).Msg("audio server process terminated gracefully")
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(GetConfig().InputSupervisorTimeout):
|
||||||
s.logger.Warn().Int("pid", pid).Msg("process did not terminate gracefully, sending SIGKILL")
|
s.logger.Warn().Int("pid", pid).Msg("process did not terminate gracefully, sending SIGKILL")
|
||||||
s.forceKillProcess()
|
s.forceKillProcess()
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,7 +234,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
|
||||||
|
|
||||||
var hitRate float64
|
var hitRate float64
|
||||||
if totalRequests > 0 {
|
if totalRequests > 0 {
|
||||||
hitRate = float64(hitCount) / float64(totalRequests) * 100
|
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
return ZeroCopyFramePoolStats{
|
return ZeroCopyFramePoolStats{
|
||||||
|
|
Loading…
Reference in New Issue