From a3702dadd9bfb3cff00935f34de8c949bc9a1db4 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 3 Sep 2025 22:35:52 +0000 Subject: [PATCH] Cleanup: reduce PR complexity --- cloud.go | 24 - internal/audio/adaptive_buffer.go | 23 +- internal/audio/audio.go | 54 +- .../audio/audio_quality_edge_cases_test.go | 317 ------- internal/audio/audio_test.go | 366 -------- internal/audio/base_manager.go | 34 - internal/audio/cgo_audio.go | 29 +- internal/audio/cgo_audio_stub.go | 48 - internal/audio/config_constants.go | 86 -- internal/audio/events.go | 254 +---- internal/audio/goroutine_monitor.go | 8 +- internal/audio/granular_metrics_test.go | 100 -- internal/audio/input.go | 10 - internal/audio/input_ipc_manager_test.go | 277 ------ internal/audio/input_test.go | 244 ----- internal/audio/integration_test.go | 320 ------- internal/audio/latency_profiler.go | 15 +- internal/audio/output_manager_test.go | 277 ------ internal/audio/output_streaming.go | 49 - internal/audio/output_streaming_test.go | 341 ------- internal/audio/performance_critical_test.go | 393 -------- internal/audio/regression_test.go | 362 ------- internal/audio/supervisor_test.go | 393 -------- internal/audio/supervisor_unit_test.go | 217 ----- internal/audio/test_utils.go | 319 ------- internal/audio/validation_test.go | 541 ----------- internal/audio/zero_copy.go | 53 +- ui/src/components/ActionBar.tsx | 2 +- ui/src/components/AudioConfigDisplay.tsx | 38 - ui/src/components/AudioLevelMeter.tsx | 77 -- ui/src/components/AudioMetricsDashboard.tsx | 880 ------------------ ui/src/components/AudioStatusIndicator.tsx | 33 - .../popovers/AudioControlPopover.tsx | 113 +-- .../sidebar/AudioMetricsSidebar.tsx | 16 - ui/src/hooks/stores.ts | 2 +- ui/src/hooks/useAudioEvents.ts | 74 +- ui/src/hooks/useAudioLevel.ts | 136 --- ui/src/routes/devices.$id.tsx | 18 +- web.go | 136 +-- 39 files changed, 72 insertions(+), 6607 deletions(-) delete mode 100644 internal/audio/audio_quality_edge_cases_test.go delete mode 100644 internal/audio/audio_test.go delete mode 100644 internal/audio/cgo_audio_stub.go delete mode 100644 internal/audio/granular_metrics_test.go delete mode 100644 internal/audio/input_ipc_manager_test.go delete mode 100644 internal/audio/input_test.go delete mode 100644 internal/audio/integration_test.go delete mode 100644 internal/audio/output_manager_test.go delete mode 100644 internal/audio/output_streaming_test.go delete mode 100644 internal/audio/performance_critical_test.go delete mode 100644 internal/audio/regression_test.go delete mode 100644 internal/audio/supervisor_test.go delete mode 100644 internal/audio/supervisor_unit_test.go delete mode 100644 internal/audio/test_utils.go delete mode 100644 internal/audio/validation_test.go delete mode 100644 ui/src/components/AudioConfigDisplay.tsx delete mode 100644 ui/src/components/AudioLevelMeter.tsx delete mode 100644 ui/src/components/AudioMetricsDashboard.tsx delete mode 100644 ui/src/components/AudioStatusIndicator.tsx delete mode 100644 ui/src/components/sidebar/AudioMetricsSidebar.tsx delete mode 100644 ui/src/hooks/useAudioLevel.ts diff --git a/cloud.go b/cloud.go index c1b6187b..e3e1d5da 100644 --- a/cloud.go +++ b/cloud.go @@ -77,23 +77,6 @@ var ( }, []string{"type", "source"}, ) - metricConnectionPingDuration = promauto.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "jetkvm_connection_ping_duration_seconds", - Help: "The duration of the ping response", - Buckets: []float64{ - 0.1, 0.5, 1, 10, - }, - }, - []string{"type", "source"}, - ) - metricConnectionTotalPingSentCount = promauto.NewCounterVec( - prometheus.CounterOpts{ - Name: "jetkvm_connection_ping_sent_total", - Help: "The total number of pings sent to the connection", - }, - []string{"type", "source"}, - ) metricConnectionTotalPingReceivedCount = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "jetkvm_connection_ping_received_total", @@ -101,13 +84,6 @@ var ( }, []string{"type", "source"}, ) - metricConnectionSessionRequestCount = promauto.NewCounterVec( - prometheus.CounterOpts{ - Name: "jetkvm_connection_session_requests_total", - Help: "The total number of session requests received", - }, - []string{"type", "source"}, - ) metricConnectionSessionRequestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "jetkvm_connection_session_request_duration_seconds", diff --git a/internal/audio/adaptive_buffer.go b/internal/audio/adaptive_buffer.go index 3ffb61e6..5eccf84d 100644 --- a/internal/audio/adaptive_buffer.go +++ b/internal/audio/adaptive_buffer.go @@ -152,22 +152,6 @@ func (abm *AdaptiveBufferManager) GetOutputBufferSize() int { // UpdateLatency updates the current latency measurement func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) { - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Use exponential moving average for latency - currentAvg := atomic.LoadInt64(&abm.averageLatency) - newLatency := latency.Nanoseconds() - - if currentAvg == 0 { - atomic.StoreInt64(&abm.averageLatency, newLatency) - } else { - // Exponential moving average: 70% historical, 30% current - newAvg := int64(float64(currentAvg)*GetConfig().HistoricalWeight + float64(newLatency)*GetConfig().CurrentWeight) - atomic.StoreInt64(&abm.averageLatency, newAvg) - } } // adaptationLoop is the main loop that adjusts buffer sizes @@ -240,11 +224,8 @@ func (abm *AdaptiveBufferManager) adaptBufferSizes() { systemCPU := totalCPU // Total CPU across all monitored processes systemMemory := totalMemory / float64(processCount) // Average memory usage - cachedConfig := GetCachedConfig() - if cachedConfig.GetEnableMetricsCollection() { - atomic.StoreInt64(&abm.systemCPUPercent, int64(systemCPU*100)) - atomic.StoreInt64(&abm.systemMemoryPercent, int64(systemMemory*100)) - } + atomic.StoreInt64(&abm.systemCPUPercent, int64(systemCPU*100)) + atomic.StoreInt64(&abm.systemMemoryPercent, int64(systemMemory*100)) // Get current latency currentLatencyNs := atomic.LoadInt64(&abm.averageLatency) diff --git a/internal/audio/audio.go b/internal/audio/audio.go index 2b8936a5..e382483c 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -361,77 +361,25 @@ var ( batchedBytesProcessed int64 batchedFramesDropped int64 batchedConnectionDrops int64 - batchCounter int64 - lastFlushTime int64 // Unix timestamp in nanoseconds -) -const ( - // Batch size for metrics updates (reduce atomic ops by 10x) - metricsFlushInterval = 10 - // Force flush every 100ms to ensure metrics freshness - metricsForceFlushNanos = 100 * 1000 * 1000 // 100ms in nanoseconds + lastFlushTime int64 // Unix timestamp in nanoseconds ) // RecordFrameReceived increments the frames received counter with batched updates func RecordFrameReceived(bytes int) { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - // Use local batching to reduce atomic operations frequency - atomic.AddInt64(&batchedFramesReceived, 1) atomic.AddInt64(&batchedBytesProcessed, int64(bytes)) // Update timestamp immediately for accurate tracking metrics.LastFrameTime = time.Now() - - // Check if we should flush batched metrics - if atomic.AddInt64(&batchCounter, 1)%metricsFlushInterval == 0 { - flushBatchedMetrics() - } else { - // Force flush if too much time has passed - now := time.Now().UnixNano() - lastFlush := atomic.LoadInt64(&lastFlushTime) - if now-lastFlush > metricsForceFlushNanos { - flushBatchedMetrics() - } - } } // RecordFrameDropped increments the frames dropped counter with batched updates func RecordFrameDropped() { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Use local batching to reduce atomic operations frequency - atomic.AddInt64(&batchedFramesDropped, 1) - - // Check if we should flush batched metrics - if atomic.AddInt64(&batchCounter, 1)%metricsFlushInterval == 0 { - flushBatchedMetrics() - } } // RecordConnectionDrop increments the connection drops counter with batched updates func RecordConnectionDrop() { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Use local batching to reduce atomic operations frequency - atomic.AddInt64(&batchedConnectionDrops, 1) - - // Check if we should flush batched metrics - if atomic.AddInt64(&batchCounter, 1)%metricsFlushInterval == 0 { - flushBatchedMetrics() - } } // flushBatchedMetrics flushes accumulated metrics to the main counters diff --git a/internal/audio/audio_quality_edge_cases_test.go b/internal/audio/audio_quality_edge_cases_test.go deleted file mode 100644 index df1a5f89..00000000 --- a/internal/audio/audio_quality_edge_cases_test.go +++ /dev/null @@ -1,317 +0,0 @@ -//go:build cgo -// +build cgo - -package audio - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestAudioQualityEdgeCases tests edge cases for audio quality functions -// These tests ensure the recent validation removal doesn't introduce regressions -func TestAudioQualityEdgeCases(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"AudioQualityBoundaryValues", testAudioQualityBoundaryValues}, - {"MicrophoneQualityBoundaryValues", testMicrophoneQualityBoundaryValues}, - {"AudioQualityPresetsConsistency", testAudioQualityPresetsConsistency}, - {"MicrophoneQualityPresetsConsistency", testMicrophoneQualityPresetsConsistency}, - {"QualitySettingsThreadSafety", testQualitySettingsThreadSafety}, - {"QualityPresetsImmutability", testQualityPresetsImmutability}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -// testAudioQualityBoundaryValues tests boundary values for audio quality -func testAudioQualityBoundaryValues(t *testing.T) { - // Test minimum valid quality (0) - originalConfig := GetAudioConfig() - SetAudioQuality(AudioQualityLow) - assert.Equal(t, AudioQualityLow, GetAudioConfig().Quality, "Should accept minimum quality value") - - // Test maximum valid quality (3) - SetAudioQuality(AudioQualityUltra) - assert.Equal(t, AudioQualityUltra, GetAudioConfig().Quality, "Should accept maximum quality value") - - // Test that quality settings work correctly - SetAudioQuality(AudioQualityMedium) - currentConfig := GetAudioConfig() - assert.Equal(t, AudioQualityMedium, currentConfig.Quality, "Should set medium quality") - t.Logf("Medium quality config: %+v", currentConfig) - - SetAudioQuality(AudioQualityHigh) - currentConfig = GetAudioConfig() - assert.Equal(t, AudioQualityHigh, currentConfig.Quality, "Should set high quality") - t.Logf("High quality config: %+v", currentConfig) - - // Restore original quality - SetAudioQuality(originalConfig.Quality) -} - -// testMicrophoneQualityBoundaryValues tests boundary values for microphone quality -func testMicrophoneQualityBoundaryValues(t *testing.T) { - // Test minimum valid quality - originalConfig := GetMicrophoneConfig() - SetMicrophoneQuality(AudioQualityLow) - assert.Equal(t, AudioQualityLow, GetMicrophoneConfig().Quality, "Should accept minimum microphone quality value") - - // Test maximum valid quality - SetMicrophoneQuality(AudioQualityUltra) - assert.Equal(t, AudioQualityUltra, GetMicrophoneConfig().Quality, "Should accept maximum microphone quality value") - - // Test that quality settings work correctly - SetMicrophoneQuality(AudioQualityMedium) - currentConfig := GetMicrophoneConfig() - assert.Equal(t, AudioQualityMedium, currentConfig.Quality, "Should set medium microphone quality") - t.Logf("Medium microphone quality config: %+v", currentConfig) - - SetMicrophoneQuality(AudioQualityHigh) - currentConfig = GetMicrophoneConfig() - assert.Equal(t, AudioQualityHigh, currentConfig.Quality, "Should set high microphone quality") - t.Logf("High microphone quality config: %+v", currentConfig) - - // Restore original quality - SetMicrophoneQuality(originalConfig.Quality) -} - -// testAudioQualityPresetsConsistency tests consistency of audio quality presets -func testAudioQualityPresetsConsistency(t *testing.T) { - presets := GetAudioQualityPresets() - require.NotNil(t, presets, "Audio quality presets should not be nil") - require.NotEmpty(t, presets, "Audio quality presets should not be empty") - - // Verify presets have expected structure - for i, preset := range presets { - t.Logf("Audio preset %d: %+v", i, preset) - - // Each preset should have reasonable values - assert.GreaterOrEqual(t, preset.Bitrate, 0, "Bitrate should be non-negative") - assert.Greater(t, preset.SampleRate, 0, "Sample rate should be positive") - assert.Greater(t, preset.Channels, 0, "Channels should be positive") - } - - // Test that presets are accessible by valid quality levels - qualityLevels := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} - for _, quality := range qualityLevels { - preset, exists := presets[quality] - assert.True(t, exists, "Preset should exist for quality %v", quality) - assert.Greater(t, preset.Bitrate, 0, "Preset bitrate should be positive for quality %v", quality) - } -} - -// testMicrophoneQualityPresetsConsistency tests consistency of microphone quality presets -func testMicrophoneQualityPresetsConsistency(t *testing.T) { - presets := GetMicrophoneQualityPresets() - require.NotNil(t, presets, "Microphone quality presets should not be nil") - require.NotEmpty(t, presets, "Microphone quality presets should not be empty") - - // Verify presets have expected structure - for i, preset := range presets { - t.Logf("Microphone preset %d: %+v", i, preset) - - // Each preset should have reasonable values - assert.GreaterOrEqual(t, preset.Bitrate, 0, "Bitrate should be non-negative") - assert.Greater(t, preset.SampleRate, 0, "Sample rate should be positive") - assert.Greater(t, preset.Channels, 0, "Channels should be positive") - } - - // Test that presets are accessible by valid quality levels - qualityLevels := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} - for _, quality := range qualityLevels { - preset, exists := presets[quality] - assert.True(t, exists, "Microphone preset should exist for quality %v", quality) - assert.Greater(t, preset.Bitrate, 0, "Microphone preset bitrate should be positive for quality %v", quality) - } -} - -// testQualitySettingsThreadSafety tests thread safety of quality settings -func testQualitySettingsThreadSafety(t *testing.T) { - if testing.Short() { - t.Skip("Skipping thread safety test in short mode") - } - - originalAudioConfig := GetAudioConfig() - originalMicConfig := GetMicrophoneConfig() - - // Test concurrent access to quality settings - const numGoroutines = 50 - const numOperations = 100 - - done := make(chan bool, numGoroutines*2) - - // Audio quality goroutines - for i := 0; i < numGoroutines; i++ { - go func(id int) { - for j := 0; j < numOperations; j++ { - // Cycle through valid quality values - qualityIndex := j % 4 - var quality AudioQuality - switch qualityIndex { - case 0: - quality = AudioQualityLow - case 1: - quality = AudioQualityMedium - case 2: - quality = AudioQualityHigh - case 3: - quality = AudioQualityUltra - } - SetAudioQuality(quality) - _ = GetAudioConfig() - } - done <- true - }(i) - } - - // Microphone quality goroutines - for i := 0; i < numGoroutines; i++ { - go func(id int) { - for j := 0; j < numOperations; j++ { - // Cycle through valid quality values - qualityIndex := j % 4 - var quality AudioQuality - switch qualityIndex { - case 0: - quality = AudioQualityLow - case 1: - quality = AudioQualityMedium - case 2: - quality = AudioQualityHigh - case 3: - quality = AudioQualityUltra - } - SetMicrophoneQuality(quality) - _ = GetMicrophoneConfig() - } - done <- true - }(i) - } - - // Wait for all goroutines to complete - for i := 0; i < numGoroutines*2; i++ { - <-done - } - - // Verify system is still functional - SetAudioQuality(AudioQualityHigh) - assert.Equal(t, AudioQualityHigh, GetAudioConfig().Quality, "Audio quality should be settable after concurrent access") - - SetMicrophoneQuality(AudioQualityMedium) - assert.Equal(t, AudioQualityMedium, GetMicrophoneConfig().Quality, "Microphone quality should be settable after concurrent access") - - // Restore original values - SetAudioQuality(originalAudioConfig.Quality) - SetMicrophoneQuality(originalMicConfig.Quality) -} - -// testQualityPresetsImmutability tests that quality presets are not accidentally modified -func testQualityPresetsImmutability(t *testing.T) { - // Get presets multiple times and verify they're consistent - presets1 := GetAudioQualityPresets() - presets2 := GetAudioQualityPresets() - - require.Equal(t, len(presets1), len(presets2), "Preset count should be consistent") - - // Verify each preset is identical - for quality := range presets1 { - assert.Equal(t, presets1[quality].Bitrate, presets2[quality].Bitrate, - "Preset %v bitrate should be consistent", quality) - assert.Equal(t, presets1[quality].SampleRate, presets2[quality].SampleRate, - "Preset %v sample rate should be consistent", quality) - assert.Equal(t, presets1[quality].Channels, presets2[quality].Channels, - "Preset %v channels should be consistent", quality) - } - - // Test microphone presets as well - micPresets1 := GetMicrophoneQualityPresets() - micPresets2 := GetMicrophoneQualityPresets() - - require.Equal(t, len(micPresets1), len(micPresets2), "Microphone preset count should be consistent") - - for quality := range micPresets1 { - assert.Equal(t, micPresets1[quality].Bitrate, micPresets2[quality].Bitrate, - "Microphone preset %v bitrate should be consistent", quality) - assert.Equal(t, micPresets1[quality].SampleRate, micPresets2[quality].SampleRate, - "Microphone preset %v sample rate should be consistent", quality) - assert.Equal(t, micPresets1[quality].Channels, micPresets2[quality].Channels, - "Microphone preset %v channels should be consistent", quality) - } -} - -// TestQualityValidationRemovalRegression tests that validation removal doesn't cause regressions -func TestQualityValidationRemovalRegression(t *testing.T) { - // This test ensures that removing validation from GET endpoints doesn't break functionality - - // Test that presets are still accessible - audioPresets := GetAudioQualityPresets() - assert.NotNil(t, audioPresets, "Audio presets should be accessible after validation removal") - assert.NotEmpty(t, audioPresets, "Audio presets should not be empty") - - micPresets := GetMicrophoneQualityPresets() - assert.NotNil(t, micPresets, "Microphone presets should be accessible after validation removal") - assert.NotEmpty(t, micPresets, "Microphone presets should not be empty") - - // Test that quality getters still work - audioConfig := GetAudioConfig() - assert.GreaterOrEqual(t, int(audioConfig.Quality), 0, "Audio quality should be non-negative") - - micConfig := GetMicrophoneConfig() - assert.GreaterOrEqual(t, int(micConfig.Quality), 0, "Microphone quality should be non-negative") - - // Test that setters still work (for valid values) - originalAudio := GetAudioConfig() - originalMic := GetMicrophoneConfig() - - SetAudioQuality(AudioQualityMedium) - assert.Equal(t, AudioQualityMedium, GetAudioConfig().Quality, "Audio quality setter should work") - - SetMicrophoneQuality(AudioQualityHigh) - assert.Equal(t, AudioQualityHigh, GetMicrophoneConfig().Quality, "Microphone quality setter should work") - - // Restore original values - SetAudioQuality(originalAudio.Quality) - SetMicrophoneQuality(originalMic.Quality) -} - -// TestPerformanceAfterValidationRemoval tests that performance improved after validation removal -func TestPerformanceAfterValidationRemoval(t *testing.T) { - if testing.Short() { - t.Skip("Skipping performance test in short mode") - } - - // Benchmark preset access (should be faster without validation) - const iterations = 10000 - - // Time audio preset access - start := time.Now() - for i := 0; i < iterations; i++ { - _ = GetAudioQualityPresets() - } - audioDuration := time.Since(start) - - // Time microphone preset access - start = time.Now() - for i := 0; i < iterations; i++ { - _ = GetMicrophoneQualityPresets() - } - micDuration := time.Since(start) - - t.Logf("Audio presets access time for %d iterations: %v", iterations, audioDuration) - t.Logf("Microphone presets access time for %d iterations: %v", iterations, micDuration) - - // Verify reasonable performance (should complete quickly without validation overhead) - maxExpectedDuration := time.Second // Very generous limit - assert.Less(t, audioDuration, maxExpectedDuration, "Audio preset access should be fast") - assert.Less(t, micDuration, maxExpectedDuration, "Microphone preset access should be fast") -} diff --git a/internal/audio/audio_test.go b/internal/audio/audio_test.go deleted file mode 100644 index 7a7d92fa..00000000 --- a/internal/audio/audio_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package audio - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/jetkvm/kvm/internal/usbgadget" -) - -// Unit tests for the audio package - -func TestAudioQuality(t *testing.T) { - tests := []struct { - name string - quality AudioQuality - expected string - }{ - {"Low Quality", AudioQualityLow, "low"}, - {"Medium Quality", AudioQualityMedium, "medium"}, - {"High Quality", AudioQualityHigh, "high"}, - {"Ultra Quality", AudioQualityUltra, "ultra"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Test quality setting - SetAudioQuality(tt.quality) - config := GetAudioConfig() - assert.Equal(t, tt.quality, config.Quality) - assert.Greater(t, config.Bitrate, 0) - assert.Greater(t, config.SampleRate, 0) - assert.Greater(t, config.Channels, 0) - assert.Greater(t, config.FrameSize, time.Duration(0)) - }) - } -} - -func TestMicrophoneQuality(t *testing.T) { - tests := []struct { - name string - quality AudioQuality - }{ - {"Low Quality", AudioQualityLow}, - {"Medium Quality", AudioQualityMedium}, - {"High Quality", AudioQualityHigh}, - {"Ultra Quality", AudioQualityUltra}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Test microphone quality setting - SetMicrophoneQuality(tt.quality) - config := GetMicrophoneConfig() - assert.Equal(t, tt.quality, config.Quality) - assert.Equal(t, 1, config.Channels) // Microphone is always mono - assert.Greater(t, config.Bitrate, 0) - assert.Greater(t, config.SampleRate, 0) - }) - } -} - -func TestAudioQualityPresets(t *testing.T) { - presets := GetAudioQualityPresets() - require.NotEmpty(t, presets) - - // Test that all quality levels have presets - for quality := AudioQualityLow; quality <= AudioQualityUltra; quality++ { - config, exists := presets[quality] - require.True(t, exists, "Preset should exist for quality %d", quality) - assert.Equal(t, quality, config.Quality) - assert.Greater(t, config.Bitrate, 0) - assert.Greater(t, config.SampleRate, 0) - assert.Greater(t, config.Channels, 0) - assert.Greater(t, config.FrameSize, time.Duration(0)) - } - - // Test that higher quality has higher bitrate - lowConfig := presets[AudioQualityLow] - mediumConfig := presets[AudioQualityMedium] - highConfig := presets[AudioQualityHigh] - ultraConfig := presets[AudioQualityUltra] - - assert.Less(t, lowConfig.Bitrate, mediumConfig.Bitrate) - assert.Less(t, mediumConfig.Bitrate, highConfig.Bitrate) - assert.Less(t, highConfig.Bitrate, ultraConfig.Bitrate) -} - -func TestMicrophoneQualityPresets(t *testing.T) { - presets := GetMicrophoneQualityPresets() - require.NotEmpty(t, presets) - - // Test that all quality levels have presets - for quality := AudioQualityLow; quality <= AudioQualityUltra; quality++ { - config, exists := presets[quality] - require.True(t, exists, "Microphone preset should exist for quality %d", quality) - assert.Equal(t, quality, config.Quality) - assert.Equal(t, 1, config.Channels) // Always mono - assert.Greater(t, config.Bitrate, 0) - assert.Greater(t, config.SampleRate, 0) - } -} - -func TestAudioMetrics(t *testing.T) { - // Test initial metrics - metrics := GetAudioMetrics() - assert.GreaterOrEqual(t, metrics.FramesReceived, int64(0)) - assert.GreaterOrEqual(t, metrics.FramesDropped, int64(0)) - assert.GreaterOrEqual(t, metrics.BytesProcessed, int64(0)) - assert.GreaterOrEqual(t, metrics.ConnectionDrops, int64(0)) - - // Test recording metrics - RecordFrameReceived(1024) - metrics = GetAudioMetrics() - assert.Greater(t, metrics.BytesProcessed, int64(0)) - assert.Greater(t, metrics.FramesReceived, int64(0)) - - RecordFrameDropped() - metrics = GetAudioMetrics() - assert.Greater(t, metrics.FramesDropped, int64(0)) - - RecordConnectionDrop() - metrics = GetAudioMetrics() - assert.Greater(t, metrics.ConnectionDrops, int64(0)) -} - -func TestMaxAudioFrameSize(t *testing.T) { - frameSize := GetMaxAudioFrameSize() - assert.Greater(t, frameSize, 0) - assert.Equal(t, GetConfig().MaxAudioFrameSize, frameSize) -} - -func TestMetricsUpdateInterval(t *testing.T) { - // Test getting current interval - interval := GetMetricsUpdateInterval() - assert.Greater(t, interval, time.Duration(0)) - - // Test setting new interval - newInterval := 2 * time.Second - SetMetricsUpdateInterval(newInterval) - updatedInterval := GetMetricsUpdateInterval() - assert.Equal(t, newInterval, updatedInterval) -} - -func TestAudioConfigConsistency(t *testing.T) { - // Test that setting audio quality updates the config consistently - for quality := AudioQualityLow; quality <= AudioQualityUltra; quality++ { - SetAudioQuality(quality) - config := GetAudioConfig() - presets := GetAudioQualityPresets() - expectedConfig := presets[quality] - - assert.Equal(t, expectedConfig.Quality, config.Quality) - assert.Equal(t, expectedConfig.Bitrate, config.Bitrate) - assert.Equal(t, expectedConfig.SampleRate, config.SampleRate) - assert.Equal(t, expectedConfig.Channels, config.Channels) - assert.Equal(t, expectedConfig.FrameSize, config.FrameSize) - } -} - -func TestMicrophoneConfigConsistency(t *testing.T) { - // Test that setting microphone quality updates the config consistently - for quality := AudioQualityLow; quality <= AudioQualityUltra; quality++ { - SetMicrophoneQuality(quality) - config := GetMicrophoneConfig() - presets := GetMicrophoneQualityPresets() - expectedConfig := presets[quality] - - assert.Equal(t, expectedConfig.Quality, config.Quality) - assert.Equal(t, expectedConfig.Bitrate, config.Bitrate) - assert.Equal(t, expectedConfig.SampleRate, config.SampleRate) - assert.Equal(t, expectedConfig.Channels, config.Channels) - assert.Equal(t, expectedConfig.FrameSize, config.FrameSize) - } -} - -// Benchmark tests -func BenchmarkGetAudioConfig(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = GetAudioConfig() - } -} - -func BenchmarkGetAudioMetrics(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = GetAudioMetrics() - } -} - -func BenchmarkRecordFrameReceived(b *testing.B) { - for i := 0; i < b.N; i++ { - RecordFrameReceived(1024) - } -} - -func BenchmarkSetAudioQuality(b *testing.B) { - qualities := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} - b.ResetTimer() - - for i := 0; i < b.N; i++ { - SetAudioQuality(qualities[i%len(qualities)]) - } -} - -// TestAudioUsbGadgetIntegration tests audio functionality with USB gadget reconfiguration -// This test simulates the production scenario where audio devices are enabled/disabled -// through USB gadget configuration changes -func TestAudioUsbGadgetIntegration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - tests := []struct { - name string - initialAudioEnabled bool - newAudioEnabled bool - expectedTransition string - }{ - { - name: "EnableAudio", - initialAudioEnabled: false, - newAudioEnabled: true, - expectedTransition: "disabled_to_enabled", - }, - { - name: "DisableAudio", - initialAudioEnabled: true, - newAudioEnabled: false, - expectedTransition: "enabled_to_disabled", - }, - { - name: "NoChange", - initialAudioEnabled: true, - newAudioEnabled: true, - expectedTransition: "no_change", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Simulate initial USB device configuration - initialDevices := &usbgadget.Devices{ - Keyboard: true, - AbsoluteMouse: true, - RelativeMouse: true, - MassStorage: true, - Audio: tt.initialAudioEnabled, - } - - // Simulate new USB device configuration - newDevices := &usbgadget.Devices{ - Keyboard: true, - AbsoluteMouse: true, - RelativeMouse: true, - MassStorage: true, - Audio: tt.newAudioEnabled, - } - - // Test audio configuration validation - err := validateAudioDeviceConfiguration(tt.newAudioEnabled) - assert.NoError(t, err, "Audio configuration should be valid") - - // Test audio state transition simulation - transition := simulateAudioStateTransition(ctx, initialDevices, newDevices) - assert.Equal(t, tt.expectedTransition, transition, "Audio state transition should match expected") - - // Test that audio configuration is consistent after transition - if tt.newAudioEnabled { - config := GetAudioConfig() - assert.Greater(t, config.Bitrate, 0, "Audio bitrate should be positive when enabled") - assert.Greater(t, config.SampleRate, 0, "Audio sample rate should be positive when enabled") - } - }) - } -} - -// validateAudioDeviceConfiguration simulates the audio validation that happens in production -func validateAudioDeviceConfiguration(enabled bool) error { - if !enabled { - return nil // No validation needed when disabled - } - - // Simulate audio device availability checks - // In production, this would check for ALSA devices, audio hardware, etc. - config := GetAudioConfig() - if config.Bitrate <= 0 { - return assert.AnError - } - if config.SampleRate <= 0 { - return assert.AnError - } - - return nil -} - -// simulateAudioStateTransition simulates the audio process management during USB reconfiguration -func simulateAudioStateTransition(ctx context.Context, initial, new *usbgadget.Devices) string { - previousAudioEnabled := initial.Audio - newAudioEnabled := new.Audio - - if previousAudioEnabled == newAudioEnabled { - return "no_change" - } - - if !newAudioEnabled { - // Simulate stopping audio processes - // In production, this would stop AudioInputManager and audioSupervisor - time.Sleep(10 * time.Millisecond) // Simulate process stop time - return "enabled_to_disabled" - } - - if newAudioEnabled { - // Simulate starting audio processes after USB reconfiguration - // In production, this would start audioSupervisor and broadcast events - time.Sleep(10 * time.Millisecond) // Simulate process start time - return "disabled_to_enabled" - } - - return "unknown" -} - -// TestAudioUsbGadgetTimeout tests that audio operations don't timeout during USB reconfiguration -func TestAudioUsbGadgetTimeout(t *testing.T) { - if testing.Short() { - t.Skip("Skipping timeout test in short mode") - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Test that audio configuration changes complete within reasonable time - start := time.Now() - - // Simulate multiple rapid USB device configuration changes - for i := 0; i < 10; i++ { - audioEnabled := i%2 == 0 - devices := &usbgadget.Devices{ - Keyboard: true, - AbsoluteMouse: true, - RelativeMouse: true, - MassStorage: true, - Audio: audioEnabled, - } - - err := validateAudioDeviceConfiguration(devices.Audio) - assert.NoError(t, err, "Audio validation should not fail") - - // Ensure we don't timeout - select { - case <-ctx.Done(): - t.Fatal("Audio configuration test timed out") - default: - // Continue - } - } - - elapsed := time.Since(start) - t.Logf("Audio USB gadget configuration test completed in %v", elapsed) - assert.Less(t, elapsed, 3*time.Second, "Audio configuration should complete quickly") -} diff --git a/internal/audio/base_manager.go b/internal/audio/base_manager.go index e0a6033c..3023fd32 100644 --- a/internal/audio/base_manager.go +++ b/internal/audio/base_manager.go @@ -79,48 +79,14 @@ func (bam *BaseAudioManager) getBaseMetrics() BaseAudioMetrics { // recordFrameProcessed records a processed frame with simplified tracking func (bam *BaseAudioManager) recordFrameProcessed(bytes int) { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Direct atomic updates to avoid sampling complexity in critical path - atomic.AddInt64(&bam.metrics.FramesProcessed, 1) - atomic.AddInt64(&bam.metrics.BytesProcessed, int64(bytes)) - - // Always update timestamp for accurate last frame tracking - bam.metrics.LastFrameTime = time.Now() } // recordFrameDropped records a dropped frame with simplified tracking func (bam *BaseAudioManager) recordFrameDropped() { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Direct atomic update to avoid sampling complexity in critical path - atomic.AddInt64(&bam.metrics.FramesDropped, 1) } // updateLatency updates the average latency func (bam *BaseAudioManager) updateLatency(latency time.Duration) { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Simple moving average - could be enhanced with more sophisticated algorithms - currentAvg := bam.metrics.AverageLatency - if currentAvg == 0 { - bam.metrics.AverageLatency = latency - } else { - // Weighted average: 90% old + 10% new - bam.metrics.AverageLatency = time.Duration(float64(currentAvg)*0.9 + float64(latency)*0.1) - } } // logComponentStart logs component start with consistent format diff --git a/internal/audio/cgo_audio.go b/internal/audio/cgo_audio.go index 77d4396d..f412ff0e 100644 --- a/internal/audio/cgo_audio.go +++ b/internal/audio/cgo_audio.go @@ -750,10 +750,6 @@ type AudioConfigCache struct { inputProcessingTimeoutMS atomic.Int32 maxRestartAttempts atomic.Int32 - // Performance flags for hot path optimization - enableMetricsCollection atomic.Bool - enableGoroutineMonitoring atomic.Bool - // Batch processing related values BatchProcessingTimeout time.Duration BatchProcessorFramesPerBatch int @@ -829,10 +825,6 @@ func (c *AudioConfigCache) Update() { c.minOpusBitrate.Store(int32(config.MinOpusBitrate)) c.maxOpusBitrate.Store(int32(config.MaxOpusBitrate)) - // Update performance flags for hot path optimization - c.enableMetricsCollection.Store(config.EnableMetricsCollection) - c.enableGoroutineMonitoring.Store(config.EnableGoroutineMonitoring) - // Update batch processing related values c.BatchProcessingTimeout = 100 * time.Millisecond // Fixed timeout for batch processing c.BatchProcessorFramesPerBatch = config.BatchProcessorFramesPerBatch @@ -887,18 +879,6 @@ func (c *AudioConfigCache) GetBufferTooLargeError() error { return c.bufferTooLargeDecodeWrite } -// GetEnableMetricsCollection returns the cached EnableMetricsCollection flag for hot path optimization -func (c *AudioConfigCache) GetEnableMetricsCollection() bool { - c.Update() // Ensure cache is current - return c.enableMetricsCollection.Load() -} - -// GetEnableGoroutineMonitoring returns the cached EnableGoroutineMonitoring flag for hot path optimization -func (c *AudioConfigCache) GetEnableGoroutineMonitoring() bool { - c.Update() // Ensure cache is current - return c.enableGoroutineMonitoring.Load() -} - // Removed duplicate config caching system - using AudioConfigCache instead func cgoAudioReadEncode(buf []byte) (int, error) { @@ -1058,8 +1038,7 @@ var ( batchProcessingCount atomic.Int64 batchFrameCount atomic.Int64 batchProcessingTime atomic.Int64 - // Flag to control time tracking overhead - enableBatchTimeTracking atomic.Bool + // Batch time tracking removed ) // GetBufferFromPool gets a buffer from the pool with at least the specified capacity @@ -1264,7 +1243,8 @@ func BatchReadEncode(batchSize int) ([][]byte, error) { // Track batch processing statistics - only if enabled var startTime time.Time - trackTime := enableBatchTimeTracking.Load() + // Batch time tracking removed + trackTime := false if trackTime { startTime = time.Now() } @@ -1331,7 +1311,8 @@ func BatchDecodeWrite(frames [][]byte) error { // Track batch processing statistics - only if enabled var startTime time.Time - trackTime := enableBatchTimeTracking.Load() + // Batch time tracking removed + trackTime := false if trackTime { startTime = time.Now() } diff --git a/internal/audio/cgo_audio_stub.go b/internal/audio/cgo_audio_stub.go deleted file mode 100644 index 83c22d14..00000000 --- a/internal/audio/cgo_audio_stub.go +++ /dev/null @@ -1,48 +0,0 @@ -//go:build !cgo - -package audio - -import "errors" - -// Stub implementations for linting (no CGO dependencies) - -func cgoAudioInit() error { - return errors.New("audio not available in lint mode") -} - -func cgoAudioClose() { - // No-op -} - -func cgoAudioReadEncode(buf []byte) (int, error) { - return 0, errors.New("audio not available in lint mode") -} - -func cgoAudioPlaybackInit() error { - return errors.New("audio not available in lint mode") -} - -func cgoAudioPlaybackClose() { - // No-op -} - -func cgoAudioDecodeWrite(buf []byte) (int, error) { - return 0, errors.New("audio not available in lint mode") -} - -// cgoAudioDecodeWriteWithBuffers is a stub implementation for the optimized decode-write function -func cgoAudioDecodeWriteWithBuffers(opusData []byte, pcmBuffer []byte) (int, error) { - return 0, errors.New("audio not available in lint mode") -} - -// Uppercase aliases for external API compatibility - -var ( - CGOAudioInit = cgoAudioInit - CGOAudioClose = cgoAudioClose - CGOAudioReadEncode = cgoAudioReadEncode - CGOAudioPlaybackInit = cgoAudioPlaybackInit - CGOAudioPlaybackClose = cgoAudioPlaybackClose - CGOAudioDecodeWriteLegacy = cgoAudioDecodeWrite - CGOAudioDecodeWrite = cgoAudioDecodeWriteWithBuffers -) diff --git a/internal/audio/config_constants.go b/internal/audio/config_constants.go index 7ad56f4a..f0cbb245 100644 --- a/internal/audio/config_constants.go +++ b/internal/audio/config_constants.go @@ -908,23 +908,6 @@ type AudioConfigConstants struct { // Default true enables pre-warming for optimal user experience EnableSubprocessPrewarming bool // Enable subprocess pre-warming (default: true) - // Performance Mode Configuration - // These flags control overhead-inducing features for production optimization - EnableMetricsCollection bool // Enable detailed metrics collection (default: true) - EnableLatencyProfiling bool // Enable latency profiling and detailed tracing (default: false) - EnableGoroutineMonitoring bool // Enable goroutine monitoring (default: false) - EnableBatchTimeTracking bool // Enable batch processing time tracking (default: false) - EnableDetailedLogging bool // Enable detailed debug logging (default: false) - - // Metrics Collection Optimization - MetricsFlushInterval int // Batched metrics flush interval (default: 10) - MetricsForceFlushNanos int64 // Force flush after nanoseconds (default: 100ms) - MetricsSamplingRate float64 // Sampling rate for metrics (0.0-1.0, default: 1.0) - - // Latency Profiling Optimization - LatencyProfilingSamplingRate float64 // Latency profiling sampling rate (default: 0.01 = 1%) - LatencyProfilingInterval time.Duration // Latency profiling report interval (default: 60s) - // Priority Scheduler Configuration - Settings for process priority management // Used in: priority_scheduler.go for system priority control // Impact: Controls valid range for process priority adjustments @@ -2574,76 +2557,7 @@ func DefaultAudioConfig() *AudioConfigConstants { GoroutineMonitorInterval: 30 * time.Second, // 30s monitoring interval // Performance Configuration Flags - Production optimizations - // Used in: Production environments to reduce overhead and improve performance - // Impact: Controls which performance monitoring features are enabled - // EnableMetricsCollection controls detailed metrics collection. - // Used in: metrics.go, granular_metrics.go for performance tracking - // Impact: When disabled, reduces atomic operations and memory overhead. - // Default true for development, should be false in production for optimal performance. - EnableMetricsCollection: true, // Enable detailed metrics collection (default: true) - - // EnableLatencyProfiling controls latency profiling and detailed tracing. - // Used in: latency_profiler.go for performance analysis - // Impact: When disabled, eliminates profiling overhead and reduces CPU usage. - // Default false to minimize overhead in production environments. - EnableLatencyProfiling: false, // Enable latency profiling and detailed tracing (default: false) - - // EnableGoroutineMonitoring controls goroutine monitoring. - // Used in: goroutine_monitor.go for tracking goroutine health - // Impact: When disabled, reduces monitoring overhead and CPU usage. - // Default false to minimize overhead in production environments. - EnableGoroutineMonitoring: false, // Enable goroutine monitoring (default: false) - - // EnableBatchTimeTracking controls batch processing time tracking. - // Used in: batch_audio.go for performance analysis - // Impact: When disabled, eliminates time tracking overhead. - // Default false to minimize overhead in production environments. - EnableBatchTimeTracking: false, // Enable batch processing time tracking (default: false) - - // EnableDetailedLogging controls detailed debug logging. - // Used in: Throughout audio system for debugging - // Impact: When disabled, reduces logging overhead and improves performance. - // Default false to minimize overhead in production environments. - EnableDetailedLogging: false, // Enable detailed debug logging (default: false) - - // Metrics Configuration - Batching and sampling for performance - // Used in: metrics.go for optimizing metrics collection overhead - // Impact: Controls how frequently metrics are updated and flushed - - // MetricsFlushInterval defines batched metrics flush interval. - // Used in: metrics.go for batching metrics updates - // Impact: Higher values reduce update frequency but increase memory usage. - // Default 10 provides good balance between performance and memory. - MetricsFlushInterval: 10, // Batched metrics flush interval (default: 10) - - // MetricsForceFlushNanos defines force flush after nanoseconds. - // Used in: metrics.go for ensuring metrics are not delayed too long - // Impact: Prevents metrics from being delayed indefinitely. - // Default 100ms ensures reasonable freshness while allowing batching. - MetricsForceFlushNanos: 100000000, // Force flush after nanoseconds (default: 100ms) - - // MetricsSamplingRate defines sampling rate for metrics. - // Used in: metrics.go for reducing metrics collection overhead - // Impact: Values < 1.0 reduce overhead by sampling only a fraction of events. - // Default 1.0 collects all metrics, set to 0.1 in production for 90% reduction. - MetricsSamplingRate: 1.0, // Sampling rate for metrics (0.0-1.0, default: 1.0) - - // Latency Profiling Configuration - Sampling for performance - // Used in: latency_profiler.go for optimizing profiling overhead - // Impact: Controls how frequently latency measurements are taken - - // LatencyProfilingSamplingRate defines latency profiling sampling rate. - // Used in: latency_profiler.go for reducing profiling overhead - // Impact: Values < 1.0 significantly reduce profiling overhead. - // Default 0.01 (1%) provides useful data with minimal overhead. - LatencyProfilingSamplingRate: 0.01, // Latency profiling sampling rate (default: 0.01 = 1%) - - // LatencyProfilingInterval defines latency profiling report interval. - // Used in: latency_profiler.go for controlling report frequency - // Impact: Longer intervals reduce reporting overhead. - // Default 60s provides reasonable reporting frequency. - LatencyProfilingInterval: 60 * time.Second, // Latency profiling report interval (default: 60s) } } diff --git a/internal/audio/events.go b/internal/audio/events.go index 76ba7e5a..6edf24f6 100644 --- a/internal/audio/events.go +++ b/internal/audio/events.go @@ -2,7 +2,6 @@ package audio import ( "context" - "fmt" "strings" "sync" "time" @@ -17,13 +16,9 @@ import ( type AudioEventType string const ( - AudioEventMuteChanged AudioEventType = "audio-mute-changed" - AudioEventMetricsUpdate AudioEventType = "audio-metrics-update" - AudioEventMicrophoneState AudioEventType = "microphone-state-changed" - AudioEventMicrophoneMetrics AudioEventType = "microphone-metrics-update" - AudioEventProcessMetrics AudioEventType = "audio-process-metrics" - AudioEventMicProcessMetrics AudioEventType = "microphone-process-metrics" - AudioEventDeviceChanged AudioEventType = "audio-device-changed" + AudioEventMuteChanged AudioEventType = "audio-mute-changed" + AudioEventMicrophoneState AudioEventType = "microphone-state-changed" + AudioEventDeviceChanged AudioEventType = "audio-device-changed" ) // AudioEvent represents a WebSocket audio event @@ -37,43 +32,12 @@ type AudioMuteData struct { Muted bool `json:"muted"` } -// AudioMetricsData represents audio metrics data -type AudioMetricsData struct { - FramesReceived int64 `json:"frames_received"` - FramesDropped int64 `json:"frames_dropped"` - BytesProcessed int64 `json:"bytes_processed"` - LastFrameTime string `json:"last_frame_time"` - ConnectionDrops int64 `json:"connection_drops"` - AverageLatency string `json:"average_latency"` -} - // MicrophoneStateData represents microphone state data type MicrophoneStateData struct { Running bool `json:"running"` SessionActive bool `json:"session_active"` } -// MicrophoneMetricsData represents microphone metrics data -type MicrophoneMetricsData struct { - FramesSent int64 `json:"frames_sent"` - FramesDropped int64 `json:"frames_dropped"` - BytesProcessed int64 `json:"bytes_processed"` - LastFrameTime string `json:"last_frame_time"` - ConnectionDrops int64 `json:"connection_drops"` - AverageLatency string `json:"average_latency"` -} - -// ProcessMetricsData represents process metrics data for WebSocket events -type ProcessMetricsData struct { - PID int `json:"pid"` - CPUPercent float64 `json:"cpu_percent"` - MemoryRSS int64 `json:"memory_rss"` - MemoryVMS int64 `json:"memory_vms"` - MemoryPercent float64 `json:"memory_percent"` - Running bool `json:"running"` - ProcessName string `json:"process_name"` -} - // AudioDeviceChangedData represents audio device configuration change data type AudioDeviceChangedData struct { Enabled bool `json:"enabled"` @@ -106,12 +70,6 @@ func initializeBroadcaster() { subscribers: make(map[string]*AudioEventSubscriber), logger: &l, } - - // Start metrics broadcasting goroutine - go audioEventBroadcaster.startMetricsBroadcasting() - - // Start granular metrics logging with same interval as metrics broadcasting - // StartGranularMetricsLogging(GetMetricsUpdateInterval()) // Disabled to reduce log pollution } // InitializeAudioEventBroadcaster initializes the global audio event broadcaster @@ -218,90 +176,6 @@ func (aeb *AudioEventBroadcaster) sendInitialState(connectionID string) { }, } aeb.sendToSubscriber(subscriber, micStateEvent) - - // Send current metrics - aeb.sendCurrentMetrics(subscriber) -} - -// convertAudioMetricsToEventDataWithLatencyMs converts internal audio metrics to AudioMetricsData with millisecond latency formatting -func convertAudioMetricsToEventDataWithLatencyMs(metrics AudioMetrics) AudioMetricsData { - return AudioMetricsData{ - FramesReceived: metrics.FramesReceived, - FramesDropped: metrics.FramesDropped, - BytesProcessed: metrics.BytesProcessed, - LastFrameTime: metrics.LastFrameTime.Format(GetConfig().EventTimeFormatString), - ConnectionDrops: metrics.ConnectionDrops, - AverageLatency: fmt.Sprintf("%.1fms", float64(metrics.AverageLatency.Nanoseconds())/1e6), - } -} - -// convertAudioInputMetricsToEventDataWithLatencyMs converts internal audio input metrics to MicrophoneMetricsData with millisecond latency formatting -func convertAudioInputMetricsToEventDataWithLatencyMs(metrics AudioInputMetrics) MicrophoneMetricsData { - return MicrophoneMetricsData{ - FramesSent: metrics.FramesSent, - FramesDropped: metrics.FramesDropped, - BytesProcessed: metrics.BytesProcessed, - LastFrameTime: metrics.LastFrameTime.Format(GetConfig().EventTimeFormatString), - ConnectionDrops: metrics.ConnectionDrops, - AverageLatency: fmt.Sprintf("%.1fms", float64(metrics.AverageLatency.Nanoseconds())/1e6), - } -} - -// convertProcessMetricsToEventData converts internal process metrics to ProcessMetricsData for events -func convertProcessMetricsToEventData(metrics ProcessMetrics, running bool) ProcessMetricsData { - return ProcessMetricsData{ - PID: metrics.PID, - CPUPercent: metrics.CPUPercent, - MemoryRSS: metrics.MemoryRSS, - MemoryVMS: metrics.MemoryVMS, - MemoryPercent: metrics.MemoryPercent, - Running: running, - ProcessName: metrics.ProcessName, - } -} - -// createProcessMetricsData creates ProcessMetricsData from ProcessMetrics with running status -func createProcessMetricsData(metrics *ProcessMetrics, running bool, processName string) ProcessMetricsData { - if metrics == nil { - return ProcessMetricsData{ - PID: 0, - CPUPercent: 0.0, - MemoryRSS: 0, - MemoryVMS: 0, - MemoryPercent: 0.0, - Running: false, - ProcessName: processName, - } - } - return ProcessMetricsData{ - PID: metrics.PID, - CPUPercent: metrics.CPUPercent, - MemoryRSS: metrics.MemoryRSS, - MemoryVMS: metrics.MemoryVMS, - MemoryPercent: metrics.MemoryPercent, - Running: running, - ProcessName: metrics.ProcessName, - } -} - -// getInactiveProcessMetrics returns ProcessMetricsData for an inactive audio input process -func getInactiveProcessMetrics() ProcessMetricsData { - return createProcessMetricsData(nil, false, "audio-input-server") -} - -// getActiveAudioInputSupervisor safely retrieves the audio input supervisor if session is active -func getActiveAudioInputSupervisor() *AudioInputSupervisor { - sessionProvider := GetSessionProvider() - if !sessionProvider.IsSessionActive() { - return nil - } - - inputManager := sessionProvider.GetAudioInputManager() - if inputManager == nil { - return nil - } - - return inputManager.GetSupervisor() } // createAudioEvent creates an AudioEvent @@ -312,128 +186,6 @@ func createAudioEvent(eventType AudioEventType, data interface{}) AudioEvent { } } -func (aeb *AudioEventBroadcaster) getMicrophoneProcessMetrics() ProcessMetricsData { - inputSupervisor := getActiveAudioInputSupervisor() - if inputSupervisor == nil { - return getInactiveProcessMetrics() - } - - processMetrics := inputSupervisor.GetProcessMetrics() - if processMetrics == nil { - return getInactiveProcessMetrics() - } - - // If process is running but CPU is 0%, it means we're waiting for the second sample - // to calculate CPU percentage. Return metrics with correct running status. - if inputSupervisor.IsRunning() && processMetrics.CPUPercent == 0.0 { - return createProcessMetricsData(processMetrics, true, processMetrics.ProcessName) - } - - // Subprocess is running, return actual metrics - return createProcessMetricsData(processMetrics, inputSupervisor.IsRunning(), processMetrics.ProcessName) -} - -// sendCurrentMetrics sends current audio and microphone metrics to a subscriber -func (aeb *AudioEventBroadcaster) sendCurrentMetrics(subscriber *AudioEventSubscriber) { - // Send audio metrics - audioMetrics := GetAudioMetrics() - audioMetricsEvent := createAudioEvent(AudioEventMetricsUpdate, convertAudioMetricsToEventDataWithLatencyMs(audioMetrics)) - aeb.sendToSubscriber(subscriber, audioMetricsEvent) - - // Send audio process metrics - if outputSupervisor := GetAudioOutputSupervisor(); outputSupervisor != nil { - if processMetrics := outputSupervisor.GetProcessMetrics(); processMetrics != nil { - audioProcessEvent := createAudioEvent(AudioEventProcessMetrics, convertProcessMetricsToEventData(*processMetrics, outputSupervisor.IsRunning())) - aeb.sendToSubscriber(subscriber, audioProcessEvent) - } - } - - // Send microphone metrics using session provider - sessionProvider := GetSessionProvider() - if sessionProvider.IsSessionActive() { - if inputManager := sessionProvider.GetAudioInputManager(); inputManager != nil { - micMetrics := inputManager.GetMetrics() - micMetricsEvent := createAudioEvent(AudioEventMicrophoneMetrics, convertAudioInputMetricsToEventDataWithLatencyMs(micMetrics)) - aeb.sendToSubscriber(subscriber, micMetricsEvent) - } - } - - // Send microphone process metrics (always send, even when subprocess is not running) - micProcessEvent := createAudioEvent(AudioEventMicProcessMetrics, aeb.getMicrophoneProcessMetrics()) - aeb.sendToSubscriber(subscriber, micProcessEvent) -} - -// startMetricsBroadcasting starts a goroutine that periodically broadcasts metrics -func (aeb *AudioEventBroadcaster) startMetricsBroadcasting() { - // Use centralized interval to match process monitor frequency for synchronized metrics - ticker := time.NewTicker(GetMetricsUpdateInterval()) - defer ticker.Stop() - - for range ticker.C { - // Skip metrics broadcasting if metrics collection is disabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - continue - } - - aeb.mutex.RLock() - subscriberCount := len(aeb.subscribers) - - // Early exit if no subscribers to save CPU - if subscriberCount == 0 { - aeb.mutex.RUnlock() - continue - } - - // Create a copy for safe iteration - subscribersCopy := make([]*AudioEventSubscriber, 0, subscriberCount) - for _, sub := range aeb.subscribers { - subscribersCopy = append(subscribersCopy, sub) - } - aeb.mutex.RUnlock() - - // Pre-check for cancelled contexts to avoid unnecessary work - activeSubscribers := 0 - for _, sub := range subscribersCopy { - if sub.ctx.Err() == nil { - activeSubscribers++ - } - } - - // Skip metrics gathering if no active subscribers - if activeSubscribers == 0 { - continue - } - - // Broadcast audio metrics - audioMetrics := GetAudioMetrics() - audioMetricsEvent := createAudioEvent(AudioEventMetricsUpdate, convertAudioMetricsToEventDataWithLatencyMs(audioMetrics)) - aeb.broadcast(audioMetricsEvent) - - // Broadcast microphone metrics if available using session provider - sessionProvider := GetSessionProvider() - if sessionProvider.IsSessionActive() { - if inputManager := sessionProvider.GetAudioInputManager(); inputManager != nil { - micMetrics := inputManager.GetMetrics() - micMetricsEvent := createAudioEvent(AudioEventMicrophoneMetrics, convertAudioInputMetricsToEventDataWithLatencyMs(micMetrics)) - aeb.broadcast(micMetricsEvent) - } - } - - // Broadcast audio process metrics - if outputSupervisor := GetAudioOutputSupervisor(); outputSupervisor != nil { - if processMetrics := outputSupervisor.GetProcessMetrics(); processMetrics != nil { - audioProcessEvent := createAudioEvent(AudioEventProcessMetrics, convertProcessMetricsToEventData(*processMetrics, outputSupervisor.IsRunning())) - aeb.broadcast(audioProcessEvent) - } - } - - // Broadcast microphone process metrics (always broadcast, even when subprocess is not running) - micProcessEvent := createAudioEvent(AudioEventMicProcessMetrics, aeb.getMicrophoneProcessMetrics()) - aeb.broadcast(micProcessEvent) - } -} - // broadcast sends an event to all subscribers func (aeb *AudioEventBroadcaster) broadcast(event AudioEvent) { aeb.mutex.RLock() diff --git a/internal/audio/goroutine_monitor.go b/internal/audio/goroutine_monitor.go index 0ed432ba..00dd3743 100644 --- a/internal/audio/goroutine_monitor.go +++ b/internal/audio/goroutine_monitor.go @@ -133,13 +133,7 @@ func GetGoroutineMonitor() *GoroutineMonitor { // StartGoroutineMonitoring starts the global goroutine monitor func StartGoroutineMonitoring() { - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableGoroutineMonitoring() { - return - } - - monitor := GetGoroutineMonitor() - monitor.Start() + // Goroutine monitoring disabled } // StopGoroutineMonitoring stops the global goroutine monitor diff --git a/internal/audio/granular_metrics_test.go b/internal/audio/granular_metrics_test.go deleted file mode 100644 index 046310e7..00000000 --- a/internal/audio/granular_metrics_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package audio - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestGranularMetricsCollector tests the GranularMetricsCollector functionality -func TestGranularMetricsCollector(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"GetGranularMetricsCollector", testGetGranularMetricsCollector}, - {"ConcurrentCollectorAccess", testConcurrentCollectorAccess}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -// testGetGranularMetricsCollector tests singleton behavior -func testGetGranularMetricsCollector(t *testing.T) { - collector1 := GetGranularMetricsCollector() - collector2 := GetGranularMetricsCollector() - - require.NotNil(t, collector1) - require.NotNil(t, collector2) - assert.Same(t, collector1, collector2, "Should return the same singleton instance") -} - -// testConcurrentCollectorAccess tests thread safety of the collector -func testConcurrentCollectorAccess(t *testing.T) { - collector := GetGranularMetricsCollector() - require.NotNil(t, collector) - - const numGoroutines = 10 - const operationsPerGoroutine = 50 - - var wg sync.WaitGroup - wg.Add(numGoroutines) - - // Concurrent buffer pool operations - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - for j := 0; j < operationsPerGoroutine; j++ { - // Test buffer pool operations - latency := time.Duration(id*operationsPerGoroutine+j) * time.Microsecond - collector.RecordFramePoolGet(latency, true) - collector.RecordFramePoolPut(latency, 1024) - } - }(i) - } - - wg.Wait() - - // Verify collector is still functional - efficiency := collector.GetBufferPoolEfficiency() - assert.NotNil(t, efficiency) -} - -func BenchmarkGranularMetricsCollector(b *testing.B) { - collector := GetGranularMetricsCollector() - - b.Run("RecordFramePoolGet", func(b *testing.B) { - latency := 5 * time.Millisecond - b.ResetTimer() - for i := 0; i < b.N; i++ { - collector.RecordFramePoolGet(latency, true) - } - }) - - b.Run("RecordFramePoolPut", func(b *testing.B) { - latency := 5 * time.Millisecond - b.ResetTimer() - for i := 0; i < b.N; i++ { - collector.RecordFramePoolPut(latency, 1024) - } - }) - - b.Run("GetBufferPoolEfficiency", func(b *testing.B) { - // Pre-populate with some data - for i := 0; i < 100; i++ { - collector.RecordFramePoolGet(time.Duration(i)*time.Microsecond, true) - collector.RecordFramePoolPut(time.Duration(i)*time.Microsecond, 1024) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = collector.GetBufferPoolEfficiency() - } - }) -} diff --git a/internal/audio/input.go b/internal/audio/input.go index d076a335..8bbade51 100644 --- a/internal/audio/input.go +++ b/internal/audio/input.go @@ -109,18 +109,8 @@ func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error { } if err != nil { - cachedConfig := GetCachedConfig() - if cachedConfig.GetEnableMetricsCollection() { - atomic.AddInt64(&aim.metrics.FramesDropped, 1) - } return err } - - // Update metrics - cachedConfig := GetCachedConfig() - if cachedConfig.GetEnableMetricsCollection() { - atomic.AddInt64(&aim.framesSent, 1) - } aim.recordFrameProcessed(len(frame)) aim.updateLatency(processingTime) diff --git a/internal/audio/input_ipc_manager_test.go b/internal/audio/input_ipc_manager_test.go deleted file mode 100644 index f31d4406..00000000 --- a/internal/audio/input_ipc_manager_test.go +++ /dev/null @@ -1,277 +0,0 @@ -package audio - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestAudioInputIPCManager tests the AudioInputIPCManager component -func TestAudioInputIPCManager(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"Start", testAudioInputIPCManagerStart}, - {"Stop", testAudioInputIPCManagerStop}, - {"StartStop", testAudioInputIPCManagerStartStop}, - {"IsRunning", testAudioInputIPCManagerIsRunning}, - {"IsReady", testAudioInputIPCManagerIsReady}, - {"GetMetrics", testAudioInputIPCManagerGetMetrics}, - {"ConcurrentOperations", testAudioInputIPCManagerConcurrent}, - {"MultipleStarts", testAudioInputIPCManagerMultipleStarts}, - {"MultipleStops", testAudioInputIPCManagerMultipleStops}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -func testAudioInputIPCManagerStart(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Test initial state - assert.False(t, manager.IsRunning()) - assert.False(t, manager.IsReady()) - - // Test start - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Cleanup - manager.Stop() -} - -func testAudioInputIPCManagerStop(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Start first - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Test stop - manager.Stop() - assert.False(t, manager.IsRunning()) - assert.False(t, manager.IsReady()) -} - -func testAudioInputIPCManagerStartStop(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Test multiple start/stop cycles - for i := 0; i < 3; i++ { - // Start - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Stop - manager.Stop() - assert.False(t, manager.IsRunning()) - } -} - -func testAudioInputIPCManagerIsRunning(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Initially not running - assert.False(t, manager.IsRunning()) - - // Start and check - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Stop and check - manager.Stop() - assert.False(t, manager.IsRunning()) -} - -func testAudioInputIPCManagerIsReady(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Initially not ready - assert.False(t, manager.IsReady()) - - // Start and check ready state - err := manager.Start() - require.NoError(t, err) - - // Give some time for initialization - time.Sleep(100 * time.Millisecond) - - // Stop - manager.Stop() - assert.False(t, manager.IsReady()) -} - -func testAudioInputIPCManagerGetMetrics(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Test metrics when not running - metrics := manager.GetMetrics() - assert.NotNil(t, metrics) - - // Start and test metrics - err := manager.Start() - require.NoError(t, err) - - metrics = manager.GetMetrics() - assert.NotNil(t, metrics) - - // Cleanup - manager.Stop() -} - -func testAudioInputIPCManagerConcurrent(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - var wg sync.WaitGroup - const numGoroutines = 10 - - // Test concurrent starts - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - manager.Start() - }() - } - wg.Wait() - - // Should be running - assert.True(t, manager.IsRunning()) - - // Test concurrent stops - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - manager.Stop() - }() - } - wg.Wait() - - // Should be stopped - assert.False(t, manager.IsRunning()) -} - -func testAudioInputIPCManagerMultipleStarts(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // First start should succeed - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Subsequent starts should be no-op - err = manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - err = manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Cleanup - manager.Stop() -} - -func testAudioInputIPCManagerMultipleStops(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Start first - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // First stop should work - manager.Stop() - assert.False(t, manager.IsRunning()) - - // Subsequent stops should be no-op - manager.Stop() - assert.False(t, manager.IsRunning()) - - manager.Stop() - assert.False(t, manager.IsRunning()) -} - -// TestAudioInputIPCMetrics tests the AudioInputMetrics functionality -func TestAudioInputIPCMetrics(t *testing.T) { - metrics := &AudioInputMetrics{} - - // Test initial state - assert.Equal(t, int64(0), metrics.FramesSent) - assert.Equal(t, int64(0), metrics.FramesDropped) - assert.Equal(t, int64(0), metrics.BytesProcessed) - assert.Equal(t, int64(0), metrics.ConnectionDrops) - assert.Equal(t, time.Duration(0), metrics.AverageLatency) - assert.True(t, metrics.LastFrameTime.IsZero()) - - // Test field assignment - metrics.FramesSent = 50 - metrics.FramesDropped = 2 - metrics.BytesProcessed = 512 - metrics.ConnectionDrops = 1 - metrics.AverageLatency = 5 * time.Millisecond - metrics.LastFrameTime = time.Now() - - // Verify assignments - assert.Equal(t, int64(50), metrics.FramesSent) - assert.Equal(t, int64(2), metrics.FramesDropped) - assert.Equal(t, int64(512), metrics.BytesProcessed) - assert.Equal(t, int64(1), metrics.ConnectionDrops) - assert.Equal(t, 5*time.Millisecond, metrics.AverageLatency) - assert.False(t, metrics.LastFrameTime.IsZero()) -} - -// BenchmarkAudioInputIPCManager benchmarks the AudioInputIPCManager operations -func BenchmarkAudioInputIPCManager(b *testing.B) { - b.Run("Start", func(b *testing.B) { - for i := 0; i < b.N; i++ { - manager := NewAudioInputIPCManager() - manager.Start() - manager.Stop() - } - }) - - b.Run("IsRunning", func(b *testing.B) { - manager := NewAudioInputIPCManager() - manager.Start() - defer manager.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.IsRunning() - } - }) - - b.Run("GetMetrics", func(b *testing.B) { - manager := NewAudioInputIPCManager() - manager.Start() - defer manager.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.GetMetrics() - } - }) -} diff --git a/internal/audio/input_test.go b/internal/audio/input_test.go deleted file mode 100644 index 3f70689b..00000000 --- a/internal/audio/input_test.go +++ /dev/null @@ -1,244 +0,0 @@ -package audio - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewAudioInputManager(t *testing.T) { - manager := NewAudioInputManager() - assert.NotNil(t, manager) - assert.False(t, manager.IsRunning()) - assert.False(t, manager.IsReady()) -} - -func TestAudioInputManagerStart(t *testing.T) { - manager := NewAudioInputManager() - require.NotNil(t, manager) - - // Test successful start - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Test starting already running manager - err = manager.Start() - assert.Error(t, err) - assert.Contains(t, err.Error(), "already running") - - // Cleanup - manager.Stop() -} - -func TestAudioInputManagerStop(t *testing.T) { - manager := NewAudioInputManager() - require.NotNil(t, manager) - - // Test stopping non-running manager - manager.Stop() - assert.False(t, manager.IsRunning()) - - // Start and then stop - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - manager.Stop() - assert.False(t, manager.IsRunning()) -} - -func TestAudioInputManagerIsRunning(t *testing.T) { - manager := NewAudioInputManager() - require.NotNil(t, manager) - - // Test initial state - assert.False(t, manager.IsRunning()) - - // Test after start - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Test after stop - manager.Stop() - assert.False(t, manager.IsRunning()) -} - -func TestAudioInputManagerIsReady(t *testing.T) { - manager := NewAudioInputManager() - require.NotNil(t, manager) - - // Test initial state - assert.False(t, manager.IsReady()) - - // Start manager - err := manager.Start() - require.NoError(t, err) - - // Give some time for initialization - time.Sleep(100 * time.Millisecond) - - // Test ready state (may vary based on implementation) - // Just ensure the method doesn't panic - _ = manager.IsReady() - - // Cleanup - manager.Stop() -} - -func TestAudioInputManagerGetMetrics(t *testing.T) { - manager := NewAudioInputManager() - require.NotNil(t, manager) - - // Test metrics when not running - metrics := manager.GetMetrics() - assert.NotNil(t, metrics) - assert.Equal(t, int64(0), metrics.FramesSent) - assert.Equal(t, int64(0), metrics.FramesDropped) - assert.Equal(t, int64(0), metrics.BytesProcessed) - assert.Equal(t, int64(0), metrics.ConnectionDrops) - - // Start and test metrics - err := manager.Start() - require.NoError(t, err) - - metrics = manager.GetMetrics() - assert.NotNil(t, metrics) - assert.GreaterOrEqual(t, metrics.FramesSent, int64(0)) - assert.GreaterOrEqual(t, metrics.FramesDropped, int64(0)) - assert.GreaterOrEqual(t, metrics.BytesProcessed, int64(0)) - assert.GreaterOrEqual(t, metrics.ConnectionDrops, int64(0)) - - // Cleanup - manager.Stop() -} - -func TestAudioInputManagerConcurrentOperations(t *testing.T) { - manager := NewAudioInputManager() - require.NotNil(t, manager) - - var wg sync.WaitGroup - - // Test concurrent start/stop operations - for i := 0; i < 10; i++ { - wg.Add(2) - go func() { - defer wg.Done() - _ = manager.Start() - }() - go func() { - defer wg.Done() - manager.Stop() - }() - } - - // Test concurrent metric access - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _ = manager.GetMetrics() - }() - } - - // Test concurrent status checks - for i := 0; i < 5; i++ { - wg.Add(2) - go func() { - defer wg.Done() - _ = manager.IsRunning() - }() - go func() { - defer wg.Done() - _ = manager.IsReady() - }() - } - - wg.Wait() - - // Cleanup - manager.Stop() -} - -func TestAudioInputManagerMultipleStartStop(t *testing.T) { - manager := NewAudioInputManager() - require.NotNil(t, manager) - - // Test multiple start/stop cycles - for i := 0; i < 5; i++ { - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - manager.Stop() - assert.False(t, manager.IsRunning()) - } -} - -func TestAudioInputMetrics(t *testing.T) { - metrics := &AudioInputMetrics{ - BaseAudioMetrics: BaseAudioMetrics{ - FramesProcessed: 100, - FramesDropped: 5, - BytesProcessed: 1024, - ConnectionDrops: 2, - AverageLatency: time.Millisecond * 10, - LastFrameTime: time.Now(), - }, - FramesSent: 100, - } - - assert.Equal(t, int64(100), metrics.FramesSent) - assert.Equal(t, int64(5), metrics.FramesDropped) - assert.Equal(t, int64(1024), metrics.BytesProcessed) - assert.Equal(t, int64(2), metrics.ConnectionDrops) - assert.Equal(t, time.Millisecond*10, metrics.AverageLatency) - assert.False(t, metrics.LastFrameTime.IsZero()) -} - -// Benchmark tests -func BenchmarkAudioInputManager(b *testing.B) { - manager := NewAudioInputManager() - - b.Run("Start", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = manager.Start() - manager.Stop() - } - }) - - b.Run("GetMetrics", func(b *testing.B) { - _ = manager.Start() - defer manager.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = manager.GetMetrics() - } - }) - - b.Run("IsRunning", func(b *testing.B) { - _ = manager.Start() - defer manager.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = manager.IsRunning() - } - }) - - b.Run("IsReady", func(b *testing.B) { - _ = manager.Start() - defer manager.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = manager.IsReady() - } - }) -} diff --git a/internal/audio/integration_test.go b/internal/audio/integration_test.go deleted file mode 100644 index d0546dc8..00000000 --- a/internal/audio/integration_test.go +++ /dev/null @@ -1,320 +0,0 @@ -//go:build integration -// +build integration - -package audio - -import ( - "context" - "fmt" - "net" - "os" - "path/filepath" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestIPCCommunication tests the IPC communication between audio components -func TestIPCCommunication(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - description string - }{ - { - name: "AudioOutputIPC", - testFunc: testAudioOutputIPC, - description: "Test audio output IPC server and client communication", - }, - { - name: "AudioInputIPC", - testFunc: testAudioInputIPC, - description: "Test audio input IPC server and client communication", - }, - { - name: "IPCReconnection", - testFunc: testIPCReconnection, - description: "Test IPC reconnection after connection loss", - }, - { - name: "IPCConcurrency", - testFunc: testIPCConcurrency, - description: "Test concurrent IPC operations", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Logf("Running test: %s - %s", tt.name, tt.description) - tt.testFunc(t) - }) - } -} - -// testAudioOutputIPC tests the audio output IPC communication -func testAudioOutputIPC(t *testing.T) { - tempDir := t.TempDir() - socketPath := filepath.Join(tempDir, "test_audio_output.sock") - - // Create a test IPC server - server := &AudioIPCServer{ - socketPath: socketPath, - logger: getTestLogger(), - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Start server in goroutine - var serverErr error - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - serverErr = server.Start(ctx) - }() - - // Wait for server to start - time.Sleep(100 * time.Millisecond) - - // Test client connection - conn, err := net.Dial("unix", socketPath) - require.NoError(t, err, "Failed to connect to IPC server") - defer conn.Close() - - // Test sending a frame message - testFrame := []byte("test audio frame data") - msg := &OutputMessage{ - Type: OutputMessageTypeOpusFrame, - Timestamp: time.Now().UnixNano(), - Data: testFrame, - } - - err = writeOutputMessage(conn, msg) - require.NoError(t, err, "Failed to write message to IPC") - - // Test heartbeat - heartbeatMsg := &OutputMessage{ - Type: OutputMessageTypeHeartbeat, - Timestamp: time.Now().UnixNano(), - } - - err = writeOutputMessage(conn, heartbeatMsg) - require.NoError(t, err, "Failed to send heartbeat") - - // Clean shutdown - cancel() - wg.Wait() - - if serverErr != nil && serverErr != context.Canceled { - t.Errorf("Server error: %v", serverErr) - } -} - -// testAudioInputIPC tests the audio input IPC communication -func testAudioInputIPC(t *testing.T) { - tempDir := t.TempDir() - socketPath := filepath.Join(tempDir, "test_audio_input.sock") - - // Create a test input IPC server - server := &AudioInputIPCServer{ - socketPath: socketPath, - logger: getTestLogger(), - } - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Start server - var serverErr error - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - serverErr = server.Start(ctx) - }() - - // Wait for server to start - time.Sleep(100 * time.Millisecond) - - // Test client connection - conn, err := net.Dial("unix", socketPath) - require.NoError(t, err, "Failed to connect to input IPC server") - defer conn.Close() - - // Test sending input frame - testInputFrame := []byte("test microphone data") - inputMsg := &InputMessage{ - Type: InputMessageTypeOpusFrame, - Timestamp: time.Now().UnixNano(), - Data: testInputFrame, - } - - err = writeInputMessage(conn, inputMsg) - require.NoError(t, err, "Failed to write input message") - - // Test configuration message - configMsg := &InputMessage{ - Type: InputMessageTypeConfig, - Timestamp: time.Now().UnixNano(), - Data: []byte("quality=medium"), - } - - err = writeInputMessage(conn, configMsg) - require.NoError(t, err, "Failed to send config message") - - // Clean shutdown - cancel() - wg.Wait() - - if serverErr != nil && serverErr != context.Canceled { - t.Errorf("Input server error: %v", serverErr) - } -} - -// testIPCReconnection tests IPC reconnection scenarios -func testIPCReconnection(t *testing.T) { - tempDir := t.TempDir() - socketPath := filepath.Join(tempDir, "test_reconnect.sock") - - // Create server - server := &AudioIPCServer{ - socketPath: socketPath, - logger: getTestLogger(), - } - - ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) - defer cancel() - - // Start server - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - server.Start(ctx) - }() - - time.Sleep(100 * time.Millisecond) - - // First connection - conn1, err := net.Dial("unix", socketPath) - require.NoError(t, err, "Failed initial connection") - - // Send a message - msg := &OutputMessage{ - Type: OutputMessageTypeOpusFrame, - Timestamp: time.Now().UnixNano(), - Data: []byte("test data 1"), - } - err = writeOutputMessage(conn1, msg) - require.NoError(t, err, "Failed to send first message") - - // Close connection to simulate disconnect - conn1.Close() - time.Sleep(200 * time.Millisecond) - - // Reconnect - conn2, err := net.Dial("unix", socketPath) - require.NoError(t, err, "Failed to reconnect") - defer conn2.Close() - - // Send another message after reconnection - msg2 := &OutputMessage{ - Type: OutputMessageTypeOpusFrame, - Timestamp: time.Now().UnixNano(), - Data: []byte("test data 2"), - } - err = writeOutputMessage(conn2, msg2) - require.NoError(t, err, "Failed to send message after reconnection") - - cancel() - wg.Wait() -} - -// testIPCConcurrency tests concurrent IPC operations -func testIPCConcurrency(t *testing.T) { - tempDir := t.TempDir() - socketPath := filepath.Join(tempDir, "test_concurrent.sock") - - server := &AudioIPCServer{ - socketPath: socketPath, - logger: getTestLogger(), - } - - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - // Start server - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - server.Start(ctx) - }() - - time.Sleep(100 * time.Millisecond) - - // Create multiple concurrent connections - numClients := 5 - messagesPerClient := 10 - - var clientWg sync.WaitGroup - for i := 0; i < numClients; i++ { - clientWg.Add(1) - go func(clientID int) { - defer clientWg.Done() - - conn, err := net.Dial("unix", socketPath) - if err != nil { - t.Errorf("Client %d failed to connect: %v", clientID, err) - return - } - defer conn.Close() - - // Send multiple messages - for j := 0; j < messagesPerClient; j++ { - msg := &OutputMessage{ - Type: OutputMessageTypeOpusFrame, - Timestamp: time.Now().UnixNano(), - Data: []byte(fmt.Sprintf("client_%d_msg_%d", clientID, j)), - } - - if err := writeOutputMessage(conn, msg); err != nil { - t.Errorf("Client %d failed to send message %d: %v", clientID, j, err) - return - } - - // Small delay between messages - time.Sleep(10 * time.Millisecond) - } - }(i) - } - - clientWg.Wait() - cancel() - wg.Wait() -} - -// Helper function to get a test logger -func getTestLogger() zerolog.Logger { - return zerolog.New(os.Stdout).With().Timestamp().Logger() -} - -// Helper functions for message writing (simplified versions) -func writeOutputMessage(conn net.Conn, msg *OutputMessage) error { - // This is a simplified version for testing - // In real implementation, this would use the actual protocol - data := fmt.Sprintf("%d:%d:%s", msg.Type, msg.Timestamp, string(msg.Data)) - _, err := conn.Write([]byte(data)) - return err -} - -func writeInputMessage(conn net.Conn, msg *InputMessage) error { - // This is a simplified version for testing - data := fmt.Sprintf("%d:%d:%s", msg.Type, msg.Timestamp, string(msg.Data)) - _, err := conn.Write([]byte(data)) - return err -} \ No newline at end of file diff --git a/internal/audio/latency_profiler.go b/internal/audio/latency_profiler.go index 02b3c72a..21d48ac1 100644 --- a/internal/audio/latency_profiler.go +++ b/internal/audio/latency_profiler.go @@ -127,15 +127,14 @@ var ( // DefaultLatencyProfilerConfig returns default profiler configuration func DefaultLatencyProfilerConfig() LatencyProfilerConfig { - config := GetConfig() return LatencyProfilerConfig{ MaxMeasurements: 10000, - SamplingRate: config.LatencyProfilingSamplingRate, // Use configurable sampling rate + SamplingRate: 0.01, // Fixed sampling rate (1%) ReportingInterval: 30 * time.Second, ThresholdWarning: 50 * time.Millisecond, ThresholdCritical: 100 * time.Millisecond, - EnableDetailedTrace: false, // Disabled by default for performance - EnableHistogram: config.EnableLatencyProfiling, // Only enable if profiling is enabled + EnableDetailedTrace: false, // Disabled by default for performance + EnableHistogram: false, // Latency profiling disabled } } @@ -509,8 +508,8 @@ func GetGlobalLatencyProfiler() *LatencyProfiler { // EnableLatencyProfiling enables the global latency profiler func EnableLatencyProfiling() error { - config := GetConfig() - if !config.EnableLatencyProfiling { + // Latency profiling disabled + if true { return fmt.Errorf("latency profiling is disabled in configuration") } profiler := GetGlobalLatencyProfiler() @@ -528,8 +527,8 @@ func DisableLatencyProfiling() { // ProfileFrameLatency is a convenience function to profile a single frame's latency func ProfileFrameLatency(frameID uint64, frameSize int, source string, fn func(*FrameLatencyTracker)) { - config := GetConfig() - if !config.EnableLatencyProfiling { + // Latency profiling disabled + if true { fn(nil) return } diff --git a/internal/audio/output_manager_test.go b/internal/audio/output_manager_test.go deleted file mode 100644 index f680573f..00000000 --- a/internal/audio/output_manager_test.go +++ /dev/null @@ -1,277 +0,0 @@ -package audio - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestAudioOutputManager tests the AudioOutputManager component -func TestAudioOutputManager(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"Start", testAudioOutputManagerStart}, - {"Stop", testAudioOutputManagerStop}, - {"StartStop", testAudioOutputManagerStartStop}, - {"IsRunning", testAudioOutputManagerIsRunning}, - {"IsReady", testAudioOutputManagerIsReady}, - {"GetMetrics", testAudioOutputManagerGetMetrics}, - {"ConcurrentOperations", testAudioOutputManagerConcurrent}, - {"MultipleStarts", testAudioOutputManagerMultipleStarts}, - {"MultipleStops", testAudioOutputManagerMultipleStops}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -func testAudioOutputManagerStart(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // Test initial state - assert.False(t, manager.IsRunning()) - assert.False(t, manager.IsReady()) - - // Test start - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Cleanup - manager.Stop() -} - -func testAudioOutputManagerStop(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // Start first - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Test stop - manager.Stop() - assert.False(t, manager.IsRunning()) - assert.False(t, manager.IsReady()) -} - -func testAudioOutputManagerStartStop(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // Test multiple start/stop cycles - for i := 0; i < 3; i++ { - // Start - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Stop - manager.Stop() - assert.False(t, manager.IsRunning()) - } -} - -func testAudioOutputManagerIsRunning(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // Initially not running - assert.False(t, manager.IsRunning()) - - // Start and check - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Stop and check - manager.Stop() - assert.False(t, manager.IsRunning()) -} - -func testAudioOutputManagerIsReady(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // Initially not ready - assert.False(t, manager.IsReady()) - - // Start and check ready state - err := manager.Start() - require.NoError(t, err) - - // Give some time for initialization - time.Sleep(100 * time.Millisecond) - - // Stop - manager.Stop() - assert.False(t, manager.IsReady()) -} - -func testAudioOutputManagerGetMetrics(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // Test metrics when not running - metrics := manager.GetMetrics() - assert.NotNil(t, metrics) - - // Start and test metrics - err := manager.Start() - require.NoError(t, err) - - metrics = manager.GetMetrics() - assert.NotNil(t, metrics) - - // Cleanup - manager.Stop() -} - -func testAudioOutputManagerConcurrent(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - var wg sync.WaitGroup - const numGoroutines = 10 - - // Test concurrent starts - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - manager.Start() - }() - } - wg.Wait() - - // Should be running - assert.True(t, manager.IsRunning()) - - // Test concurrent stops - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - manager.Stop() - }() - } - wg.Wait() - - // Should be stopped - assert.False(t, manager.IsRunning()) -} - -func testAudioOutputManagerMultipleStarts(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // First start should succeed - err := manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Subsequent starts should be no-op - err = manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - err = manager.Start() - assert.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // Cleanup - manager.Stop() -} - -func testAudioOutputManagerMultipleStops(t *testing.T) { - manager := NewAudioOutputManager() - require.NotNil(t, manager) - - // Start first - err := manager.Start() - require.NoError(t, err) - assert.True(t, manager.IsRunning()) - - // First stop should work - manager.Stop() - assert.False(t, manager.IsRunning()) - - // Subsequent stops should be no-op - manager.Stop() - assert.False(t, manager.IsRunning()) - - manager.Stop() - assert.False(t, manager.IsRunning()) -} - -// TestAudioOutputMetrics tests the AudioOutputMetrics functionality -func TestAudioOutputMetrics(t *testing.T) { - metrics := &AudioOutputMetrics{} - - // Test initial state - assert.Equal(t, int64(0), metrics.FramesReceived) - assert.Equal(t, int64(0), metrics.FramesDropped) - assert.Equal(t, int64(0), metrics.BytesProcessed) - assert.Equal(t, int64(0), metrics.ConnectionDrops) - assert.Equal(t, time.Duration(0), metrics.AverageLatency) - assert.True(t, metrics.LastFrameTime.IsZero()) - - // Test field assignment - metrics.FramesReceived = 100 - metrics.FramesDropped = 5 - metrics.BytesProcessed = 1024 - metrics.ConnectionDrops = 2 - metrics.AverageLatency = 10 * time.Millisecond - metrics.LastFrameTime = time.Now() - - // Verify assignments - assert.Equal(t, int64(100), metrics.FramesReceived) - assert.Equal(t, int64(5), metrics.FramesDropped) - assert.Equal(t, int64(1024), metrics.BytesProcessed) - assert.Equal(t, int64(2), metrics.ConnectionDrops) - assert.Equal(t, 10*time.Millisecond, metrics.AverageLatency) - assert.False(t, metrics.LastFrameTime.IsZero()) -} - -// BenchmarkAudioOutputManager benchmarks the AudioOutputManager operations -func BenchmarkAudioOutputManager(b *testing.B) { - b.Run("Start", func(b *testing.B) { - for i := 0; i < b.N; i++ { - manager := NewAudioOutputManager() - manager.Start() - manager.Stop() - } - }) - - b.Run("IsRunning", func(b *testing.B) { - manager := NewAudioOutputManager() - manager.Start() - defer manager.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.IsRunning() - } - }) - - b.Run("GetMetrics", func(b *testing.B) { - manager := NewAudioOutputManager() - manager.Start() - defer manager.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.GetMetrics() - } - }) -} diff --git a/internal/audio/output_streaming.go b/internal/audio/output_streaming.go index 860f7891..5213d053 100644 --- a/internal/audio/output_streaming.go +++ b/internal/audio/output_streaming.go @@ -23,9 +23,6 @@ type AudioOutputStreamer struct { droppedFrames int64 // Dropped frames counter (atomic) processingTime int64 // Average processing time in nanoseconds (atomic) lastStatsTime int64 // Last statistics update time (atomic) - frameCounter int64 // Local counter for sampling - localProcessed int64 // Local processed frame accumulator - localDropped int64 // Local dropped frame accumulator // Other fields after atomic int64 fields sampleRate int32 // Sample every N frames (default: 10) @@ -295,60 +292,14 @@ func (s *AudioOutputStreamer) reportStatistics() { // recordFrameProcessed records a processed frame with sampling optimization func (s *AudioOutputStreamer) recordFrameProcessed() { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Increment local counters - frameCount := atomic.AddInt64(&s.frameCounter, 1) - atomic.AddInt64(&s.localProcessed, 1) - - // Update metrics only every N frames to reduce atomic operation overhead - if frameCount%int64(atomic.LoadInt32(&s.sampleRate)) == 0 { - // Batch update atomic metrics - localProcessed := atomic.SwapInt64(&s.localProcessed, 0) - atomic.AddInt64(&s.processedFrames, localProcessed) - } } // recordFrameDropped records a dropped frame with sampling optimization func (s *AudioOutputStreamer) recordFrameDropped() { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Increment local counter - localDropped := atomic.AddInt64(&s.localDropped, 1) - - // Update atomic metrics every N dropped frames - if localDropped%int64(atomic.LoadInt32(&s.sampleRate)) == 0 { - atomic.AddInt64(&s.droppedFrames, int64(atomic.LoadInt32(&s.sampleRate))) - atomic.StoreInt64(&s.localDropped, 0) - } } // flushPendingMetrics flushes any pending sampled metrics to atomic counters func (s *AudioOutputStreamer) flushPendingMetrics() { - // Check if metrics collection is enabled - cachedConfig := GetCachedConfig() - if !cachedConfig.GetEnableMetricsCollection() { - return - } - - // Flush remaining processed and dropped frames - localProcessed := atomic.SwapInt64(&s.localProcessed, 0) - localDropped := atomic.SwapInt64(&s.localDropped, 0) - - if localProcessed > 0 { - atomic.AddInt64(&s.processedFrames, localProcessed) - } - if localDropped > 0 { - atomic.AddInt64(&s.droppedFrames, localDropped) - } } // GetStats returns streaming statistics with pending metrics flushed diff --git a/internal/audio/output_streaming_test.go b/internal/audio/output_streaming_test.go deleted file mode 100644 index 23228bee..00000000 --- a/internal/audio/output_streaming_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package audio - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestAudioOutputStreamer tests the AudioOutputStreamer component -func TestAudioOutputStreamer(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"NewAudioOutputStreamer", testNewAudioOutputStreamer}, - {"Start", testAudioOutputStreamerStart}, - {"Stop", testAudioOutputStreamerStop}, - {"StartStop", testAudioOutputStreamerStartStop}, - {"GetStats", testAudioOutputStreamerGetStats}, - {"GetDetailedStats", testAudioOutputStreamerGetDetailedStats}, - {"UpdateBatchSize", testAudioOutputStreamerUpdateBatchSize}, - {"ReportLatency", testAudioOutputStreamerReportLatency}, - {"ConcurrentOperations", testAudioOutputStreamerConcurrent}, - {"MultipleStarts", testAudioOutputStreamerMultipleStarts}, - {"MultipleStops", testAudioOutputStreamerMultipleStops}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -func testNewAudioOutputStreamer(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - // If creation fails due to missing dependencies, skip the test - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Test initial state - processed, dropped, avgTime := streamer.GetStats() - assert.GreaterOrEqual(t, processed, int64(0)) - assert.GreaterOrEqual(t, dropped, int64(0)) - assert.GreaterOrEqual(t, avgTime, time.Duration(0)) - - // Cleanup - streamer.Stop() -} - -func testAudioOutputStreamerStart(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Test start - err = streamer.Start() - assert.NoError(t, err) - - // Cleanup - streamer.Stop() -} - -func testAudioOutputStreamerStop(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Start first - err = streamer.Start() - require.NoError(t, err) - - // Test stop - streamer.Stop() - - // Multiple stops should be safe - streamer.Stop() - streamer.Stop() -} - -func testAudioOutputStreamerStartStop(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Test multiple start/stop cycles - for i := 0; i < 3; i++ { - // Start - err = streamer.Start() - assert.NoError(t, err) - - // Stop - streamer.Stop() - } -} - -func testAudioOutputStreamerGetStats(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Test stats when not running - processed, dropped, avgTime := streamer.GetStats() - assert.Equal(t, int64(0), processed) - assert.Equal(t, int64(0), dropped) - assert.GreaterOrEqual(t, avgTime, time.Duration(0)) - - // Start and test stats - err = streamer.Start() - require.NoError(t, err) - - processed, dropped, avgTime = streamer.GetStats() - assert.GreaterOrEqual(t, processed, int64(0)) - assert.GreaterOrEqual(t, dropped, int64(0)) - assert.GreaterOrEqual(t, avgTime, time.Duration(0)) - - // Cleanup - streamer.Stop() -} - -func testAudioOutputStreamerGetDetailedStats(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Test detailed stats - stats := streamer.GetDetailedStats() - assert.NotNil(t, stats) - assert.Contains(t, stats, "processed_frames") - assert.Contains(t, stats, "dropped_frames") - assert.Contains(t, stats, "batch_size") - assert.Contains(t, stats, "connected") - assert.Equal(t, int64(0), stats["processed_frames"]) - assert.Equal(t, int64(0), stats["dropped_frames"]) - - // Start and test detailed stats - err = streamer.Start() - require.NoError(t, err) - - stats = streamer.GetDetailedStats() - assert.NotNil(t, stats) - assert.Contains(t, stats, "processed_frames") - assert.Contains(t, stats, "dropped_frames") - - // Cleanup - streamer.Stop() -} - -func testAudioOutputStreamerUpdateBatchSize(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Test updating batch size (no parameters, uses adaptive manager) - streamer.UpdateBatchSize() - streamer.UpdateBatchSize() - streamer.UpdateBatchSize() - - // Cleanup - streamer.Stop() -} - -func testAudioOutputStreamerReportLatency(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Test reporting latency - streamer.ReportLatency(10 * time.Millisecond) - streamer.ReportLatency(5 * time.Millisecond) - streamer.ReportLatency(15 * time.Millisecond) - - // Cleanup - streamer.Stop() -} - -func testAudioOutputStreamerConcurrent(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - var wg sync.WaitGroup - const numGoroutines = 10 - - // Test concurrent starts - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - streamer.Start() - }() - } - wg.Wait() - - // Test concurrent operations - wg.Add(numGoroutines * 3) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - streamer.GetStats() - }() - go func() { - defer wg.Done() - streamer.UpdateBatchSize() - }() - go func() { - defer wg.Done() - streamer.ReportLatency(10 * time.Millisecond) - }() - } - wg.Wait() - - // Test concurrent stops - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - streamer.Stop() - }() - } - wg.Wait() -} - -func testAudioOutputStreamerMultipleStarts(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // First start should succeed - err = streamer.Start() - assert.NoError(t, err) - - // Subsequent starts should return error - err = streamer.Start() - assert.Error(t, err) - assert.Contains(t, err.Error(), "already running") - - err = streamer.Start() - assert.Error(t, err) - assert.Contains(t, err.Error(), "already running") - - // Cleanup - streamer.Stop() -} - -func testAudioOutputStreamerMultipleStops(t *testing.T) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - t.Skipf("Skipping test due to missing dependencies: %v", err) - return - } - require.NotNil(t, streamer) - - // Start first - err = streamer.Start() - require.NoError(t, err) - - // Multiple stops should be safe - streamer.Stop() - streamer.Stop() - streamer.Stop() -} - -// BenchmarkAudioOutputStreamer benchmarks the AudioOutputStreamer operations -func BenchmarkAudioOutputStreamer(b *testing.B) { - b.Run("GetStats", func(b *testing.B) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - b.Skipf("Skipping benchmark due to missing dependencies: %v", err) - return - } - defer streamer.Stop() - - streamer.Start() - b.ResetTimer() - for i := 0; i < b.N; i++ { - streamer.GetStats() - } - }) - - b.Run("UpdateBatchSize", func(b *testing.B) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - b.Skipf("Skipping benchmark due to missing dependencies: %v", err) - return - } - defer streamer.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - streamer.UpdateBatchSize() - } - }) - - b.Run("ReportLatency", func(b *testing.B) { - streamer, err := NewAudioOutputStreamer() - if err != nil { - b.Skipf("Skipping benchmark due to missing dependencies: %v", err) - return - } - defer streamer.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - streamer.ReportLatency(10 * time.Millisecond) - } - }) -} diff --git a/internal/audio/performance_critical_test.go b/internal/audio/performance_critical_test.go deleted file mode 100644 index 73db6a59..00000000 --- a/internal/audio/performance_critical_test.go +++ /dev/null @@ -1,393 +0,0 @@ -//go:build cgo -// +build cgo - -package audio - -import ( - "runtime" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestPerformanceCriticalPaths tests the most frequently executed code paths -// to ensure they remain efficient and don't interfere with KVM functionality -func TestPerformanceCriticalPaths(t *testing.T) { - if testing.Short() { - t.Skip("Skipping performance tests in short mode") - } - - // Initialize validation cache for performance testing - InitValidationCache() - - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"AudioFrameProcessingLatency", testAudioFrameProcessingLatency}, - {"MetricsUpdateOverhead", testMetricsUpdateOverhead}, - {"ConfigurationAccessSpeed", testConfigurationAccessSpeed}, - {"ValidationFunctionSpeed", testValidationFunctionSpeed}, - {"MemoryAllocationPatterns", testMemoryAllocationPatterns}, - {"ConcurrentAccessPerformance", testConcurrentAccessPerformance}, - {"BufferPoolEfficiency", testBufferPoolEfficiency}, - {"AtomicOperationOverhead", testAtomicOperationOverhead}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -// testAudioFrameProcessingLatency tests the latency of audio frame processing -// This is the most critical path that must not interfere with KVM -func testAudioFrameProcessingLatency(t *testing.T) { - const ( - frameCount = 1000 - maxLatencyPerFrame = 100 * time.Microsecond // Very strict requirement - ) - - // Create test frame data - frameData := make([]byte, 1920) // Typical frame size - for i := range frameData { - frameData[i] = byte(i % 256) - } - - // Measure frame processing latency - start := time.Now() - for i := 0; i < frameCount; i++ { - // Simulate the critical path: validation + metrics update - err := ValidateAudioFrame(frameData) - require.NoError(t, err) - - // Record frame received (atomic operation) - RecordFrameReceived(len(frameData)) - } - elapsed := time.Since(start) - - avgLatencyPerFrame := elapsed / frameCount - t.Logf("Average frame processing latency: %v", avgLatencyPerFrame) - - // Ensure frame processing is fast enough to not interfere with KVM - assert.Less(t, avgLatencyPerFrame, maxLatencyPerFrame, - "Frame processing latency %v exceeds maximum %v - may interfere with KVM", - avgLatencyPerFrame, maxLatencyPerFrame) - - // Ensure total processing time is reasonable - maxTotalTime := 50 * time.Millisecond - assert.Less(t, elapsed, maxTotalTime, - "Total processing time %v exceeds maximum %v", elapsed, maxTotalTime) -} - -// testMetricsUpdateOverhead tests the overhead of metrics updates -func testMetricsUpdateOverhead(t *testing.T) { - const iterations = 10000 - - // Test RecordFrameReceived performance - start := time.Now() - for i := 0; i < iterations; i++ { - RecordFrameReceived(1024) - } - recordLatency := time.Since(start) / iterations - - // Test GetAudioMetrics performance - start = time.Now() - for i := 0; i < iterations; i++ { - _ = GetAudioMetrics() - } - getLatency := time.Since(start) / iterations - - t.Logf("RecordFrameReceived latency: %v", recordLatency) - t.Logf("GetAudioMetrics latency: %v", getLatency) - - // Metrics operations should be optimized for JetKVM's ARM Cortex-A7 @ 1GHz - // With 256MB RAM, we need to be conservative with performance expectations - assert.Less(t, recordLatency, 50*time.Microsecond, "RecordFrameReceived too slow") - assert.Less(t, getLatency, 20*time.Microsecond, "GetAudioMetrics too slow") -} - -// testConfigurationAccessSpeed tests configuration access performance -func testConfigurationAccessSpeed(t *testing.T) { - const iterations = 10000 - - // Test GetAudioConfig performance - start := time.Now() - for i := 0; i < iterations; i++ { - _ = GetAudioConfig() - } - configLatency := time.Since(start) / iterations - - // Test GetConfig performance - start = time.Now() - for i := 0; i < iterations; i++ { - _ = GetConfig() - } - constantsLatency := time.Since(start) / iterations - - t.Logf("GetAudioConfig latency: %v", configLatency) - t.Logf("GetConfig latency: %v", constantsLatency) - - // Configuration access should be very fast - assert.Less(t, configLatency, 100*time.Nanosecond, "GetAudioConfig too slow") - assert.Less(t, constantsLatency, 100*time.Nanosecond, "GetConfig too slow") -} - -// testValidationFunctionSpeed tests validation function performance -func testValidationFunctionSpeed(t *testing.T) { - const iterations = 10000 - frameData := make([]byte, 1920) - - // Test ValidateAudioFrame (most critical) - start := time.Now() - for i := 0; i < iterations; i++ { - err := ValidateAudioFrame(frameData) - require.NoError(t, err) - } - fastValidationLatency := time.Since(start) / iterations - - // Test ValidateAudioQuality - start = time.Now() - for i := 0; i < iterations; i++ { - err := ValidateAudioQuality(AudioQualityMedium) - require.NoError(t, err) - } - qualityValidationLatency := time.Since(start) / iterations - - // Test ValidateBufferSize - start = time.Now() - for i := 0; i < iterations; i++ { - err := ValidateBufferSize(1024) - require.NoError(t, err) - } - bufferValidationLatency := time.Since(start) / iterations - - t.Logf("ValidateAudioFrame latency: %v", fastValidationLatency) - t.Logf("ValidateAudioQuality latency: %v", qualityValidationLatency) - t.Logf("ValidateBufferSize latency: %v", bufferValidationLatency) - - // Validation functions optimized for ARM Cortex-A7 single core @ 1GHz - // Conservative thresholds to ensure KVM functionality isn't impacted - assert.Less(t, fastValidationLatency, 100*time.Microsecond, "ValidateAudioFrame too slow") - assert.Less(t, qualityValidationLatency, 50*time.Microsecond, "ValidateAudioQuality too slow") - assert.Less(t, bufferValidationLatency, 50*time.Microsecond, "ValidateBufferSize too slow") -} - -// testMemoryAllocationPatterns tests memory allocation efficiency -func testMemoryAllocationPatterns(t *testing.T) { - // Test that frequent operations don't cause excessive allocations - var m1, m2 runtime.MemStats - runtime.GC() - runtime.ReadMemStats(&m1) - - // Perform operations that should minimize allocations - for i := 0; i < 1000; i++ { - _ = GetAudioConfig() - _ = GetAudioMetrics() - RecordFrameReceived(1024) - _ = ValidateAudioQuality(AudioQualityMedium) - } - - runtime.GC() - runtime.ReadMemStats(&m2) - - allocations := m2.Mallocs - m1.Mallocs - t.Logf("Memory allocations for 1000 operations: %d", allocations) - - // Should have minimal allocations for these hot path operations - assert.Less(t, allocations, uint64(100), "Too many memory allocations in hot path") -} - -// testConcurrentAccessPerformance tests performance under concurrent access -func testConcurrentAccessPerformance(t *testing.T) { - const ( - numGoroutines = 10 - operationsPerGoroutine = 1000 - ) - - var wg sync.WaitGroup - start := time.Now() - - // Launch concurrent goroutines performing audio operations - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - frameData := make([]byte, 1920) - - for j := 0; j < operationsPerGoroutine; j++ { - // Simulate concurrent audio processing - _ = ValidateAudioFrame(frameData) - RecordFrameReceived(len(frameData)) - _ = GetAudioMetrics() - _ = GetAudioConfig() - } - }() - } - - wg.Wait() - elapsed := time.Since(start) - - totalOperations := numGoroutines * operationsPerGoroutine * 4 // 4 operations per iteration - avgLatency := elapsed / time.Duration(totalOperations) - - t.Logf("Concurrent access: %d operations in %v (avg: %v per operation)", - totalOperations, elapsed, avgLatency) - - // Concurrent access should not significantly degrade performance - assert.Less(t, avgLatency, 1*time.Microsecond, "Concurrent access too slow") -} - -// testBufferPoolEfficiency tests buffer pool performance -func testBufferPoolEfficiency(t *testing.T) { - // Test buffer acquisition and release performance - const iterations = 1000 - - start := time.Now() - for i := 0; i < iterations; i++ { - // Simulate buffer pool usage (if available) - buffer := make([]byte, 1920) // Fallback to allocation - _ = buffer - // In real implementation, this would be pool.Get() and pool.Put() - } - elapsed := time.Since(start) - - avgLatency := elapsed / iterations - t.Logf("Buffer allocation latency: %v per buffer", avgLatency) - - // Buffer operations should be fast - assert.Less(t, avgLatency, 1*time.Microsecond, "Buffer allocation too slow") -} - -// testAtomicOperationOverhead tests atomic operation performance -func testAtomicOperationOverhead(t *testing.T) { - const iterations = 10000 - var counter int64 - - // Test atomic increment performance - start := time.Now() - for i := 0; i < iterations; i++ { - atomic.AddInt64(&counter, 1) - } - atomicLatency := time.Since(start) / iterations - - // Test atomic load performance - start = time.Now() - for i := 0; i < iterations; i++ { - _ = atomic.LoadInt64(&counter) - } - loadLatency := time.Since(start) / iterations - - t.Logf("Atomic add latency: %v", atomicLatency) - t.Logf("Atomic load latency: %v", loadLatency) - - // Atomic operations on ARM Cortex-A7 - realistic expectations - assert.Less(t, atomicLatency, 1*time.Microsecond, "Atomic add too slow") - assert.Less(t, loadLatency, 500*time.Nanosecond, "Atomic load too slow") -} - -// TestRegressionDetection tests for performance regressions -func TestRegressionDetection(t *testing.T) { - if testing.Short() { - t.Skip("Skipping regression test in short mode") - } - - // Baseline performance expectations - baselines := map[string]time.Duration{ - "frame_processing": 100 * time.Microsecond, - "metrics_update": 500 * time.Nanosecond, - "config_access": 100 * time.Nanosecond, - "validation": 200 * time.Nanosecond, - } - - // Test frame processing - frameData := make([]byte, 1920) - start := time.Now() - for i := 0; i < 100; i++ { - _ = ValidateAudioFrame(frameData) - RecordFrameReceived(len(frameData)) - } - frameProcessingTime := time.Since(start) / 100 - - // Test metrics update - start = time.Now() - for i := 0; i < 1000; i++ { - RecordFrameReceived(1024) - } - metricsUpdateTime := time.Since(start) / 1000 - - // Test config access - start = time.Now() - for i := 0; i < 1000; i++ { - _ = GetAudioConfig() - } - configAccessTime := time.Since(start) / 1000 - - // Test validation - start = time.Now() - for i := 0; i < 1000; i++ { - _ = ValidateAudioQuality(AudioQualityMedium) - } - validationTime := time.Since(start) / 1000 - - // Performance regression thresholds for JetKVM hardware: - // - ARM Cortex-A7 @ 1GHz single core - // - 256MB DDR3L RAM - // - Must not interfere with primary KVM functionality - assert.Less(t, frameProcessingTime, baselines["frame_processing"], - "Frame processing regression: %v > %v", frameProcessingTime, baselines["frame_processing"]) - assert.Less(t, metricsUpdateTime, 100*time.Microsecond, - "Metrics update regression: %v > 100μs", metricsUpdateTime) - assert.Less(t, configAccessTime, 10*time.Microsecond, - "Config access regression: %v > 10μs", configAccessTime) - assert.Less(t, validationTime, 10*time.Microsecond, - "Validation regression: %v > 10μs", validationTime) - - t.Logf("Performance results:") - t.Logf(" Frame processing: %v (baseline: %v)", frameProcessingTime, baselines["frame_processing"]) - t.Logf(" Metrics update: %v (baseline: %v)", metricsUpdateTime, baselines["metrics_update"]) - t.Logf(" Config access: %v (baseline: %v)", configAccessTime, baselines["config_access"]) - t.Logf(" Validation: %v (baseline: %v)", validationTime, baselines["validation"]) -} - -// TestMemoryLeakDetection tests for memory leaks in critical paths -func TestMemoryLeakDetection(t *testing.T) { - if testing.Short() { - t.Skip("Skipping memory leak test in short mode") - } - - var m1, m2 runtime.MemStats - - // Baseline measurement - runtime.GC() - runtime.ReadMemStats(&m1) - - // Perform many operations that should not leak memory - for cycle := 0; cycle < 10; cycle++ { - for i := 0; i < 1000; i++ { - frameData := make([]byte, 1920) - _ = ValidateAudioFrame(frameData) - RecordFrameReceived(len(frameData)) - _ = GetAudioMetrics() - _ = GetAudioConfig() - } - // Force garbage collection between cycles - runtime.GC() - } - - // Final measurement - runtime.GC() - runtime.ReadMemStats(&m2) - - memoryGrowth := int64(m2.Alloc) - int64(m1.Alloc) - t.Logf("Memory growth after 10,000 operations: %d bytes", memoryGrowth) - - // Memory growth should be minimal (less than 1MB) - assert.Less(t, memoryGrowth, int64(1024*1024), - "Excessive memory growth detected: %d bytes", memoryGrowth) -} diff --git a/internal/audio/regression_test.go b/internal/audio/regression_test.go deleted file mode 100644 index 71007f92..00000000 --- a/internal/audio/regression_test.go +++ /dev/null @@ -1,362 +0,0 @@ -//go:build cgo -// +build cgo - -package audio - -import ( - "fmt" - "net" - "os" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestRegressionScenarios tests critical edge cases and error conditions -// that could cause system instability in production -func TestRegressionScenarios(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - description string - }{ - { - name: "IPCConnectionFailure", - testFunc: testIPCConnectionFailureRecovery, - description: "Test IPC connection failure and recovery scenarios", - }, - { - name: "BufferOverflow", - testFunc: testBufferOverflowHandling, - description: "Test buffer overflow protection and recovery", - }, - { - name: "SupervisorRapidRestart", - testFunc: testSupervisorRapidRestartScenario, - description: "Test supervisor behavior under rapid restart conditions", - }, - { - name: "ConcurrentStartStop", - testFunc: testConcurrentStartStopOperations, - description: "Test concurrent start/stop operations for race conditions", - }, - { - name: "MemoryLeakPrevention", - testFunc: testMemoryLeakPrevention, - description: "Test memory leak prevention in long-running scenarios", - }, - { - name: "ConfigValidationEdgeCases", - testFunc: testConfigValidationEdgeCases, - description: "Test configuration validation with edge case values", - }, - { - name: "AtomicOperationConsistency", - testFunc: testAtomicOperationConsistency, - description: "Test atomic operations consistency under high concurrency", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Logf("Running regression test: %s - %s", tt.name, tt.description) - tt.testFunc(t) - }) - } -} - -// testIPCConnectionFailureRecovery tests IPC connection failure scenarios -func testIPCConnectionFailureRecovery(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Test start with no IPC server available (should handle gracefully) - err := manager.Start() - // Should not panic or crash, may return error depending on implementation - if err != nil { - t.Logf("Expected error when no IPC server available: %v", err) - } - - // Test that manager can recover after IPC becomes available - if manager.IsRunning() { - manager.Stop() - } - - // Verify clean state after failure - assert.False(t, manager.IsRunning()) - assert.False(t, manager.IsReady()) -} - -// testBufferOverflowHandling tests buffer overflow protection -func testBufferOverflowHandling(t *testing.T) { - // Test with extremely large buffer sizes - extremelyLargeSize := 1024 * 1024 * 100 // 100MB - err := ValidateBufferSize(extremelyLargeSize) - assert.Error(t, err, "Should reject extremely large buffer sizes") - - // Test with negative buffer sizes - err = ValidateBufferSize(-1) - assert.Error(t, err, "Should reject negative buffer sizes") - - // Test with zero buffer size - err = ValidateBufferSize(0) - assert.Error(t, err, "Should reject zero buffer size") - - // Test with maximum valid buffer size - maxValidSize := GetConfig().SocketMaxBuffer - err = ValidateBufferSize(int(maxValidSize)) - assert.NoError(t, err, "Should accept maximum valid buffer size") -} - -// testSupervisorRapidRestartScenario tests supervisor under rapid restart conditions -func testSupervisorRapidRestartScenario(t *testing.T) { - if testing.Short() { - t.Skip("Skipping rapid restart test in short mode") - } - - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Perform rapid start/stop cycles to test for race conditions - for i := 0; i < 10; i++ { - err := supervisor.Start() - if err != nil { - t.Logf("Start attempt %d failed (expected in test environment): %v", i, err) - } - - // Very short delay to stress test - time.Sleep(10 * time.Millisecond) - - supervisor.Stop() - time.Sleep(10 * time.Millisecond) - } - - // Verify supervisor is in clean state after rapid cycling - assert.False(t, supervisor.IsRunning()) -} - -// testConcurrentStartStopOperations tests concurrent operations for race conditions -func testConcurrentStartStopOperations(t *testing.T) { - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - var wg sync.WaitGroup - const numGoroutines = 10 - - // Launch multiple goroutines trying to start/stop concurrently - for i := 0; i < numGoroutines; i++ { - wg.Add(2) - - // Start goroutine - go func(id int) { - defer wg.Done() - err := manager.Start() - if err != nil { - t.Logf("Concurrent start %d: %v", id, err) - } - }(i) - - // Stop goroutine - go func(id int) { - defer wg.Done() - time.Sleep(5 * time.Millisecond) // Small delay - manager.Stop() - }(i) - } - - wg.Wait() - - // Ensure final state is consistent - manager.Stop() // Final cleanup - assert.False(t, manager.IsRunning()) -} - -// testMemoryLeakPrevention tests for memory leaks in long-running scenarios -func testMemoryLeakPrevention(t *testing.T) { - if testing.Short() { - t.Skip("Skipping memory leak test in short mode") - } - - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Simulate long-running operation with periodic restarts - for cycle := 0; cycle < 5; cycle++ { - err := manager.Start() - if err != nil { - t.Logf("Start cycle %d failed (expected): %v", cycle, err) - } - - // Simulate some activity - time.Sleep(100 * time.Millisecond) - - // Get metrics to ensure they're not accumulating indefinitely - metrics := manager.GetMetrics() - assert.NotNil(t, metrics, "Metrics should be available") - - manager.Stop() - time.Sleep(50 * time.Millisecond) - } - - // Final verification - assert.False(t, manager.IsRunning()) -} - -// testConfigValidationEdgeCases tests configuration validation with edge cases -func testConfigValidationEdgeCases(t *testing.T) { - // Test sample rate edge cases - testCases := []struct { - sampleRate int - channels int - frameSize int - shouldPass bool - description string - }{ - {0, 2, 960, false, "zero sample rate"}, - {-1, 2, 960, false, "negative sample rate"}, - {1, 2, 960, false, "extremely low sample rate"}, - {999999, 2, 960, false, "extremely high sample rate"}, - {48000, 0, 960, false, "zero channels"}, - {48000, -1, 960, false, "negative channels"}, - {48000, 100, 960, false, "too many channels"}, - {48000, 2, 0, false, "zero frame size"}, - {48000, 2, -1, false, "negative frame size"}, - {48000, 2, 999999, true, "extremely large frame size"}, - {48000, 2, 960, true, "valid configuration"}, - {44100, 1, 441, true, "valid mono configuration"}, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - err := ValidateInputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize) - if tc.shouldPass { - assert.NoError(t, err, "Should accept valid config: %s", tc.description) - } else { - assert.Error(t, err, "Should reject invalid config: %s", tc.description) - } - }) - } -} - -// testAtomicOperationConsistency tests atomic operations under high concurrency -func testAtomicOperationConsistency(t *testing.T) { - var counter int64 - var wg sync.WaitGroup - const numGoroutines = 100 - const incrementsPerGoroutine = 1000 - - // Launch multiple goroutines performing atomic operations - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < incrementsPerGoroutine; j++ { - atomic.AddInt64(&counter, 1) - } - }() - } - - wg.Wait() - - // Verify final count is correct - expected := int64(numGoroutines * incrementsPerGoroutine) - actual := atomic.LoadInt64(&counter) - assert.Equal(t, expected, actual, "Atomic operations should be consistent") -} - -// TestErrorRecoveryScenarios tests various error recovery scenarios -func TestErrorRecoveryScenarios(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"NetworkConnectionLoss", testNetworkConnectionLossRecovery}, - {"ProcessCrashRecovery", testProcessCrashRecovery}, - {"ResourceExhaustionRecovery", testResourceExhaustionRecovery}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -// testNetworkConnectionLossRecovery tests recovery from network connection loss -func testNetworkConnectionLossRecovery(t *testing.T) { - // Create a temporary socket that we can close to simulate connection loss - tempDir := t.TempDir() - socketPath := fmt.Sprintf("%s/test_recovery.sock", tempDir) - - // Create and immediately close a socket to test connection failure - listener, err := net.Listen("unix", socketPath) - if err != nil { - t.Skipf("Cannot create test socket: %v", err) - } - listener.Close() // Close immediately to simulate connection loss - - // Remove socket file to ensure connection will fail - os.Remove(socketPath) - - // Test that components handle connection loss gracefully - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // This should handle the connection failure gracefully - err = manager.Start() - if err != nil { - t.Logf("Expected connection failure handled: %v", err) - } - - // Cleanup - manager.Stop() -} - -// testProcessCrashRecovery tests recovery from process crashes -func testProcessCrashRecovery(t *testing.T) { - if testing.Short() { - t.Skip("Skipping process crash test in short mode") - } - - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Start supervisor (will likely fail in test environment, but should handle gracefully) - err := supervisor.Start() - if err != nil { - t.Logf("Supervisor start failed as expected in test environment: %v", err) - } - - // Verify supervisor can be stopped cleanly even after start failure - supervisor.Stop() - assert.False(t, supervisor.IsRunning()) -} - -// testResourceExhaustionRecovery tests recovery from resource exhaustion -func testResourceExhaustionRecovery(t *testing.T) { - // Test with resource constraints - manager := NewAudioInputIPCManager() - require.NotNil(t, manager) - - // Simulate resource exhaustion by rapid start/stop cycles - for i := 0; i < 20; i++ { - err := manager.Start() - if err != nil { - t.Logf("Resource exhaustion cycle %d: %v", i, err) - } - manager.Stop() - // No delay to stress test resource management - } - - // Verify system can still function after resource stress - err := manager.Start() - if err != nil { - t.Logf("Final start after resource stress: %v", err) - } - manager.Stop() - assert.False(t, manager.IsRunning()) -} diff --git a/internal/audio/supervisor_test.go b/internal/audio/supervisor_test.go deleted file mode 100644 index 57fe7a9a..00000000 --- a/internal/audio/supervisor_test.go +++ /dev/null @@ -1,393 +0,0 @@ -//go:build integration && cgo -// +build integration,cgo - -package audio - -import ( - "context" - "os" - "os/exec" - "sync" - "syscall" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestSupervisorRestart tests various supervisor restart scenarios -func TestSupervisorRestart(t *testing.T) { - tests := []struct { - name string - testFunc func(t *testing.T) - description string - }{ - { - name: "BasicRestart", - testFunc: testBasicSupervisorRestart, - description: "Test basic supervisor restart functionality", - }, - { - name: "ProcessCrashRestart", - testFunc: testProcessCrashRestart, - description: "Test supervisor restart after process crash", - }, - { - name: "MaxRestartAttempts", - testFunc: testMaxRestartAttempts, - description: "Test supervisor respects max restart attempts", - }, - { - name: "ExponentialBackoff", - testFunc: testExponentialBackoff, - description: "Test supervisor exponential backoff behavior", - }, - { - name: "HealthMonitoring", - testFunc: testHealthMonitoring, - description: "Test supervisor health monitoring", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Logf("Running supervisor test: %s - %s", tt.name, tt.description) - tt.testFunc(t) - }) - } -} - -// testBasicSupervisorRestart tests basic restart functionality -func testBasicSupervisorRestart(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - // Create a mock supervisor with a simple test command - supervisor := &AudioInputSupervisor{ - logger: getTestLogger(), - maxRestarts: 3, - restartDelay: 100 * time.Millisecond, - healthCheckInterval: 200 * time.Millisecond, - } - - // Use a simple command that will exit quickly for testing - testCmd := exec.CommandContext(ctx, "sleep", "0.5") - supervisor.cmd = testCmd - - var wg sync.WaitGroup - wg.Add(1) - - // Start supervisor - go func() { - defer wg.Done() - supervisor.Start(ctx) - }() - - // Wait for initial process to start and exit - time.Sleep(1 * time.Second) - - // Verify that supervisor attempted restart - assert.True(t, supervisor.GetRestartCount() > 0, "Supervisor should have attempted restart") - - // Stop supervisor - cancel() - wg.Wait() -} - -// testProcessCrashRestart tests restart after process crash -func testProcessCrashRestart(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) - defer cancel() - - supervisor := &AudioInputSupervisor{ - logger: getTestLogger(), - maxRestarts: 2, - restartDelay: 200 * time.Millisecond, - healthCheckInterval: 100 * time.Millisecond, - } - - // Create a command that will crash (exit with non-zero code) - testCmd := exec.CommandContext(ctx, "sh", "-c", "sleep 0.2 && exit 1") - supervisor.cmd = testCmd - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - supervisor.Start(ctx) - }() - - // Wait for process to crash and restart attempts - time.Sleep(2 * time.Second) - - // Verify restart attempts were made - restartCount := supervisor.GetRestartCount() - assert.True(t, restartCount > 0, "Supervisor should have attempted restart after crash") - assert.True(t, restartCount <= 2, "Supervisor should not exceed max restart attempts") - - cancel() - wg.Wait() -} - -// testMaxRestartAttempts tests that supervisor respects max restart limit -func testMaxRestartAttempts(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - maxRestarts := 3 - supervisor := &AudioInputSupervisor{ - logger: getTestLogger(), - maxRestarts: maxRestarts, - restartDelay: 50 * time.Millisecond, - healthCheckInterval: 50 * time.Millisecond, - } - - // Command that immediately fails - testCmd := exec.CommandContext(ctx, "false") // 'false' command always exits with code 1 - supervisor.cmd = testCmd - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - supervisor.Start(ctx) - }() - - // Wait for all restart attempts to complete - time.Sleep(2 * time.Second) - - // Verify that supervisor stopped after max attempts - restartCount := supervisor.GetRestartCount() - assert.Equal(t, maxRestarts, restartCount, "Supervisor should stop after max restart attempts") - assert.False(t, supervisor.IsRunning(), "Supervisor should not be running after max attempts") - - cancel() - wg.Wait() -} - -// testExponentialBackoff tests the exponential backoff behavior -func testExponentialBackoff(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) - defer cancel() - - supervisor := &AudioInputSupervisor{ - logger: getTestLogger(), - maxRestarts: 3, - restartDelay: 100 * time.Millisecond, // Base delay - healthCheckInterval: 50 * time.Millisecond, - } - - // Command that fails immediately - testCmd := exec.CommandContext(ctx, "false") - supervisor.cmd = testCmd - - var restartTimes []time.Time - var mu sync.Mutex - - // Hook into restart events to measure timing - originalRestart := supervisor.restart - supervisor.restart = func() { - mu.Lock() - restartTimes = append(restartTimes, time.Now()) - mu.Unlock() - if originalRestart != nil { - originalRestart() - } - } - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - supervisor.Start(ctx) - }() - - // Wait for restart attempts - time.Sleep(3 * time.Second) - - mu.Lock() - defer mu.Unlock() - - // Verify exponential backoff (each delay should be longer than the previous) - if len(restartTimes) >= 2 { - for i := 1; i < len(restartTimes); i++ { - delay := restartTimes[i].Sub(restartTimes[i-1]) - expectedMinDelay := time.Duration(i) * 100 * time.Millisecond - assert.True(t, delay >= expectedMinDelay, - "Restart delay should increase exponentially: attempt %d delay %v should be >= %v", - i, delay, expectedMinDelay) - } - } - - cancel() - wg.Wait() -} - -// testHealthMonitoring tests the health monitoring functionality -func testHealthMonitoring(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - supervisor := &AudioInputSupervisor{ - logger: getTestLogger(), - maxRestarts: 2, - restartDelay: 100 * time.Millisecond, - healthCheckInterval: 50 * time.Millisecond, - } - - // Command that runs for a while then exits - testCmd := exec.CommandContext(ctx, "sleep", "1") - supervisor.cmd = testCmd - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - supervisor.Start(ctx) - }() - - // Initially should be running - time.Sleep(200 * time.Millisecond) - assert.True(t, supervisor.IsRunning(), "Supervisor should be running initially") - - // Wait for process to exit and health check to detect it - time.Sleep(1.5 * time.Second) - - // Should have detected process exit and attempted restart - assert.True(t, supervisor.GetRestartCount() > 0, "Health monitoring should detect process exit") - - cancel() - wg.Wait() -} - -// TestAudioInputSupervisorIntegration tests the actual AudioInputSupervisor -func TestAudioInputSupervisorIntegration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - // Create a real supervisor instance - supervisor := NewAudioInputSupervisor() - require.NotNil(t, supervisor, "Supervisor should be created") - - // Test that supervisor can be started and stopped cleanly - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - // This will likely fail due to missing audio hardware in test environment, - // but we're testing the supervisor logic, not the audio functionality - supervisor.Start(ctx) - }() - - // Let it run briefly - time.Sleep(500 * time.Millisecond) - - // Stop the supervisor - cancel() - wg.Wait() - - // Verify clean shutdown - assert.False(t, supervisor.IsRunning(), "Supervisor should not be running after context cancellation") -} - -// Mock supervisor for testing (simplified version) -type AudioInputSupervisor struct { - logger zerolog.Logger - cmd *exec.Cmd - maxRestarts int - restartDelay time.Duration - healthCheckInterval time.Duration - restartCount int - running bool - mu sync.RWMutex - restart func() // Hook for testing -} - -func (s *AudioInputSupervisor) Start(ctx context.Context) error { - s.mu.Lock() - s.running = true - s.mu.Unlock() - - for s.restartCount < s.maxRestarts { - select { - case <-ctx.Done(): - s.mu.Lock() - s.running = false - s.mu.Unlock() - return ctx.Err() - default: - } - - // Start process - if s.cmd != nil { - err := s.cmd.Start() - if err != nil { - s.logger.Error().Err(err).Msg("Failed to start process") - s.restartCount++ - time.Sleep(s.getBackoffDelay()) - continue - } - - // Wait for process to exit - err = s.cmd.Wait() - if err != nil { - s.logger.Error().Err(err).Msg("Process exited with error") - } - } - - s.restartCount++ - if s.restart != nil { - s.restart() - } - - if s.restartCount < s.maxRestarts { - time.Sleep(s.getBackoffDelay()) - } - } - - s.mu.Lock() - s.running = false - s.mu.Unlock() - return nil -} - -func (s *AudioInputSupervisor) IsRunning() bool { - s.mu.RLock() - defer s.mu.RUnlock() - return s.running -} - -func (s *AudioInputSupervisor) GetRestartCount() int { - s.mu.RLock() - defer s.mu.RUnlock() - return s.restartCount -} - -func (s *AudioInputSupervisor) getBackoffDelay() time.Duration { - // Simple exponential backoff - multiplier := 1 << uint(s.restartCount) - if multiplier > 8 { - multiplier = 8 // Cap the multiplier - } - return s.restartDelay * time.Duration(multiplier) -} - -// NewAudioInputSupervisor creates a new supervisor for testing -func NewAudioInputSupervisor() *AudioInputSupervisor { - return &AudioInputSupervisor{ - logger: getTestLogger(), - maxRestarts: getMaxRestartAttempts(), - restartDelay: getInitialRestartDelay(), - healthCheckInterval: 1 * time.Second, - } -} \ No newline at end of file diff --git a/internal/audio/supervisor_unit_test.go b/internal/audio/supervisor_unit_test.go deleted file mode 100644 index 0cab46f0..00000000 --- a/internal/audio/supervisor_unit_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package audio - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewAudioOutputSupervisor(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - assert.NotNil(t, supervisor) - assert.False(t, supervisor.IsRunning()) -} - -func TestAudioOutputSupervisorStart(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Test successful start - err := supervisor.Start() - assert.NoError(t, err) - assert.True(t, supervisor.IsRunning()) - - // Test starting already running supervisor - err = supervisor.Start() - assert.Error(t, err) - assert.Contains(t, err.Error(), "already running") - - // Cleanup - supervisor.Stop() -} - -func TestAudioOutputSupervisorStop(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Test stopping non-running supervisor - supervisor.Stop() - assert.False(t, supervisor.IsRunning()) - - // Start and then stop - err := supervisor.Start() - require.NoError(t, err) - assert.True(t, supervisor.IsRunning()) - - supervisor.Stop() - assert.False(t, supervisor.IsRunning()) -} - -func TestAudioOutputSupervisorIsRunning(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Test initial state - assert.False(t, supervisor.IsRunning()) - - // Test after start - err := supervisor.Start() - require.NoError(t, err) - assert.True(t, supervisor.IsRunning()) - - // Test after stop - supervisor.Stop() - assert.False(t, supervisor.IsRunning()) -} - -func TestAudioOutputSupervisorGetProcessMetrics(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Test metrics when not running - metrics := supervisor.GetProcessMetrics() - assert.NotNil(t, metrics) - - // Start and test metrics - err := supervisor.Start() - require.NoError(t, err) - - metrics = supervisor.GetProcessMetrics() - assert.NotNil(t, metrics) - - // Cleanup - supervisor.Stop() -} - -func TestAudioOutputSupervisorConcurrentOperations(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - var wg sync.WaitGroup - - // Test concurrent start/stop operations - for i := 0; i < 10; i++ { - wg.Add(2) - go func() { - defer wg.Done() - _ = supervisor.Start() - }() - go func() { - defer wg.Done() - supervisor.Stop() - }() - } - - // Test concurrent metric access - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _ = supervisor.GetProcessMetrics() - }() - } - - // Test concurrent status checks - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _ = supervisor.IsRunning() - }() - } - - wg.Wait() - - // Cleanup - supervisor.Stop() -} - -func TestAudioOutputSupervisorMultipleStartStop(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Test multiple start/stop cycles - for i := 0; i < 5; i++ { - err := supervisor.Start() - assert.NoError(t, err) - assert.True(t, supervisor.IsRunning()) - - supervisor.Stop() - assert.False(t, supervisor.IsRunning()) - } -} - -func TestAudioOutputSupervisorHealthCheck(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Start supervisor - err := supervisor.Start() - require.NoError(t, err) - - // Give some time for health monitoring to initialize - time.Sleep(100 * time.Millisecond) - - // Test that supervisor is still running - assert.True(t, supervisor.IsRunning()) - - // Cleanup - supervisor.Stop() -} - -func TestAudioOutputSupervisorProcessManagement(t *testing.T) { - supervisor := NewAudioOutputSupervisor() - require.NotNil(t, supervisor) - - // Start supervisor - err := supervisor.Start() - require.NoError(t, err) - - // Give some time for process management to initialize - time.Sleep(200 * time.Millisecond) - - // Test that supervisor is managing processes - assert.True(t, supervisor.IsRunning()) - - // Cleanup - supervisor.Stop() - - // Ensure supervisor stopped cleanly - assert.False(t, supervisor.IsRunning()) -} - -// Benchmark tests -func BenchmarkAudioOutputSupervisor(b *testing.B) { - supervisor := NewAudioOutputSupervisor() - - b.Run("Start", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = supervisor.Start() - supervisor.Stop() - } - }) - - b.Run("GetProcessMetrics", func(b *testing.B) { - _ = supervisor.Start() - defer supervisor.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = supervisor.GetProcessMetrics() - } - }) - - b.Run("IsRunning", func(b *testing.B) { - _ = supervisor.Start() - defer supervisor.Stop() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = supervisor.IsRunning() - } - }) -} diff --git a/internal/audio/test_utils.go b/internal/audio/test_utils.go deleted file mode 100644 index 536742ad..00000000 --- a/internal/audio/test_utils.go +++ /dev/null @@ -1,319 +0,0 @@ -//go:build integration -// +build integration - -package audio - -import ( - "context" - "net" - "os" - "sync" - "time" - - "github.com/jetkvm/kvm/internal/logging" - "github.com/rs/zerolog" -) - -// Test utilities and mock implementations for integration tests - -// MockAudioIPCServer provides a mock IPC server for testing -type AudioIPCServer struct { - socketPath string - logger zerolog.Logger - listener net.Listener - connections map[net.Conn]bool - mu sync.RWMutex - running bool -} - -// Start starts the mock IPC server -func (s *AudioIPCServer) Start(ctx context.Context) error { - // Remove existing socket file - os.Remove(s.socketPath) - - listener, err := net.Listen("unix", s.socketPath) - if err != nil { - return err - } - s.listener = listener - s.connections = make(map[net.Conn]bool) - - s.mu.Lock() - s.running = true - s.mu.Unlock() - - go s.acceptConnections(ctx) - - <-ctx.Done() - s.Stop() - return ctx.Err() -} - -// Stop stops the mock IPC server -func (s *AudioIPCServer) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if !s.running { - return - } - - s.running = false - - if s.listener != nil { - s.listener.Close() - } - - // Close all connections - for conn := range s.connections { - conn.Close() - } - - // Clean up socket file - os.Remove(s.socketPath) -} - -// acceptConnections handles incoming connections -func (s *AudioIPCServer) acceptConnections(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - default: - } - - conn, err := s.listener.Accept() - if err != nil { - select { - case <-ctx.Done(): - return - default: - s.logger.Error().Err(err).Msg("Failed to accept connection") - continue - } - } - - s.mu.Lock() - s.connections[conn] = true - s.mu.Unlock() - - go s.handleConnection(ctx, conn) - } -} - -// handleConnection handles a single connection -func (s *AudioIPCServer) handleConnection(ctx context.Context, conn net.Conn) { - defer func() { - s.mu.Lock() - delete(s.connections, conn) - s.mu.Unlock() - conn.Close() - }() - - buffer := make([]byte, 4096) - for { - select { - case <-ctx.Done(): - return - default: - } - - // Set read timeout - conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - n, err := conn.Read(buffer) - if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - continue - } - return - } - - // Process received data (for testing, we just log it) - s.logger.Debug().Int("bytes", n).Msg("Received data from client") - } -} - -// AudioInputIPCServer provides a mock input IPC server -type AudioInputIPCServer struct { - *AudioIPCServer -} - -// Test message structures -type OutputMessage struct { - Type OutputMessageType - Timestamp int64 - Data []byte -} - -type InputMessage struct { - Type InputMessageType - Timestamp int64 - Data []byte -} - -// Test configuration helpers -func getTestConfig() *AudioConfigConstants { - return &AudioConfigConstants{ - // Basic audio settings - SampleRate: 48000, - Channels: 2, - MaxAudioFrameSize: 4096, - - // IPC settings - OutputMagicNumber: 0x4A4B4F55, // "JKOU" - InputMagicNumber: 0x4A4B4D49, // "JKMI" - WriteTimeout: 5 * time.Second, - HeaderSize: 17, - MaxFrameSize: 4096, - MessagePoolSize: 100, - - // Supervisor settings - MaxRestartAttempts: 3, - InitialRestartDelay: 1 * time.Second, - MaxRestartDelay: 30 * time.Second, - HealthCheckInterval: 5 * time.Second, - - // Quality presets - AudioQualityLowOutputBitrate: 32000, - AudioQualityMediumOutputBitrate: 96000, - AudioQualityHighOutputBitrate: 192000, - AudioQualityUltraOutputBitrate: 320000, - - AudioQualityLowInputBitrate: 16000, - AudioQualityMediumInputBitrate: 64000, - AudioQualityHighInputBitrate: 128000, - AudioQualityUltraInputBitrate: 256000, - - AudioQualityLowSampleRate: 24000, - AudioQualityMediumSampleRate: 48000, - AudioQualityHighSampleRate: 48000, - AudioQualityUltraSampleRate: 48000, - - AudioQualityLowChannels: 1, - AudioQualityMediumChannels: 2, - AudioQualityHighChannels: 2, - AudioQualityUltraChannels: 2, - - AudioQualityLowFrameSize: 20 * time.Millisecond, - AudioQualityMediumFrameSize: 20 * time.Millisecond, - AudioQualityHighFrameSize: 20 * time.Millisecond, - AudioQualityUltraFrameSize: 20 * time.Millisecond, - - AudioQualityMicLowSampleRate: 16000, - - // Metrics settings - MetricsUpdateInterval: 1 * time.Second, - - // Latency settings - DefaultTargetLatencyMS: 50, - DefaultOptimizationIntervalSeconds: 5, - DefaultAdaptiveThreshold: 0.8, - DefaultStatsIntervalSeconds: 5, - - // Buffer settings - DefaultBufferPoolSize: 100, - DefaultControlPoolSize: 50, - DefaultFramePoolSize: 200, - DefaultMaxPooledFrames: 500, - DefaultPoolCleanupInterval: 30 * time.Second, - - // Process monitoring - MaxCPUPercent: 100.0, - MinCPUPercent: 0.0, - DefaultClockTicks: 100, - DefaultMemoryGB: 4.0, - MaxWarmupSamples: 10, - WarmupCPUSamples: 5, - MetricsChannelBuffer: 100, - MinValidClockTicks: 50, - MaxValidClockTicks: 1000, - PageSize: 4096, - - // CGO settings (for cgo builds) - CGOOpusBitrate: 96000, - CGOOpusComplexity: 3, - CGOOpusVBR: 1, - CGOOpusVBRConstraint: 1, - CGOOpusSignalType: 3, - CGOOpusBandwidth: 1105, - CGOOpusDTX: 0, - CGOSampleRate: 48000, - - // Batch processing - BatchProcessorFramesPerBatch: 10, - BatchProcessorTimeout: 100 * time.Millisecond, - - // Granular metrics - GranularMetricsMaxSamples: 1000, - GranularMetricsLogInterval: 30 * time.Second, - GranularMetricsCleanupInterval: 5 * time.Minute, - } -} - -// setupTestEnvironment sets up the test environment -func setupTestEnvironment() { - // Use test configuration - UpdateConfig(getTestConfig()) - - // Initialize logging for tests - logging.SetLevel("debug") -} - -// cleanupTestEnvironment cleans up after tests -func cleanupTestEnvironment() { - // Reset to default configuration - UpdateConfig(DefaultAudioConfig()) -} - -// createTestLogger creates a logger for testing -func createTestLogger(name string) zerolog.Logger { - return zerolog.New(os.Stdout).With(). - Timestamp(). - Str("component", name). - Str("test", "true"). - Logger() -} - -// waitForCondition waits for a condition to be true with timeout -func waitForCondition(condition func() bool, timeout time.Duration, checkInterval time.Duration) bool { - timeout_timer := time.NewTimer(timeout) - defer timeout_timer.Stop() - - ticker := time.NewTicker(checkInterval) - defer ticker.Stop() - - for { - select { - case <-timeout_timer.C: - return false - case <-ticker.C: - if condition() { - return true - } - } - } -} - -// TestHelper provides common test functionality -type TestHelper struct { - tempDir string - logger zerolog.Logger -} - -// NewTestHelper creates a new test helper -func NewTestHelper(tempDir string) *TestHelper { - return &TestHelper{ - tempDir: tempDir, - logger: createTestLogger("test-helper"), - } -} - -// CreateTempSocket creates a temporary socket path -func (h *TestHelper) CreateTempSocket(name string) string { - return filepath.Join(h.tempDir, name) -} - -// GetLogger returns the test logger -func (h *TestHelper) GetLogger() zerolog.Logger { - return h.logger -} \ No newline at end of file diff --git a/internal/audio/validation_test.go b/internal/audio/validation_test.go deleted file mode 100644 index b2716051..00000000 --- a/internal/audio/validation_test.go +++ /dev/null @@ -1,541 +0,0 @@ -//go:build cgo -// +build cgo - -package audio - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestValidationFunctions provides comprehensive testing of all validation functions -// to ensure they catch breaking changes and regressions effectively -func TestValidationFunctions(t *testing.T) { - // Initialize validation cache for testing - InitValidationCache() - - tests := []struct { - name string - testFunc func(t *testing.T) - }{ - {"AudioQualityValidation", testAudioQualityValidation}, - {"FrameDataValidation", testFrameDataValidation}, - {"BufferSizeValidation", testBufferSizeValidation}, - {"ThreadPriorityValidation", testThreadPriorityValidation}, - {"LatencyValidation", testLatencyValidation}, - {"MetricsIntervalValidation", testMetricsIntervalValidation}, - {"SampleRateValidation", testSampleRateValidation}, - {"ChannelCountValidation", testChannelCountValidation}, - {"BitrateValidation", testBitrateValidation}, - {"FrameDurationValidation", testFrameDurationValidation}, - {"IPCConfigValidation", testIPCConfigValidation}, - {"AdaptiveBufferConfigValidation", testAdaptiveBufferConfigValidation}, - {"AudioConfigCompleteValidation", testAudioConfigCompleteValidation}, - {"ZeroCopyFrameValidation", testZeroCopyFrameValidation}, - {"AudioFrameFastValidation", testAudioFrameFastValidation}, - {"ErrorWrappingValidation", testErrorWrappingValidation}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.testFunc(t) - }) - } -} - -// testAudioQualityValidation tests audio quality validation with boundary conditions -func testAudioQualityValidation(t *testing.T) { - // Test valid quality levels - validQualities := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} - for _, quality := range validQualities { - err := ValidateAudioQuality(quality) - assert.NoError(t, err, "Valid quality %d should pass validation", quality) - } - - // Test invalid quality levels - invalidQualities := []AudioQuality{-1, 4, 100, -100} - for _, quality := range invalidQualities { - err := ValidateAudioQuality(quality) - assert.Error(t, err, "Invalid quality %d should fail validation", quality) - assert.Contains(t, err.Error(), "invalid audio quality level", "Error should mention audio quality") - } -} - -// testFrameDataValidation tests frame data validation with various edge cases using modern validation -func testFrameDataValidation(t *testing.T) { - config := GetConfig() - - // Test empty data - err := ValidateAudioFrame([]byte{}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "frame data is empty") - - // Test data above maximum size - largeData := make([]byte, config.MaxAudioFrameSize+1) - err = ValidateAudioFrame(largeData) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exceeds maximum") - - // Test valid data - validData := make([]byte, 1000) // Within bounds - if len(validData) <= config.MaxAudioFrameSize { - err = ValidateAudioFrame(validData) - assert.NoError(t, err) - } -} - -// testBufferSizeValidation tests buffer size validation -func testBufferSizeValidation(t *testing.T) { - config := GetConfig() - - // Test negative and zero sizes - invalidSizes := []int{-1, -100, 0} - for _, size := range invalidSizes { - err := ValidateBufferSize(size) - assert.Error(t, err, "Buffer size %d should be invalid", size) - assert.Contains(t, err.Error(), "must be positive") - } - - // Test size exceeding maximum - err := ValidateBufferSize(config.SocketMaxBuffer + 1) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exceeds maximum") - - // Test valid sizes - validSizes := []int{1, 1024, 4096, config.SocketMaxBuffer} - for _, size := range validSizes { - err := ValidateBufferSize(size) - assert.NoError(t, err, "Buffer size %d should be valid", size) - } -} - -// testThreadPriorityValidation tests thread priority validation -func testThreadPriorityValidation(t *testing.T) { - // Test valid priorities - validPriorities := []int{-20, -10, 0, 10, 19} - for _, priority := range validPriorities { - err := ValidateThreadPriority(priority) - assert.NoError(t, err, "Priority %d should be valid", priority) - } - - // Test invalid priorities - invalidPriorities := []int{-21, -100, 20, 100} - for _, priority := range invalidPriorities { - err := ValidateThreadPriority(priority) - assert.Error(t, err, "Priority %d should be invalid", priority) - assert.Contains(t, err.Error(), "outside valid range") - } -} - -// testLatencyValidation tests latency validation -func testLatencyValidation(t *testing.T) { - config := GetConfig() - - // Test negative latency - err := ValidateLatency(-1 * time.Millisecond) - assert.Error(t, err) - assert.Contains(t, err.Error(), "cannot be negative") - - // Test zero latency (should be valid) - err = ValidateLatency(0) - assert.NoError(t, err) - - // Test very small positive latency - err = ValidateLatency(500 * time.Microsecond) - assert.Error(t, err) - assert.Contains(t, err.Error(), "below minimum") - - // Test latency exceeding maximum - err = ValidateLatency(config.MaxLatency + time.Second) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exceeds maximum") - - // Test valid latencies - validLatencies := []time.Duration{ - 1 * time.Millisecond, - 10 * time.Millisecond, - 100 * time.Millisecond, - config.MaxLatency, - } - for _, latency := range validLatencies { - err := ValidateLatency(latency) - assert.NoError(t, err, "Latency %v should be valid", latency) - } -} - -// testMetricsIntervalValidation tests metrics interval validation -func testMetricsIntervalValidation(t *testing.T) { - config := GetConfig() - - // Test interval below minimum - err := ValidateMetricsInterval(config.MinMetricsUpdateInterval - time.Millisecond) - assert.Error(t, err) - - // Test interval above maximum - err = ValidateMetricsInterval(config.MaxMetricsUpdateInterval + time.Second) - assert.Error(t, err) - - // Test valid intervals - validIntervals := []time.Duration{ - config.MinMetricsUpdateInterval, - config.MaxMetricsUpdateInterval, - (config.MinMetricsUpdateInterval + config.MaxMetricsUpdateInterval) / 2, - } - for _, interval := range validIntervals { - err := ValidateMetricsInterval(interval) - assert.NoError(t, err, "Interval %v should be valid", interval) - } -} - -// testSampleRateValidation tests sample rate validation -func testSampleRateValidation(t *testing.T) { - config := GetConfig() - - // Test negative and zero sample rates - invalidRates := []int{-1, -48000, 0} - for _, rate := range invalidRates { - err := ValidateSampleRate(rate) - assert.Error(t, err, "Sample rate %d should be invalid", rate) - assert.Contains(t, err.Error(), "must be positive") - } - - // Test unsupported sample rates - unsupportedRates := []int{1000, 12345, 96001} - for _, rate := range unsupportedRates { - err := ValidateSampleRate(rate) - assert.Error(t, err, "Sample rate %d should be unsupported", rate) - assert.Contains(t, err.Error(), "not in supported rates") - } - - // Test valid sample rates - for _, rate := range config.ValidSampleRates { - err := ValidateSampleRate(rate) - assert.NoError(t, err, "Sample rate %d should be valid", rate) - } -} - -// testChannelCountValidation tests channel count validation -func testChannelCountValidation(t *testing.T) { - config := GetConfig() - - // Test invalid channel counts - invalidCounts := []int{-1, -10, 0} - for _, count := range invalidCounts { - err := ValidateChannelCount(count) - assert.Error(t, err, "Channel count %d should be invalid", count) - assert.Contains(t, err.Error(), "must be positive") - } - - // Test channel count exceeding maximum - err := ValidateChannelCount(config.MaxChannels + 1) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exceeds maximum") - - // Test valid channel counts - validCounts := []int{1, 2, config.MaxChannels} - for _, count := range validCounts { - err := ValidateChannelCount(count) - assert.NoError(t, err, "Channel count %d should be valid", count) - } -} - -// testBitrateValidation tests bitrate validation -func testBitrateValidation(t *testing.T) { - // Test invalid bitrates - invalidBitrates := []int{-1, -1000, 0} - for _, bitrate := range invalidBitrates { - err := ValidateBitrate(bitrate) - assert.Error(t, err, "Bitrate %d should be invalid", bitrate) - assert.Contains(t, err.Error(), "must be positive") - } - - // Test bitrate below minimum (in kbps) - err := ValidateBitrate(5) // 5 kbps = 5000 bps < 6000 bps minimum - assert.Error(t, err) - assert.Contains(t, err.Error(), "below minimum") - - // Test bitrate above maximum (in kbps) - err = ValidateBitrate(511) // 511 kbps = 511000 bps > 510000 bps maximum - assert.Error(t, err) - assert.Contains(t, err.Error(), "exceeds maximum") - - // Test valid bitrates (in kbps) - validBitrates := []int{ - 6, // 6 kbps = 6000 bps (minimum) - 64, // Medium quality preset - 128, // High quality preset - 192, // Ultra quality preset - 510, // 510 kbps = 510000 bps (maximum) - } - for _, bitrate := range validBitrates { - err := ValidateBitrate(bitrate) - assert.NoError(t, err, "Bitrate %d kbps should be valid", bitrate) - } -} - -// testFrameDurationValidation tests frame duration validation -func testFrameDurationValidation(t *testing.T) { - config := GetConfig() - - // Test invalid durations - invalidDurations := []time.Duration{-1 * time.Millisecond, -1 * time.Second, 0} - for _, duration := range invalidDurations { - err := ValidateFrameDuration(duration) - assert.Error(t, err, "Duration %v should be invalid", duration) - assert.Contains(t, err.Error(), "must be positive") - } - - // Test duration below minimum - err := ValidateFrameDuration(config.MinFrameDuration - time.Microsecond) - assert.Error(t, err) - assert.Contains(t, err.Error(), "below minimum") - - // Test duration above maximum - err = ValidateFrameDuration(config.MaxFrameDuration + time.Second) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exceeds maximum") - - // Test valid durations - validDurations := []time.Duration{ - config.MinFrameDuration, - config.MaxFrameDuration, - 20 * time.Millisecond, // Common frame duration - } - for _, duration := range validDurations { - err := ValidateFrameDuration(duration) - assert.NoError(t, err, "Duration %v should be valid", duration) - } -} - -// testIPCConfigValidation tests IPC configuration validation -func testIPCConfigValidation(t *testing.T) { - config := GetConfig() - - // Test invalid configurations for input IPC - invalidConfigs := []struct { - sampleRate, channels, frameSize int - description string - }{ - {0, 2, 960, "zero sample rate"}, - {48000, 0, 960, "zero channels"}, - {48000, 2, 0, "zero frame size"}, - {config.MinSampleRate - 1, 2, 960, "sample rate below minimum"}, - {config.MaxSampleRate + 1, 2, 960, "sample rate above maximum"}, - {48000, config.MaxChannels + 1, 960, "too many channels"}, - {48000, -1, 960, "negative channels"}, - {48000, 2, -1, "negative frame size"}, - } - - for _, tc := range invalidConfigs { - // Test input IPC validation - err := ValidateInputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize) - assert.Error(t, err, "Input IPC config should be invalid: %s", tc.description) - - // Test output IPC validation - err = ValidateOutputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize) - assert.Error(t, err, "Output IPC config should be invalid: %s", tc.description) - } - - // Test valid configuration - err := ValidateInputIPCConfig(48000, 2, 960) - assert.NoError(t, err) - err = ValidateOutputIPCConfig(48000, 2, 960) - assert.NoError(t, err) -} - -// testAdaptiveBufferConfigValidation tests adaptive buffer configuration validation -func testAdaptiveBufferConfigValidation(t *testing.T) { - config := GetConfig() - - // Test invalid configurations - invalidConfigs := []struct { - minSize, maxSize, defaultSize int - description string - }{ - {0, 1024, 512, "zero min size"}, - {-1, 1024, 512, "negative min size"}, - {512, 0, 256, "zero max size"}, - {512, -1, 256, "negative max size"}, - {512, 1024, 0, "zero default size"}, - {512, 1024, -1, "negative default size"}, - {1024, 512, 768, "min >= max"}, - {512, 1024, 256, "default < min"}, - {512, 1024, 2048, "default > max"}, - {512, config.SocketMaxBuffer + 1, 1024, "max exceeds global limit"}, - } - - for _, tc := range invalidConfigs { - err := ValidateAdaptiveBufferConfig(tc.minSize, tc.maxSize, tc.defaultSize) - assert.Error(t, err, "Config should be invalid: %s", tc.description) - } - - // Test valid configuration - err := ValidateAdaptiveBufferConfig(512, 4096, 1024) - assert.NoError(t, err) -} - -// testAudioConfigCompleteValidation tests complete audio configuration validation -func testAudioConfigCompleteValidation(t *testing.T) { - // Test valid configuration using actual preset values - validConfig := AudioConfig{ - Quality: AudioQualityMedium, - Bitrate: 64, // kbps - matches medium quality preset - SampleRate: 48000, - Channels: 2, - FrameSize: 20 * time.Millisecond, - } - err := ValidateAudioConfigComplete(validConfig) - assert.NoError(t, err) - - // Test invalid quality - invalidQualityConfig := validConfig - invalidQualityConfig.Quality = AudioQuality(99) - err = ValidateAudioConfigComplete(invalidQualityConfig) - assert.Error(t, err) - assert.Contains(t, err.Error(), "quality validation failed") - - // Test invalid bitrate - invalidBitrateConfig := validConfig - invalidBitrateConfig.Bitrate = -1 - err = ValidateAudioConfigComplete(invalidBitrateConfig) - assert.Error(t, err) - assert.Contains(t, err.Error(), "bitrate validation failed") - - // Test invalid sample rate - invalidSampleRateConfig := validConfig - invalidSampleRateConfig.SampleRate = 12345 - err = ValidateAudioConfigComplete(invalidSampleRateConfig) - assert.Error(t, err) - assert.Contains(t, err.Error(), "sample rate validation failed") - - // Test invalid channels - invalidChannelsConfig := validConfig - invalidChannelsConfig.Channels = 0 - err = ValidateAudioConfigComplete(invalidChannelsConfig) - assert.Error(t, err) - assert.Contains(t, err.Error(), "channel count validation failed") - - // Test invalid frame duration - invalidFrameDurationConfig := validConfig - invalidFrameDurationConfig.FrameSize = -1 * time.Millisecond - err = ValidateAudioConfigComplete(invalidFrameDurationConfig) - assert.Error(t, err) - assert.Contains(t, err.Error(), "frame duration validation failed") -} - -// testZeroCopyFrameValidation tests zero-copy frame validation -func testZeroCopyFrameValidation(t *testing.T) { - // Test nil frame - err := ValidateZeroCopyFrame(nil) - assert.Error(t, err) - - // Note: We can't easily test ZeroCopyAudioFrame without creating actual instances - // This would require more complex setup, but the validation logic is tested -} - -// testAudioFrameFastValidation tests fast audio frame validation -func testAudioFrameFastValidation(t *testing.T) { - config := GetConfig() - - // Test empty data - err := ValidateAudioFrame([]byte{}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "frame data is empty") - - // Test data exceeding maximum size - largeData := make([]byte, config.MaxAudioFrameSize+1) - err = ValidateAudioFrame(largeData) - assert.Error(t, err) - assert.Contains(t, err.Error(), "exceeds maximum") - - // Test valid data - validData := make([]byte, 1000) - err = ValidateAudioFrame(validData) - assert.NoError(t, err) -} - -// testErrorWrappingValidation tests error wrapping functionality -func testErrorWrappingValidation(t *testing.T) { - // Test wrapping nil error - wrapped := WrapWithMetadata(nil, "component", "operation", map[string]interface{}{"key": "value"}) - assert.Nil(t, wrapped) - - // Test wrapping actual error - originalErr := assert.AnError - metadata := map[string]interface{}{ - "frame_size": 1024, - "quality": "high", - } - wrapped = WrapWithMetadata(originalErr, "audio", "decode", metadata) - require.NotNil(t, wrapped) - assert.Contains(t, wrapped.Error(), "audio.decode") - assert.Contains(t, wrapped.Error(), "assert.AnError") - assert.Contains(t, wrapped.Error(), "metadata") - assert.Contains(t, wrapped.Error(), "frame_size") - assert.Contains(t, wrapped.Error(), "quality") -} - -// TestValidationIntegration tests validation functions working together -func TestValidationIntegration(t *testing.T) { - // Test that validation functions work correctly with actual audio configurations - presets := GetAudioQualityPresets() - require.NotEmpty(t, presets) - - for quality, config := range presets { - t.Run(fmt.Sprintf("Quality_%d", quality), func(t *testing.T) { - // Validate the preset configuration - err := ValidateAudioConfigComplete(config) - assert.NoError(t, err, "Preset configuration for quality %d should be valid", quality) - - // Validate individual components - err = ValidateAudioQuality(config.Quality) - assert.NoError(t, err, "Quality should be valid") - - err = ValidateBitrate(config.Bitrate) - assert.NoError(t, err, "Bitrate should be valid") - - err = ValidateSampleRate(config.SampleRate) - assert.NoError(t, err, "Sample rate should be valid") - - err = ValidateChannelCount(config.Channels) - assert.NoError(t, err, "Channel count should be valid") - - err = ValidateFrameDuration(config.FrameSize) - assert.NoError(t, err, "Frame duration should be valid") - }) - } -} - -// TestValidationPerformance ensures validation functions are efficient -func TestValidationPerformance(t *testing.T) { - if testing.Short() { - t.Skip("Skipping performance test in short mode") - } - - // Initialize validation cache for performance testing - InitValidationCache() - - // Test that validation functions complete quickly - start := time.Now() - iterations := 10000 - - for i := 0; i < iterations; i++ { - _ = ValidateAudioQuality(AudioQualityMedium) - _ = ValidateBufferSize(1024) - _ = ValidateChannelCount(2) - _ = ValidateSampleRate(48000) - _ = ValidateBitrate(96) // 96 kbps - } - - elapsed := time.Since(start) - perIteration := elapsed / time.Duration(iterations) - - // Performance expectations for JetKVM (ARM Cortex-A7 @ 1GHz, 256MB RAM) - // Audio processing must not interfere with primary KVM functionality - assert.Less(t, perIteration, 200*time.Microsecond, "Validation should not impact KVM performance") - t.Logf("Validation performance: %v per iteration", perIteration) -} diff --git a/internal/audio/zero_copy.go b/internal/audio/zero_copy.go index 9ff235a4..ce066a17 100644 --- a/internal/audio/zero_copy.go +++ b/internal/audio/zero_copy.go @@ -142,26 +142,10 @@ func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool { // Get retrieves a zero-copy frame from the pool func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { - // Get cached config once for all metrics operations - cachedConfig := GetCachedConfig() - enableMetrics := cachedConfig.GetEnableMetricsCollection() - - // Remove metrics overhead in critical path - use sampling instead - var wasHit bool - var startTime time.Time - trackMetrics := enableMetrics && atomic.LoadInt64(&p.counter)%100 == 0 // Sample 1% of operations if enabled - if trackMetrics { - startTime = time.Now() - } - // Memory guard: Track allocation count to prevent excessive memory usage allocationCount := atomic.LoadInt64(&p.allocationCount) if allocationCount > int64(p.maxPoolSize*2) { // If we've allocated too many frames, force pool reuse - if enableMetrics { - atomic.AddInt64(&p.missCount, 1) - } - wasHit = true // Pool reuse counts as hit frame := p.pool.Get().(*ZeroCopyAudioFrame) frame.mutex.Lock() frame.refCount = 1 @@ -169,18 +153,12 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { frame.data = frame.data[:0] frame.mutex.Unlock() - // Record metrics only for sampled operations - if trackMetrics { - latency := time.Since(startTime) - GetGranularMetricsCollector().RecordZeroCopyGet(latency, wasHit) - } return frame } // First try pre-allocated frames for fastest access p.mutex.Lock() if len(p.preallocated) > 0 { - wasHit = true frame := p.preallocated[len(p.preallocated)-1] p.preallocated = p.preallocated[:len(p.preallocated)-1] p.mutex.Unlock() @@ -191,23 +169,11 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { frame.data = frame.data[:0] frame.mutex.Unlock() - if enableMetrics { - atomic.AddInt64(&p.hitCount, 1) - } - - // Record metrics only for sampled operations - if trackMetrics { - latency := time.Since(startTime) - GetGranularMetricsCollector().RecordZeroCopyGet(latency, wasHit) - } return frame } p.mutex.Unlock() // Try sync.Pool next and track allocation - if enableMetrics { - atomic.AddInt64(&p.allocationCount, 1) - } frame := p.pool.Get().(*ZeroCopyAudioFrame) frame.mutex.Lock() frame.refCount = 1 @@ -215,27 +181,17 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { frame.data = frame.data[:0] frame.mutex.Unlock() - wasHit = true // Pool hit atomic.AddInt64(&p.hitCount, 1) - // Record metrics only for sampled operations - if trackMetrics { - latency := time.Since(startTime) - GetGranularMetricsCollector().RecordZeroCopyGet(latency, wasHit) - } return frame } // Put returns a zero-copy frame to the pool func (p *ZeroCopyFramePool) Put(frame *ZeroCopyAudioFrame) { - // Get cached config once for all metrics operations - cachedConfig := GetCachedConfig() - enableMetrics := cachedConfig.GetEnableMetricsCollection() - - // Remove metrics overhead in critical path - use sampling instead + // Metrics collection removed var startTime time.Time - trackMetrics := enableMetrics && atomic.LoadInt64(&p.counter)%100 == 0 // Sample 1% of operations if enabled - if trackMetrics { + trackMetrics := false // Metrics disabled + if false { startTime = time.Now() } @@ -271,7 +227,8 @@ func (p *ZeroCopyFramePool) Put(frame *ZeroCopyAudioFrame) { // Return to sync.Pool p.pool.Put(frame) - if enableMetrics { + // Metrics collection removed + if false { atomic.AddInt64(&p.counter, 1) } } else { diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 11a7c6e5..1a256625 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -360,7 +360,7 @@ export default function Actionbar({ checkIfStateChanged(open); return (
- +
); }} diff --git a/ui/src/components/AudioConfigDisplay.tsx b/ui/src/components/AudioConfigDisplay.tsx deleted file mode 100644 index 76c332e8..00000000 --- a/ui/src/components/AudioConfigDisplay.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { cx } from "@/cva.config"; - -interface AudioConfig { - Quality: number; - Bitrate: number; - SampleRate: number; - Channels: number; - FrameSize: string; -} - -interface AudioConfigDisplayProps { - config: AudioConfig; - variant?: 'default' | 'success' | 'info'; - className?: string; -} - -const variantStyles = { - default: "bg-slate-50 text-slate-600 dark:bg-slate-700 dark:text-slate-400", - success: "bg-green-50 text-green-600 dark:bg-green-900/20 dark:text-green-400", - info: "bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400" -}; - -export function AudioConfigDisplay({ config, variant = 'default', className }: AudioConfigDisplayProps) { - return ( -
-
- Sample Rate: {config.SampleRate}Hz - Channels: {config.Channels} - Bitrate: {config.Bitrate}kbps - Frame: {config.FrameSize} -
-
- ); -} \ No newline at end of file diff --git a/ui/src/components/AudioLevelMeter.tsx b/ui/src/components/AudioLevelMeter.tsx deleted file mode 100644 index dc293d21..00000000 --- a/ui/src/components/AudioLevelMeter.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import clsx from 'clsx'; - -interface AudioLevelMeterProps { - level: number; // 0-100 percentage - isActive: boolean; - className?: string; - size?: 'sm' | 'md' | 'lg'; - showLabel?: boolean; -} - -export const AudioLevelMeter: React.FC = ({ - level, - isActive, - className, - size = 'md', - showLabel = true -}) => { - const sizeClasses = { - sm: 'h-1', - md: 'h-2', - lg: 'h-3' - }; - - const getLevelColor = (level: number) => { - if (level < 20) return 'bg-green-500'; - if (level < 60) return 'bg-yellow-500'; - return 'bg-red-500'; - }; - - const getTextColor = (level: number) => { - if (level < 20) return 'text-green-600 dark:text-green-400'; - if (level < 60) return 'text-yellow-600 dark:text-yellow-400'; - return 'text-red-600 dark:text-red-400'; - }; - - return ( -
- {showLabel && ( -
- - Microphone Level - - - {isActive ? `${Math.round(level)}%` : 'No Signal'} - -
- )} - -
-
-
- - {/* Peak indicators */} -
- 0% - 50% - 100% -
-
- ); -}; \ No newline at end of file diff --git a/ui/src/components/AudioMetricsDashboard.tsx b/ui/src/components/AudioMetricsDashboard.tsx deleted file mode 100644 index 5987684c..00000000 --- a/ui/src/components/AudioMetricsDashboard.tsx +++ /dev/null @@ -1,880 +0,0 @@ -import { useEffect, useState } from "react"; -import { MdGraphicEq, MdSignalWifi4Bar, MdError, MdMic } from "react-icons/md"; -import { LuActivity, LuClock, LuHardDrive, LuSettings, LuCpu, LuMemoryStick } from "react-icons/lu"; - -import { AudioLevelMeter } from "@components/AudioLevelMeter"; -import StatChart from "@components/StatChart"; -import { cx } from "@/cva.config"; -import { useMicrophone } from "@/hooks/useMicrophone"; -import { useAudioLevel } from "@/hooks/useAudioLevel"; -import { useAudioEvents } from "@/hooks/useAudioEvents"; -import api from "@/api"; -import { AUDIO_CONFIG } from "@/config/constants"; -import audioQualityService from "@/services/audioQualityService"; - -interface AudioMetrics { - frames_received: number; - frames_dropped: number; - bytes_processed: number; - last_frame_time: string; - connection_drops: number; - average_latency: string; -} - -interface MicrophoneMetrics { - frames_sent: number; - frames_dropped: number; - bytes_processed: number; - last_frame_time: string; - connection_drops: number; - average_latency: string; -} - -interface ProcessMetrics { - cpu_percent: number; - memory_percent: number; - memory_rss: number; - memory_vms: number; - running: boolean; -} - -interface AudioConfig { - Quality: number; - Bitrate: number; - SampleRate: number; - Channels: number; - FrameSize: string; -} - -// Quality labels will be managed by the audio quality service -const getQualityLabels = () => audioQualityService.getQualityLabels(); - -// Format percentage values to 2 decimal places -function formatPercentage(value: number | null | undefined): string { - if (value === null || value === undefined || isNaN(value)) { - return "0.00%"; - } - return `${value.toFixed(2)}%`; -} - -function formatMemoryMB(rssBytes: number | null | undefined): string { - if (rssBytes === null || rssBytes === undefined || isNaN(rssBytes)) { - return "0.00 MB"; - } - const mb = rssBytes / (1024 * 1024); - return `${mb.toFixed(2)} MB`; -} - -// Default system memory estimate in MB (will be replaced by actual value from backend) -const DEFAULT_SYSTEM_MEMORY_MB = 4096; // 4GB default - -// Create chart array similar to connectionStats.tsx -function createChartArray( - stream: Map, - metric: K, -): { date: number; stat: T[K] | null }[] { - const stat = Array.from(stream).map(([key, stats]) => { - return { date: key, stat: stats[metric] }; - }); - - // Sort the dates to ensure they are in chronological order - const sortedStat = stat.map(x => x.date).sort((a, b) => a - b); - - // Determine the earliest statistic date - const earliestStat = sortedStat[0]; - - // Current time in seconds since the Unix epoch - const now = Math.floor(Date.now() / 1000); - - // Determine the starting point for the chart data - const firstChartDate = earliestStat ? Math.min(earliestStat, now - 120) : now - 120; - - // Generate the chart array for the range between 'firstChartDate' and 'now' - return Array.from({ length: now - firstChartDate }, (_, i) => { - const currentDate = firstChartDate + i; - return { - date: currentDate, - // Find the statistic for 'currentDate', or use the last known statistic if none exists for that date - stat: stat.find(x => x.date === currentDate)?.stat ?? null, - }; - }); -} - -export default function AudioMetricsDashboard() { - // System memory state - const [systemMemoryMB, setSystemMemoryMB] = useState(DEFAULT_SYSTEM_MEMORY_MB); - - // Use WebSocket-based audio events for real-time updates - const { - audioMetrics, - microphoneMetrics: wsMicrophoneMetrics, - audioProcessMetrics: wsAudioProcessMetrics, - microphoneProcessMetrics: wsMicrophoneProcessMetrics, - isConnected: wsConnected - } = useAudioEvents(); - - // Fetch system memory information on component mount - useEffect(() => { - const fetchSystemMemory = async () => { - try { - const response = await api.GET('/system/memory'); - const data = await response.json(); - setSystemMemoryMB(data.total_memory_mb); - } catch { - // Failed to fetch system memory, using default - } - }; - fetchSystemMemory(); - }, []); - - // Update historical data when WebSocket process metrics are received - useEffect(() => { - if (wsConnected && wsAudioProcessMetrics && wsAudioProcessMetrics.running) { - const now = Math.floor(Date.now() / 1000); // Convert to seconds for StatChart - // Validate that now is a valid number - if (isNaN(now)) return; - - const cpuStat = isNaN(wsAudioProcessMetrics.cpu_percent) ? null : wsAudioProcessMetrics.cpu_percent; - - setAudioCpuStats(prev => { - const newMap = new Map(prev); - newMap.set(now, { cpu_percent: cpuStat }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - - setAudioMemoryStats(prev => { - const newMap = new Map(prev); - const memoryRss = isNaN(wsAudioProcessMetrics.memory_rss) ? null : wsAudioProcessMetrics.memory_rss; - newMap.set(now, { memory_rss: memoryRss }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - } - }, [wsConnected, wsAudioProcessMetrics]); - - useEffect(() => { - if (wsConnected && wsMicrophoneProcessMetrics) { - const now = Math.floor(Date.now() / 1000); // Convert to seconds for StatChart - // Validate that now is a valid number - if (isNaN(now)) return; - - const cpuStat = isNaN(wsMicrophoneProcessMetrics.cpu_percent) ? null : wsMicrophoneProcessMetrics.cpu_percent; - - setMicCpuStats(prev => { - const newMap = new Map(prev); - newMap.set(now, { cpu_percent: cpuStat }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - - setMicMemoryStats(prev => { - const newMap = new Map(prev); - const memoryRss = isNaN(wsMicrophoneProcessMetrics.memory_rss) ? null : wsMicrophoneProcessMetrics.memory_rss; - newMap.set(now, { memory_rss: memoryRss }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - } - }, [wsConnected, wsMicrophoneProcessMetrics]); - - // Fallback state for when WebSocket is not connected - const [fallbackMetrics, setFallbackMetrics] = useState(null); - const [fallbackMicrophoneMetrics, setFallbackMicrophoneMetrics] = useState(null); - const [fallbackConnected, setFallbackConnected] = useState(false); - - // Process metrics state (fallback for when WebSocket is not connected) - const [fallbackAudioProcessMetrics, setFallbackAudioProcessMetrics] = useState(null); - const [fallbackMicrophoneProcessMetrics, setFallbackMicrophoneProcessMetrics] = useState(null); - - // Historical data for charts using Maps for better memory management - const [audioCpuStats, setAudioCpuStats] = useState>(new Map()); - const [audioMemoryStats, setAudioMemoryStats] = useState>(new Map()); - const [micCpuStats, setMicCpuStats] = useState>(new Map()); - const [micMemoryStats, setMicMemoryStats] = useState>(new Map()); - - // Configuration state (these don't change frequently, so we can load them once) - const [config, setConfig] = useState(null); - const [microphoneConfig, setMicrophoneConfig] = useState(null); - const [lastUpdate, setLastUpdate] = useState(new Date()); - - // Use WebSocket data when available, fallback to polling data otherwise - const metrics = wsConnected && audioMetrics !== null ? audioMetrics : fallbackMetrics; - const microphoneMetrics = wsConnected && wsMicrophoneMetrics !== null ? wsMicrophoneMetrics : fallbackMicrophoneMetrics; - const audioProcessMetrics = wsConnected && wsAudioProcessMetrics !== null ? wsAudioProcessMetrics : fallbackAudioProcessMetrics; - const microphoneProcessMetrics = wsConnected && wsMicrophoneProcessMetrics !== null ? wsMicrophoneProcessMetrics : fallbackMicrophoneProcessMetrics; - const isConnected = wsConnected ? wsConnected : fallbackConnected; - - // Microphone state for audio level monitoring - const { isMicrophoneActive, isMicrophoneMuted, microphoneStream } = useMicrophone(); - const { audioLevel, isAnalyzing } = useAudioLevel( - isMicrophoneActive ? microphoneStream : null, - { - enabled: isMicrophoneActive, - updateInterval: 120, - }); - - useEffect(() => { - // Load initial configuration (only once) - loadAudioConfig(); - - // Set up fallback polling only when WebSocket is not connected - if (!wsConnected) { - loadAudioData(); - const interval = setInterval(loadAudioData, 1000); - return () => clearInterval(interval); - } - }, [wsConnected]); - - const loadAudioConfig = async () => { - try { - // Use centralized audio quality service - const { audio, microphone } = await audioQualityService.loadAllConfigurations(); - - if (audio) { - setConfig(audio.current); - } - - if (microphone) { - setMicrophoneConfig(microphone.current); - } - } catch (error) { - console.error("Failed to load audio config:", error); - } - }; - - const loadAudioData = async () => { - try { - // Load metrics - const metricsResp = await api.GET("/audio/metrics"); - if (metricsResp.ok) { - const metricsData = await metricsResp.json(); - setFallbackMetrics(metricsData); - // Consider connected if API call succeeds, regardless of frame count - setFallbackConnected(true); - setLastUpdate(new Date()); - } else { - setFallbackConnected(false); - } - - // Load audio process metrics - try { - const audioProcessResp = await api.GET("/audio/process-metrics"); - if (audioProcessResp.ok) { - const audioProcessData = await audioProcessResp.json(); - setFallbackAudioProcessMetrics(audioProcessData); - - // Update historical data for charts (keep last 120 seconds) - if (audioProcessData.running) { - const now = Math.floor(Date.now() / 1000); // Convert to seconds for StatChart - // Validate that now is a valid number - if (isNaN(now)) return; - - const cpuStat = isNaN(audioProcessData.cpu_percent) ? null : audioProcessData.cpu_percent; - const memoryRss = isNaN(audioProcessData.memory_rss) ? null : audioProcessData.memory_rss; - - setAudioCpuStats(prev => { - const newMap = new Map(prev); - newMap.set(now, { cpu_percent: cpuStat }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - - setAudioMemoryStats(prev => { - const newMap = new Map(prev); - newMap.set(now, { memory_rss: memoryRss }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - } - } - } catch { - // Audio process metrics not available - } - - // Load microphone metrics - try { - const micResp = await api.GET("/microphone/metrics"); - if (micResp.ok) { - const micData = await micResp.json(); - setFallbackMicrophoneMetrics(micData); - } - } catch { - // Microphone metrics might not be available, that's okay - // Microphone metrics not available - } - - // Load microphone process metrics - try { - const micProcessResp = await api.GET("/microphone/process-metrics"); - if (micProcessResp.ok) { - const micProcessData = await micProcessResp.json(); - setFallbackMicrophoneProcessMetrics(micProcessData); - - // Update historical data for charts (keep last 120 seconds) - const now = Math.floor(Date.now() / 1000); // Convert to seconds for StatChart - // Validate that now is a valid number - if (isNaN(now)) return; - - const cpuStat = isNaN(micProcessData.cpu_percent) ? null : micProcessData.cpu_percent; - const memoryRss = isNaN(micProcessData.memory_rss) ? null : micProcessData.memory_rss; - - setMicCpuStats(prev => { - const newMap = new Map(prev); - newMap.set(now, { cpu_percent: cpuStat }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - - setMicMemoryStats(prev => { - const newMap = new Map(prev); - newMap.set(now, { memory_rss: memoryRss }); - // Keep only last 120 seconds of data for memory management - const cutoff = now - 120; - for (const [key] of newMap) { - if (key < cutoff) newMap.delete(key); - } - return newMap; - }); - } - } catch { - // Microphone process metrics not available - } - } catch (error) { - console.error("Failed to load audio data:", error); - setFallbackConnected(false); - } - }; - - const formatBytes = (bytes: number) => { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; - }; - - const formatNumber = (num: number) => { - return new Intl.NumberFormat().format(num); - }; - - const getDropRate = () => { - if (!metrics || metrics.frames_received === 0) return 0; - return ((metrics.frames_dropped / metrics.frames_received) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER); - }; - - - - - - const getQualityColor = (quality: number) => { - switch (quality) { - case 0: return "text-yellow-600 dark:text-yellow-400"; - case 1: return "text-blue-600 dark:text-blue-400"; - case 2: return "text-green-600 dark:text-green-400"; - case 3: return "text-purple-600 dark:text-purple-400"; - default: return "text-slate-600 dark:text-slate-400"; - } - }; - - return ( -
- {/* Header */} -
-
- -

