mirror of https://github.com/jetkvm/kvm.git
Cleanup: reduce PR complexity
This commit is contained in:
parent
2568660149
commit
a3702dadd9
24
cloud.go
24
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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -360,7 +360,7 @@ export default function Actionbar({
|
|||
checkIfStateChanged(open);
|
||||
return (
|
||||
<div className="mx-auto">
|
||||
<AudioControlPopover microphone={microphone} open={open} />
|
||||
<AudioControlPopover microphone={microphone} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -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 (
|
||||
<div className={cx(
|
||||
"rounded-md p-2 text-xs",
|
||||
variantStyles[variant],
|
||||
className
|
||||
)}>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<span>Sample Rate: {config.SampleRate}Hz</span>
|
||||
<span>Channels: {config.Channels}</span>
|
||||
<span>Bitrate: {config.Bitrate}kbps</span>
|
||||
<span>Frame: {config.FrameSize}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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<AudioLevelMeterProps> = ({
|
||||
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 (
|
||||
<div className={clsx('space-y-1', className)}>
|
||||
{showLabel && (
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-slate-500 dark:text-slate-400">
|
||||
Microphone Level
|
||||
</span>
|
||||
<span className={clsx(
|
||||
'font-mono',
|
||||
isActive ? getTextColor(level) : 'text-slate-400 dark:text-slate-500'
|
||||
)}>
|
||||
{isActive ? `${Math.round(level)}%` : 'No Signal'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx(
|
||||
'w-full rounded-full bg-slate-200 dark:bg-slate-700',
|
||||
sizeClasses[size]
|
||||
)}>
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-full transition-all duration-150 ease-out',
|
||||
sizeClasses[size],
|
||||
isActive ? getLevelColor(level) : 'bg-slate-300 dark:bg-slate-600'
|
||||
)}
|
||||
style={{
|
||||
width: isActive ? `${Math.min(100, Math.max(2, level))}%` : '0%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Peak indicators */}
|
||||
<div className="flex justify-between text-xs text-slate-400 dark:text-slate-500">
|
||||
<span>0%</span>
|
||||
<span>50%</span>
|
||||
<span>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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<T, K extends keyof T>(
|
||||
stream: Map<number, T>,
|
||||
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<AudioMetrics | null>(null);
|
||||
const [fallbackMicrophoneMetrics, setFallbackMicrophoneMetrics] = useState<MicrophoneMetrics | null>(null);
|
||||
const [fallbackConnected, setFallbackConnected] = useState(false);
|
||||
|
||||
// Process metrics state (fallback for when WebSocket is not connected)
|
||||
const [fallbackAudioProcessMetrics, setFallbackAudioProcessMetrics] = useState<ProcessMetrics | null>(null);
|
||||
const [fallbackMicrophoneProcessMetrics, setFallbackMicrophoneProcessMetrics] = useState<ProcessMetrics | null>(null);
|
||||
|
||||
// Historical data for charts using Maps for better memory management
|
||||
const [audioCpuStats, setAudioCpuStats] = useState<Map<number, { cpu_percent: number | null }>>(new Map());
|
||||
const [audioMemoryStats, setAudioMemoryStats] = useState<Map<number, { memory_rss: number | null }>>(new Map());
|
||||
const [micCpuStats, setMicCpuStats] = useState<Map<number, { cpu_percent: number | null }>>(new Map());
|
||||
const [micMemoryStats, setMicMemoryStats] = useState<Map<number, { memory_rss: number | null }>>(new Map());
|
||||
|
||||
// Configuration state (these don't change frequently, so we can load them once)
|
||||
const [config, setConfig] = useState<AudioConfig | null>(null);
|
||||
const [microphoneConfig, setMicrophoneConfig] = useState<AudioConfig | null>(null);
|
||||
const [lastUpdate, setLastUpdate] = useState<Date>(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 (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<MdGraphicEq className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
||||
Audio Metrics
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cx(
|
||||
"h-2 w-2 rounded-full",
|
||||
isConnected ? "bg-green-500" : "bg-red-500"
|
||||
)} />
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{isConnected ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Configuration */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{config && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LuSettings className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Audio Output Config
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Quality:</span>
|
||||
<span className={cx("font-medium", getQualityColor(config.Quality))}>
|
||||
{getQualityLabels()[config.Quality]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Bitrate:</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{config.Bitrate}kbps
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Sample Rate:</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{config.SampleRate}Hz
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Channels:</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{config.Channels}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{microphoneConfig && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<MdMic className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Audio Input Config
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Quality:</span>
|
||||
<span className={cx("font-medium", getQualityColor(microphoneConfig.Quality))}>
|
||||
{getQualityLabels()[microphoneConfig.Quality]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Bitrate:</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{microphoneConfig.Bitrate}kbps
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Sample Rate:</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{microphoneConfig.SampleRate}Hz
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-500 dark:text-slate-400">Channels:</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{microphoneConfig.Channels}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Subprocess Resource Usage - Histogram View */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Audio Output Subprocess */}
|
||||
{audioProcessMetrics && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<LuCpu className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Audio Output Process
|
||||
</span>
|
||||
<div className={cx(
|
||||
"h-2 w-2 rounded-full ml-auto",
|
||||
audioProcessMetrics.running ? "bg-green-500" : "bg-red-500"
|
||||
)} />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-900 dark:text-slate-100 mb-2">CPU Usage</h4>
|
||||
<div className="h-24">
|
||||
<StatChart
|
||||
data={createChartArray(audioCpuStats, 'cpu_percent')}
|
||||
unit="%"
|
||||
domain={[0, 100]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-900 dark:text-slate-100 mb-2">Memory Usage</h4>
|
||||
<div className="h-24">
|
||||
<StatChart
|
||||
data={createChartArray(audioMemoryStats, 'memory_rss').map(item => ({
|
||||
date: item.date,
|
||||
stat: item.stat ? item.stat / (1024 * 1024) : null // Convert bytes to MB
|
||||
}))}
|
||||
unit="MB"
|
||||
domain={[0, systemMemoryMB]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="text-center p-2 bg-slate-50 dark:bg-slate-800 rounded">
|
||||
<div className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{formatPercentage(audioProcessMetrics.cpu_percent)}
|
||||
</div>
|
||||
<div className="text-slate-500 dark:text-slate-400">CPU</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-slate-50 dark:bg-slate-800 rounded">
|
||||
<div className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{formatMemoryMB(audioProcessMetrics.memory_rss)}
|
||||
</div>
|
||||
<div className="text-slate-500 dark:text-slate-400">Memory</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Microphone Input Subprocess */}
|
||||
{microphoneProcessMetrics && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<LuMemoryStick className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Microphone Input Process
|
||||
</span>
|
||||
<div className={cx(
|
||||
"h-2 w-2 rounded-full ml-auto",
|
||||
microphoneProcessMetrics.running ? "bg-green-500" : "bg-red-500"
|
||||
)} />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-900 dark:text-slate-100 mb-2">CPU Usage</h4>
|
||||
<div className="h-24">
|
||||
<StatChart
|
||||
data={createChartArray(micCpuStats, 'cpu_percent')}
|
||||
unit="%"
|
||||
domain={[0, 100]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-900 dark:text-slate-100 mb-2">Memory Usage</h4>
|
||||
<div className="h-24">
|
||||
<StatChart
|
||||
data={createChartArray(micMemoryStats, 'memory_rss').map(item => ({
|
||||
date: item.date,
|
||||
stat: item.stat ? item.stat / (1024 * 1024) : null // Convert bytes to MB
|
||||
}))}
|
||||
unit="MB"
|
||||
domain={[0, systemMemoryMB]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="text-center p-2 bg-slate-50 dark:bg-slate-800 rounded">
|
||||
<div className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{formatPercentage(microphoneProcessMetrics.cpu_percent)}
|
||||
</div>
|
||||
<div className="text-slate-500 dark:text-slate-400">CPU</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-slate-50 dark:bg-slate-800 rounded">
|
||||
<div className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{formatMemoryMB(microphoneProcessMetrics.memory_rss)}
|
||||
</div>
|
||||
<div className="text-slate-500 dark:text-slate-400">Memory</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Performance Metrics */}
|
||||
{metrics && (
|
||||
<div className="space-y-3">
|
||||
{/* Audio Output Frames */}
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LuActivity className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Audio Output
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{formatNumber(metrics.frames_received)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Frames Received
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={cx(
|
||||
"text-2xl font-bold",
|
||||
metrics.frames_dropped > 0
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
)}>
|
||||
{formatNumber(metrics.frames_dropped)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Frames Dropped
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Drop Rate */}
|
||||
<div className="mt-3 rounded-md bg-slate-50 p-2 dark:bg-slate-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Drop Rate
|
||||
</span>
|
||||
<span className={cx(
|
||||
"font-bold",
|
||||
getDropRate() > 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)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 h-2 w-full rounded-full bg-slate-200 dark:bg-slate-600">
|
||||
<div
|
||||
className={cx(
|
||||
"h-2 rounded-full transition-all duration-300",
|
||||
getDropRate() > 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)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Microphone Input Metrics */}
|
||||
{microphoneMetrics && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<MdMic className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Microphone Input
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-600 dark:text-orange-400">
|
||||
{formatNumber(microphoneMetrics.frames_sent)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Frames Sent
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={cx(
|
||||
"text-2xl font-bold",
|
||||
microphoneMetrics.frames_dropped > 0
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
)}>
|
||||
{formatNumber(microphoneMetrics.frames_dropped)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Frames Dropped
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Microphone Drop Rate */}
|
||||
<div className="mt-3 rounded-md bg-slate-50 p-2 dark:bg-slate-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Drop Rate
|
||||
</span>
|
||||
<span className={cx(
|
||||
"font-bold",
|
||||
(microphoneMetrics.frames_sent > 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"}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 h-2 w-full rounded-full bg-slate-200 dark:bg-slate-600">
|
||||
<div
|
||||
className={cx(
|
||||
"h-2 rounded-full transition-all duration-300",
|
||||
(microphoneMetrics.frames_sent > 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)}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Microphone Audio Level */}
|
||||
{isMicrophoneActive && (
|
||||
<div className="mt-3 rounded-md bg-slate-50 p-2 dark:bg-slate-700">
|
||||
<AudioLevelMeter
|
||||
level={audioLevel}
|
||||
isActive={isMicrophoneActive && !isMicrophoneMuted && isAnalyzing}
|
||||
size="sm"
|
||||
showLabel={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Microphone Connection Health */}
|
||||
<div className="mt-3 rounded-md bg-slate-50 p-2 dark:bg-slate-700">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<MdSignalWifi4Bar className="h-3 w-3 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||
Connection Health
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Connection Drops:
|
||||
</span>
|
||||
<span className={cx(
|
||||
"text-xs font-medium",
|
||||
microphoneMetrics.connection_drops > 0
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
)}>
|
||||
{formatNumber(microphoneMetrics.connection_drops)}
|
||||
</span>
|
||||
</div>
|
||||
{microphoneMetrics.average_latency && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Avg Latency:
|
||||
</span>
|
||||
<span className="text-xs font-medium text-slate-900 dark:text-slate-100">
|
||||
{microphoneMetrics.average_latency}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data Transfer */}
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LuHardDrive className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Data Transfer
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{formatBytes(metrics.bytes_processed)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Total Processed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection Health */}
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<MdSignalWifi4Bar className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Connection Health
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Connection Drops:
|
||||
</span>
|
||||
<span className={cx(
|
||||
"font-medium",
|
||||
metrics.connection_drops > 0
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
)}>
|
||||
{formatNumber(metrics.connection_drops)}
|
||||
</span>
|
||||
</div>
|
||||
{metrics.average_latency && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Avg Latency:
|
||||
</span>
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
{metrics.average_latency}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Last Update */}
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
<LuClock className="h-3 w-3" />
|
||||
<span>Last updated: {lastUpdate.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
|
||||
{/* No Data State */}
|
||||
{!metrics && (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<MdError className="h-12 w-12 text-slate-400 dark:text-slate-600" />
|
||||
<h3 className="mt-2 text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||
No Audio Data
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
Audio metrics will appear when audio streaming is active.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<div className={cx(
|
||||
"text-center p-2 bg-slate-50 dark:bg-slate-800 rounded",
|
||||
className
|
||||
)}>
|
||||
<div className={cx(
|
||||
"font-medium",
|
||||
hasIssues
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
)}>
|
||||
{hasIssues ? "Issues" : "Good"}
|
||||
</div>
|
||||
<div className="text-slate-500 dark:text-slate-400">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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<AudioConfig | null>(null);
|
||||
const [currentMicrophoneConfig, setCurrentMicrophoneConfig] = useState<AudioConfig | null>(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
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audio Level Meter */}
|
||||
{/* Audio Level Display */}
|
||||
{isMicrophoneActive && (
|
||||
<div className="rounded-lg bg-slate-50 p-3 dark:bg-slate-700">
|
||||
<AudioLevelMeter
|
||||
level={audioLevel}
|
||||
isActive={isMicrophoneActive && !isMicrophoneMuted && isAnalyzing}
|
||||
size="md"
|
||||
showLabel={true}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
Audio Level: {Math.round(audioLevel * 100)}%
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{isMicrophoneMuted ? 'Muted' : isAnalyzing ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
{/* Debug information */}
|
||||
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
|
@ -514,10 +503,11 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
|
|||
</div>
|
||||
|
||||
{currentMicrophoneConfig && (
|
||||
<AudioConfigDisplay
|
||||
config={currentMicrophoneConfig}
|
||||
variant="success"
|
||||
/>
|
||||
<div className="text-xs text-slate-600 dark:text-slate-400 mt-2">
|
||||
Quality: {currentMicrophoneConfig.Quality} |
|
||||
Bitrate: {currentMicrophoneConfig.Bitrate}kbps |
|
||||
Sample Rate: {currentMicrophoneConfig.SampleRate}Hz
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
@ -551,59 +541,32 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
|
|||
</div>
|
||||
|
||||
{currentConfig && (
|
||||
<AudioConfigDisplay
|
||||
config={currentConfig}
|
||||
variant="default"
|
||||
/>
|
||||
<div className="text-xs text-slate-600 dark:text-slate-400 mt-2">
|
||||
Quality: {currentConfig.Quality} |
|
||||
Bitrate: {currentConfig.Bitrate}kbps |
|
||||
Sample Rate: {currentConfig.SampleRate}Hz
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Status Summary */}
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-600">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LuActivity className="h-4 w-4 text-slate-600 dark:text-slate-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Quick Status
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{metrics ? (
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<AudioStatusIndicator
|
||||
metrics={metrics}
|
||||
label="Audio Output"
|
||||
/>
|
||||
|
||||
{micMetrics && (
|
||||
<AudioStatusIndicator
|
||||
metrics={micMetrics}
|
||||
label="Microphone"
|
||||
/>
|
||||
)}
|
||||
{/* Audio Level Display */}
|
||||
{isMicrophoneActive && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-600">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LuActivity className="h-4 w-4 text-slate-600 dark:text-slate-400" />
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||||
Microphone Level
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
<div className="text-center py-2">
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400">
|
||||
No data available
|
||||
<div className="text-sm text-slate-700 dark:text-slate-300">
|
||||
Level: {Math.round(audioLevel * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Audio Metrics Dashboard Button */}
|
||||
<div className="pt-2 border-t border-slate-200 dark:border-slate-600">
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
toggleSidebarView("audio-metrics");
|
||||
}}
|
||||
className="flex items-center gap-2 rounded-md border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600 transition-colors"
|
||||
>
|
||||
<LuSignal className="h-4 w-4 text-blue-500" />
|
||||
<span>View Full Audio Metrics</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<SidebarHeader title="Audio Metrics" setSidebarView={setSidebarView} />
|
||||
<div className="h-full overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
|
||||
<AudioMetricsDashboard />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -40,7 +40,7 @@ const appendStatToMap = <T extends { timestamp: number }>(
|
|||
};
|
||||
|
||||
// Constants and types
|
||||
export type AvailableSidebarViews = "connection-stats" | "audio-metrics";
|
||||
export type AvailableSidebarViews = "connection-stats";
|
||||
export type AvailableTerminalTypes = "kvm" | "serial" | "none";
|
||||
|
||||
export interface User {
|
||||
|
|
|
@ -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<boolean | null>(null);
|
||||
const [audioMetrics, setAudioMetrics] = useState<AudioMetricsData | null>(null);
|
||||
const [microphoneState, setMicrophoneState] = useState<MicrophoneStateData | null>(null);
|
||||
const [microphoneMetrics, setMicrophoneMetricsData] = useState<MicrophoneMetricsData | null>(null);
|
||||
const [audioProcessMetrics, setAudioProcessMetrics] = useState<ProcessMetricsData | null>(null);
|
||||
const [microphoneProcessMetrics, setMicrophoneProcessMetrics] = useState<ProcessMetricsData | null>(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,
|
||||
|
|
|
@ -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<AudioContext | null>(null);
|
||||
const analyserRef = useRef<AnalyserNode | null>(null);
|
||||
const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
const lastUpdateTimeRef = useRef<number>(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 };
|
||||
};
|
|
@ -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) {
|
|||
<ConnectionStatsSidebar />
|
||||
</motion.div>
|
||||
)}
|
||||
{sidebarView === "audio-metrics" && (
|
||||
<motion.div
|
||||
className="absolute inset-0"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
|
||||
<AudioMetricsSidebar />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
|
136
web.go
136
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")
|
||||
|
|
Loading…
Reference in New Issue