Cleanup: reduce PR complexity

This commit is contained in:
Alex P 2025-09-03 22:35:52 +00:00
parent 2568660149
commit a3702dadd9
39 changed files with 72 additions and 6607 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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

View File

@ -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()
}

View File

@ -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
)

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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

View File

@ -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()
}
})
}

View File

@ -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)

View File

@ -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()
}
})
}

View File

@ -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()
}
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
})
}

View File

@ -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

View File

@ -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)
}
})
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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,
}
}

View File

@ -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()
}
})
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -360,7 +360,7 @@ export default function Actionbar({
checkIfStateChanged(open);
return (
<div className="mx-auto">
<AudioControlPopover microphone={microphone} open={open} />
<AudioControlPopover microphone={microphone} />
</div>
);
}}

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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>
</>
);
}

View File

@ -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 {

View File

@ -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,

View File

@ -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 };
};

View File

@ -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
View File

@ -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")