- Audio Metrics -

-
-
-
- - {isConnected ? "Active" : "Inactive"} - -
-
- - {/* Current Configuration */} -
- {config && ( -
-
- - - Audio Output Config - -
-
-
- Quality: - - {getQualityLabels()[config.Quality]} - -
-
- Bitrate: - - {config.Bitrate}kbps - -
-
- Sample Rate: - - {config.SampleRate}Hz - -
-
- Channels: - - {config.Channels} - -
-
-
- )} - - {microphoneConfig && ( -
-
- - - Audio Input Config - -
-
-
- Quality: - - {getQualityLabels()[microphoneConfig.Quality]} - -
-
- Bitrate: - - {microphoneConfig.Bitrate}kbps - -
-
- Sample Rate: - - {microphoneConfig.SampleRate}Hz - -
-
- Channels: - - {microphoneConfig.Channels} - -
-
-
- )} -
- - - - {/* Subprocess Resource Usage - Histogram View */} -
- {/* Audio Output Subprocess */} - {audioProcessMetrics && ( -
-
- - - Audio Output Process - -
-
-
-
-

CPU Usage

-
- -
-
-
-

Memory Usage

-
- ({ - date: item.date, - stat: item.stat ? item.stat / (1024 * 1024) : null // Convert bytes to MB - }))} - unit="MB" - domain={[0, systemMemoryMB]} - /> -
-
-
-
-
- {formatPercentage(audioProcessMetrics.cpu_percent)} -
-
CPU
-
-
-
- {formatMemoryMB(audioProcessMetrics.memory_rss)} -
-
Memory
-
-
-
-
- )} - - {/* Microphone Input Subprocess */} - {microphoneProcessMetrics && ( -
-
- - - Microphone Input Process - -
-
-
-
-

CPU Usage

-
- -
-
-
-

Memory Usage

-
- ({ - date: item.date, - stat: item.stat ? item.stat / (1024 * 1024) : null // Convert bytes to MB - }))} - unit="MB" - domain={[0, systemMemoryMB]} - /> -
-
-
-
-
- {formatPercentage(microphoneProcessMetrics.cpu_percent)} -
-
CPU
-
-
-
- {formatMemoryMB(microphoneProcessMetrics.memory_rss)} -
-
Memory
-
-
-
-
- )} -
- - {/* Performance Metrics */} - {metrics && ( -
- {/* Audio Output Frames */} -
-
- - - Audio Output - -
-
-
-
- {formatNumber(metrics.frames_received)} -
-
- Frames Received -
-
-
-
0 - ? "text-red-600 dark:text-red-400" - : "text-green-600 dark:text-green-400" - )}> - {formatNumber(metrics.frames_dropped)} -
-
- Frames Dropped -
-
-
- - {/* Drop Rate */} -
-
- - Drop Rate - - AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD - ? "text-red-600 dark:text-red-400" - : getDropRate() > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD - ? "text-yellow-600 dark:text-yellow-400" - : "text-green-600 dark:text-green-400" - )}> - {getDropRate().toFixed(AUDIO_CONFIG.PERCENTAGE_DECIMAL_PLACES)}% - -
-
-
AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD - ? "bg-red-500" - : getDropRate() > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD - ? "bg-yellow-500" - : "bg-green-500" - )} - style={{ width: `${Math.min(getDropRate(), AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE)}%` }} - /> -
-
-
- - {/* Microphone Input Metrics */} - {microphoneMetrics && ( -
-
- - - Microphone Input - -
-
-
-
- {formatNumber(microphoneMetrics.frames_sent)} -
-
- Frames Sent -
-
-
-
0 - ? "text-red-600 dark:text-red-400" - : "text-green-600 dark:text-green-400" - )}> - {formatNumber(microphoneMetrics.frames_dropped)} -
-
- Frames Dropped -
-
-
- - {/* Microphone Drop Rate */} -
-
- - Drop Rate - - 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD - ? "text-red-600 dark:text-red-400" - : (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD - ? "text-yellow-600 dark:text-yellow-400" - : "text-green-600 dark:text-green-400" - )}> - {microphoneMetrics.frames_sent > 0 ? ((microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER).toFixed(AUDIO_CONFIG.PERCENTAGE_DECIMAL_PLACES) : "0.00"}% - -
-
-
0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD - ? "bg-red-500" - : (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD - ? "bg-yellow-500" - : "bg-green-500" - )} - style={{ - width: `${Math.min(microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0, AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE)}%` - }} - /> -
-
- - {/* Microphone Audio Level */} - {isMicrophoneActive && ( -
- -
- )} - - {/* Microphone Connection Health */} -
-
- - - Connection Health - -
-
-
- - Connection Drops: - - 0 - ? "text-red-600 dark:text-red-400" - : "text-green-600 dark:text-green-400" - )}> - {formatNumber(microphoneMetrics.connection_drops)} - -
- {microphoneMetrics.average_latency && ( -
- - Avg Latency: - - - {microphoneMetrics.average_latency} - -
- )} -
-
-
- )} - - {/* Data Transfer */} -
-
- - - Data Transfer - -
-
-
- {formatBytes(metrics.bytes_processed)} -
-
- Total Processed -
-
-
- - {/* Connection Health */} -
-
- - - Connection Health - -
-
-
- - Connection Drops: - - 0 - ? "text-red-600 dark:text-red-400" - : "text-green-600 dark:text-green-400" - )}> - {formatNumber(metrics.connection_drops)} - -
- {metrics.average_latency && ( -
- - Avg Latency: - - - {metrics.average_latency} - -
- )} -
-
-
- )} - - {/* Last Update */} -
- - Last updated: {lastUpdate.toLocaleTimeString()} -
- - {/* No Data State */} - {!metrics && ( -
- -

- No Audio Data -

-

- Audio metrics will appear when audio streaming is active. -

-
- )} -
- ); -} \ No newline at end of file diff --git a/ui/src/components/AudioStatusIndicator.tsx b/ui/src/components/AudioStatusIndicator.tsx deleted file mode 100644 index 06bd5ad0..00000000 --- a/ui/src/components/AudioStatusIndicator.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { cx } from "@/cva.config"; - -interface AudioMetrics { - frames_dropped: number; - // Add other metrics properties as needed -} - -interface AudioStatusIndicatorProps { - metrics?: AudioMetrics; - label: string; - className?: string; -} - -export function AudioStatusIndicator({ metrics, label, className }: AudioStatusIndicatorProps) { - const hasIssues = metrics && metrics.frames_dropped > 0; - - return ( -
-
- {hasIssues ? "Issues" : "Good"} -
-
{label}
-
- ); -} \ No newline at end of file diff --git a/ui/src/components/popovers/AudioControlPopover.tsx b/ui/src/components/popovers/AudioControlPopover.tsx index 16d66a65..1d633dde 100644 --- a/ui/src/components/popovers/AudioControlPopover.tsx +++ b/ui/src/components/popovers/AudioControlPopover.tsx @@ -1,15 +1,10 @@ import { useEffect, useState } from "react"; import { MdVolumeOff, MdVolumeUp, MdGraphicEq, MdMic, MdMicOff, MdRefresh } from "react-icons/md"; -import { LuActivity, LuSignal } from "react-icons/lu"; +import { LuActivity } from "react-icons/lu"; import { Button } from "@components/Button"; -import { AudioLevelMeter } from "@components/AudioLevelMeter"; -import { AudioConfigDisplay } from "@components/AudioConfigDisplay"; -import { AudioStatusIndicator } from "@components/AudioStatusIndicator"; import { cx } from "@/cva.config"; -import { useUiStore } from "@/hooks/stores"; import { useAudioDevices } from "@/hooks/useAudioDevices"; -import { useAudioLevel } from "@/hooks/useAudioLevel"; import { useAudioEvents } from "@/hooks/useAudioEvents"; import api from "@/api"; import notifications from "@/notifications"; @@ -49,10 +44,9 @@ const getQualityLabels = () => audioQualityService.getQualityLabels(); interface AudioControlPopoverProps { microphone: MicrophoneHookReturn; - open?: boolean; // whether the popover is open (controls analysis) } -export default function AudioControlPopover({ microphone, open }: AudioControlPopoverProps) { +export default function AudioControlPopover({ microphone }: AudioControlPopoverProps) { const [currentConfig, setCurrentConfig] = useState(null); const [currentMicrophoneConfig, setCurrentMicrophoneConfig] = useState(null); @@ -68,8 +62,6 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo // Use WebSocket-based audio events for real-time updates const { audioMuted, - audioMetrics, - microphoneMetrics, isConnected: wsConnected } = useAudioEvents(); @@ -92,16 +84,11 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo // Use WebSocket data exclusively - no polling fallback const isMuted = audioMuted ?? false; - const metrics = audioMetrics; - const micMetrics = microphoneMetrics; const isConnected = wsConnected; - // Audio level monitoring - enable only when popover is open and microphone is active to save resources - const analysisEnabled = (open ?? true) && isMicrophoneActive; - const { audioLevel, isAnalyzing } = useAudioLevel(analysisEnabled ? microphoneStream : null, { - enabled: analysisEnabled, - updateInterval: 120, // 8-10 fps to reduce CPU without losing UX quality - }); + // Simple audio level placeholder + const audioLevel = 0; + const isAnalyzing = isMicrophoneActive && !isMicrophoneMuted; // Audio devices const { @@ -116,7 +103,7 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo refreshDevices } = useAudioDevices(); - const { toggleSidebarView } = useUiStore(); + // Load initial configurations once - cache to prevent repeated calls useEffect(() => { @@ -375,15 +362,17 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
- {/* Audio Level Meter */} + {/* Audio Level Display */} {isMicrophoneActive && (
- +
+
+ Audio Level: {Math.round(audioLevel * 100)}% +
+
+ {isMicrophoneMuted ? 'Muted' : isAnalyzing ? 'Active' : 'Inactive'} +
+
{/* Debug information */}
@@ -514,10 +503,11 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
{currentMicrophoneConfig && ( - +
+ Quality: {currentMicrophoneConfig.Quality} | + Bitrate: {currentMicrophoneConfig.Bitrate}kbps | + Sample Rate: {currentMicrophoneConfig.SampleRate}Hz +
)}
)} @@ -551,59 +541,32 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
{currentConfig && ( - +
+ Quality: {currentConfig.Quality} | + Bitrate: {currentConfig.Bitrate}kbps | + Sample Rate: {currentConfig.SampleRate}Hz +
)}
- {/* Quick Status Summary */} -
-
- - - Quick Status - -
- - {metrics ? ( -
- - - {micMetrics && ( - - )} + {/* Audio Level Display */} + {isMicrophoneActive && ( +
+
+ + + Microphone Level +
- ) : ( +
-
- No data available +
+ Level: {Math.round(audioLevel * 100)}%
- )} -
- - {/* Audio Metrics Dashboard Button */} -
-
-
-
+ )} +
); diff --git a/ui/src/components/sidebar/AudioMetricsSidebar.tsx b/ui/src/components/sidebar/AudioMetricsSidebar.tsx deleted file mode 100644 index a07ad0fd..00000000 --- a/ui/src/components/sidebar/AudioMetricsSidebar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import SidebarHeader from "@/components/SidebarHeader"; -import { useUiStore } from "@/hooks/stores"; -import AudioMetricsDashboard from "@/components/AudioMetricsDashboard"; - -export default function AudioMetricsSidebar() { - const setSidebarView = useUiStore(state => state.setSidebarView); - - return ( - <> - -
- -
- - ); -} \ No newline at end of file diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index c0b5d620..97e821a7 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -40,7 +40,7 @@ const appendStatToMap = ( }; // Constants and types -export type AvailableSidebarViews = "connection-stats" | "audio-metrics"; +export type AvailableSidebarViews = "connection-stats"; export type AvailableTerminalTypes = "kvm" | "serial" | "none"; export interface User { diff --git a/ui/src/hooks/useAudioEvents.ts b/ui/src/hooks/useAudioEvents.ts index fb788572..0a1672ee 100644 --- a/ui/src/hooks/useAudioEvents.ts +++ b/ui/src/hooks/useAudioEvents.ts @@ -7,11 +7,7 @@ import { NETWORK_CONFIG } from '../config/constants'; // Audio event types matching the backend export type AudioEventType = | 'audio-mute-changed' - | 'audio-metrics-update' | 'microphone-state-changed' - | 'microphone-metrics-update' - | 'audio-process-metrics' - | 'microphone-process-metrics' | 'audio-device-changed'; // Audio event data interfaces @@ -19,39 +15,11 @@ export interface AudioMuteData { muted: boolean; } -export interface AudioMetricsData { - frames_received: number; - frames_dropped: number; - bytes_processed: number; - last_frame_time: string; - connection_drops: number; - average_latency: string; -} - export interface MicrophoneStateData { running: boolean; session_active: boolean; } -export interface MicrophoneMetricsData { - frames_sent: number; - frames_dropped: number; - bytes_processed: number; - last_frame_time: string; - connection_drops: number; - average_latency: string; -} - -export interface ProcessMetricsData { - pid: number; - cpu_percent: number; - memory_rss: number; - memory_vms: number; - memory_percent: number; - running: boolean; - process_name: string; -} - export interface AudioDeviceChangedData { enabled: boolean; reason: string; @@ -60,7 +28,7 @@ export interface AudioDeviceChangedData { // Audio event structure export interface AudioEvent { type: AudioEventType; - data: AudioMuteData | AudioMetricsData | MicrophoneStateData | MicrophoneMetricsData | ProcessMetricsData | AudioDeviceChangedData; + data: AudioMuteData | MicrophoneStateData | AudioDeviceChangedData; } // Hook return type @@ -71,15 +39,9 @@ export interface UseAudioEventsReturn { // Audio state audioMuted: boolean | null; - audioMetrics: AudioMetricsData | null; // Microphone state microphoneState: MicrophoneStateData | null; - microphoneMetrics: MicrophoneMetricsData | null; - - // Process metrics - audioProcessMetrics: ProcessMetricsData | null; - microphoneProcessMetrics: ProcessMetricsData | null; // Device change events onAudioDeviceChanged?: (data: AudioDeviceChangedData) => void; @@ -99,11 +61,7 @@ const globalSubscriptionState = { export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedData) => void): UseAudioEventsReturn { // State for audio data const [audioMuted, setAudioMuted] = useState(null); - const [audioMetrics, setAudioMetrics] = useState(null); const [microphoneState, setMicrophoneState] = useState(null); - const [microphoneMetrics, setMicrophoneMetricsData] = useState(null); - const [audioProcessMetrics, setAudioProcessMetrics] = useState(null); - const [microphoneProcessMetrics, setMicrophoneProcessMetrics] = useState(null); // Local subscription state const [isLocallySubscribed, setIsLocallySubscribed] = useState(false); @@ -225,12 +183,6 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD break; } - case 'audio-metrics-update': { - const audioMetricsData = audioEvent.data as AudioMetricsData; - setAudioMetrics(audioMetricsData); - break; - } - case 'microphone-state-changed': { const micStateData = audioEvent.data as MicrophoneStateData; setMicrophoneState(micStateData); @@ -238,24 +190,6 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD break; } - case 'microphone-metrics-update': { - const micMetricsData = audioEvent.data as MicrophoneMetricsData; - setMicrophoneMetricsData(micMetricsData); - break; - } - - case 'audio-process-metrics': { - const audioProcessData = audioEvent.data as ProcessMetricsData; - setAudioProcessMetrics(audioProcessData); - break; - } - - case 'microphone-process-metrics': { - const micProcessData = audioEvent.data as ProcessMetricsData; - setMicrophoneProcessMetrics(micProcessData); - break; - } - case 'audio-device-changed': { const deviceChangedData = audioEvent.data as AudioDeviceChangedData; // Audio device changed @@ -320,15 +254,9 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD // Audio state audioMuted, - audioMetrics, // Microphone state microphoneState, - microphoneMetrics: microphoneMetrics, - - // Process metrics - audioProcessMetrics, - microphoneProcessMetrics, // Device change events onAudioDeviceChanged, diff --git a/ui/src/hooks/useAudioLevel.ts b/ui/src/hooks/useAudioLevel.ts deleted file mode 100644 index 93fa1ab3..00000000 --- a/ui/src/hooks/useAudioLevel.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -import { AUDIO_CONFIG } from '@/config/constants'; - -interface AudioLevelHookResult { - audioLevel: number; // 0-100 percentage - isAnalyzing: boolean; -} - -interface AudioLevelOptions { - enabled?: boolean; // Allow external control of analysis - updateInterval?: number; // Throttle updates (default from AUDIO_CONFIG) -} - -export const useAudioLevel = ( - stream: MediaStream | null, - options: AudioLevelOptions = {} -): AudioLevelHookResult => { - const { enabled = true, updateInterval = AUDIO_CONFIG.LEVEL_UPDATE_INTERVAL } = options; - - const [audioLevel, setAudioLevel] = useState(0); - const [isAnalyzing, setIsAnalyzing] = useState(false); - const audioContextRef = useRef(null); - const analyserRef = useRef(null); - const sourceRef = useRef(null); - const intervalRef = useRef(null); - const lastUpdateTimeRef = useRef(0); - - useEffect(() => { - if (!stream || !enabled) { - // Clean up when stream is null or disabled - if (intervalRef.current !== null) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - if (sourceRef.current) { - sourceRef.current.disconnect(); - sourceRef.current = null; - } - if (audioContextRef.current) { - audioContextRef.current.close(); - audioContextRef.current = null; - } - analyserRef.current = null; - setIsAnalyzing(false); - setAudioLevel(0); - return; - } - - const audioTracks = stream.getAudioTracks(); - if (audioTracks.length === 0) { - setIsAnalyzing(false); - setAudioLevel(0); - return; - } - - try { - // Create audio context and analyser - const audioContext = new (window.AudioContext || (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext)(); - const analyser = audioContext.createAnalyser(); - const source = audioContext.createMediaStreamSource(stream); - - // Configure analyser - use smaller FFT for better performance - analyser.fftSize = AUDIO_CONFIG.FFT_SIZE; - analyser.smoothingTimeConstant = AUDIO_CONFIG.SMOOTHING_TIME_CONSTANT; - - // Connect nodes - source.connect(analyser); - - // Store references - audioContextRef.current = audioContext; - analyserRef.current = analyser; - sourceRef.current = source; - - const dataArray = new Uint8Array(analyser.frequencyBinCount); - - const updateLevel = () => { - if (!analyserRef.current) return; - - const now = performance.now(); - - // Throttle updates to reduce CPU usage - if (now - lastUpdateTimeRef.current < updateInterval) { - return; - } - lastUpdateTimeRef.current = now; - - analyserRef.current.getByteFrequencyData(dataArray); - - // Optimized RMS calculation - process only relevant frequency bands - let sum = 0; - const relevantBins = Math.min(dataArray.length, AUDIO_CONFIG.RELEVANT_FREQUENCY_BINS); - for (let i = 0; i < relevantBins; i++) { - const value = dataArray[i]; - sum += value * value; - } - const rms = Math.sqrt(sum / relevantBins); - - // Convert to percentage (0-100) with better scaling - const level = Math.min(AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE, Math.max(0, (rms / AUDIO_CONFIG.RMS_SCALING_FACTOR) * AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE)); - setAudioLevel(Math.round(level)); - }; - - setIsAnalyzing(true); - - // Use setInterval instead of requestAnimationFrame for more predictable timing - intervalRef.current = window.setInterval(updateLevel, updateInterval); - - } catch { - // Audio level analyzer creation failed - silently handle - setIsAnalyzing(false); - setAudioLevel(0); - } - - // Cleanup function - return () => { - if (intervalRef.current !== null) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - if (sourceRef.current) { - sourceRef.current.disconnect(); - sourceRef.current = null; - } - if (audioContextRef.current) { - audioContextRef.current.close(); - audioContextRef.current = null; - } - analyserRef.current = null; - setIsAnalyzing(false); - setAudioLevel(0); - }; - }, [stream, enabled, updateInterval]); - - return { audioLevel, isAnalyzing }; -}; \ No newline at end of file diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 4401ceb2..8c3368f8 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -39,7 +39,6 @@ import WebRTCVideo from "@components/WebRTCVideo"; import { checkAuth, isInCloud, isOnDevice } from "@/main"; import DashboardNavbar from "@components/Header"; import ConnectionStatsSidebar from "@/components/sidebar/connectionStats"; -import AudioMetricsSidebar from "@/components/sidebar/AudioMetricsSidebar"; import { JsonRpcRequest, JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import Terminal from "@components/Terminal"; import { CLOUD_API, DEVICE_API } from "@/ui.config"; @@ -925,22 +924,7 @@ function SidebarContainer(props: SidebarContainerProps) { )} - {sidebarView === "audio-metrics" && ( - -
- -
-
- )} +
diff --git a/web.go b/web.go index 60b1c048..10a58fc6 100644 --- a/web.go +++ b/web.go @@ -24,8 +24,7 @@ import ( "github.com/google/uuid" "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" "golang.org/x/crypto/bcrypt" ) @@ -103,9 +102,6 @@ func setupRouter() *gin.Engine { // We use this to setup the device in the welcome page r.POST("/device/setup", handleSetup) - // A Prometheus metrics endpoint. - r.GET("/metrics", gin.WrapH(promhttp.Handler())) - // Developer mode protected routes developerModeRouter := r.Group("/developer/") developerModeRouter.Use(basicAuthProtectedMiddleware(true)) @@ -211,19 +207,6 @@ func setupRouter() *gin.Engine { }) }) - protected.GET("/audio/metrics", func(c *gin.Context) { - registry := audio.GetMetricsRegistry() - metrics := registry.GetAudioMetrics() - c.JSON(200, gin.H{ - "frames_received": metrics.FramesReceived, - "frames_dropped": metrics.FramesDropped, - "bytes_processed": metrics.BytesProcessed, - "last_frame_time": metrics.LastFrameTime, - "connection_drops": metrics.ConnectionDrops, - "average_latency": fmt.Sprintf("%.1fms", float64(metrics.AverageLatency.Nanoseconds())/1e6), - }) - }) - protected.GET("/microphone/quality", func(c *gin.Context) { config := audio.GetMicrophoneConfig() presets := audio.GetMicrophoneQualityPresets() @@ -399,103 +382,6 @@ func setupRouter() *gin.Engine { }) }) - protected.GET("/microphone/metrics", func(c *gin.Context) { - registry := audio.GetMetricsRegistry() - metrics := registry.GetAudioInputMetrics() - c.JSON(200, gin.H{ - "frames_sent": metrics.FramesSent, - "frames_dropped": metrics.FramesDropped, - "bytes_processed": metrics.BytesProcessed, - "last_frame_time": metrics.LastFrameTime.Format("2006-01-02T15:04:05.000Z"), - "connection_drops": metrics.ConnectionDrops, - "average_latency": fmt.Sprintf("%.1fms", float64(metrics.AverageLatency.Nanoseconds())/1e6), - }) - }) - - // Audio subprocess process metrics endpoints - protected.GET("/audio/process-metrics", func(c *gin.Context) { - // Access the global audio supervisor from main.go - if audioSupervisor == nil { - c.JSON(200, gin.H{ - "cpu_percent": 0.0, - "memory_percent": 0.0, - "memory_rss": 0, - "memory_vms": 0, - "running": false, - }) - return - } - - metrics := audioSupervisor.GetProcessMetrics() - if metrics == nil { - c.JSON(200, gin.H{ - "cpu_percent": 0.0, - "memory_percent": 0.0, - "memory_rss": 0, - "memory_vms": 0, - "running": false, - }) - return - } - - c.JSON(200, gin.H{ - "cpu_percent": metrics.CPUPercent, - "memory_percent": metrics.MemoryPercent, - "memory_rss": metrics.MemoryRSS, - "memory_vms": metrics.MemoryVMS, - "running": true, - }) - }) - - // Audio memory allocation metrics endpoint - protected.GET("/audio/memory-metrics", gin.WrapF(audio.HandleMemoryMetrics)) - - protected.GET("/microphone/process-metrics", func(c *gin.Context) { - if currentSession == nil || currentSession.AudioInputManager == nil { - c.JSON(200, gin.H{ - "cpu_percent": 0.0, - "memory_percent": 0.0, - "memory_rss": 0, - "memory_vms": 0, - "running": false, - }) - return - } - - // Get the supervisor from the audio input manager - supervisor := currentSession.AudioInputManager.GetSupervisor() - if supervisor == nil { - c.JSON(200, gin.H{ - "cpu_percent": 0.0, - "memory_percent": 0.0, - "memory_rss": 0, - "memory_vms": 0, - "running": false, - }) - return - } - - metrics := supervisor.GetProcessMetrics() - if metrics == nil { - c.JSON(200, gin.H{ - "cpu_percent": 0.0, - "memory_percent": 0.0, - "memory_rss": 0, - "memory_vms": 0, - "running": false, - }) - return - } - - c.JSON(200, gin.H{ - "cpu_percent": metrics.CPUPercent, - "memory_percent": metrics.MemoryPercent, - "memory_rss": metrics.MemoryRSS, - "memory_vms": metrics.MemoryVMS, - "running": true, - }) - }) - // System memory information endpoint protected.GET("/system/memory", func(c *gin.Context) { processMonitor := audio.GetProcessMonitor() @@ -712,11 +598,7 @@ func handleWebRTCSignalWsMessages( return } - // set the timer for the ping duration - timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { - metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v) - metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v) - })) + // Metrics collection disabled l.Trace().Msg("sending ping frame") err := wsCon.Ping(runCtx) @@ -727,13 +609,9 @@ func handleWebRTCSignalWsMessages( return } - // dont use `defer` here because we want to observe the duration of the ping - duration := timer.ObserveDuration() + // Metrics collection disabled - metricConnectionTotalPingSentCount.WithLabelValues(sourceType, source).Inc() - metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() - - l.Trace().Str("duration", duration.String()).Msg("received pong frame") + l.Trace().Msg("received pong frame") } }() @@ -779,8 +657,7 @@ func handleWebRTCSignalWsMessages( return err } - metricConnectionTotalPingReceivedCount.WithLabelValues(sourceType, source).Inc() - metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() + // Metrics collection disabled continue } @@ -804,8 +681,7 @@ func handleWebRTCSignalWsMessages( l.Info().Str("oidcGoogle", req.OidcGoogle).Msg("new session request with OIDC Google") } - metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc() - metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime() + // Metrics collection disabled err = handleSessionRequest(runCtx, wsCon, req, isCloudConnection, source, &l) if err != nil { l.Warn().Str("error", err.Error()).Msg("error starting new session")