Compare commits

..

No commits in common. "fff2d2b7910e9916dfb03ee8d63e6b4f01dfc86f" and "7ec583ed6a844b9f2c72ff75ff3b42388d6a2548" have entirely different histories.

38 changed files with 471 additions and 5864 deletions

View File

@ -107,9 +107,6 @@ if [ "$RUN_GO_TESTS" = true ]; then
msg_info "▶ Building go tests"
make build_dev_test
msg_info "▶ Cleaning up /tmp directory on remote host"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /tmp/tmp.* /tmp/device-tests.* || true"
msg_info "▶ Copying device-tests.tar.gz to remote host"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
@ -122,7 +119,7 @@ tar zxf /tmp/device-tests.tar.gz
./gotestsum --format=testdox \
--jsonfile=/tmp/device-tests.json \
--post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \
--raw-command -- sh ./run_all_tests -json
--raw-command -- ./run_all_tests -json
GOTESTSUM_EXIT_CODE=$?
if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then

View File

@ -37,25 +37,25 @@ type AdaptiveBufferConfig struct {
func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
return AdaptiveBufferConfig{
// Conservative buffer sizes for 256MB RAM constraint
MinBufferSize: GetConfig().AdaptiveMinBufferSize,
MaxBufferSize: GetConfig().AdaptiveMaxBufferSize,
DefaultBufferSize: GetConfig().AdaptiveDefaultBufferSize,
MinBufferSize: 3, // Minimum 3 frames (slightly higher for stability)
MaxBufferSize: 20, // Maximum 20 frames (increased for high load scenarios)
DefaultBufferSize: 6, // Default 6 frames (increased for better stability)
// CPU thresholds optimized for single-core ARM Cortex A7 under load
LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU
HighCPUThreshold: GetConfig().HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive)
LowCPUThreshold: 20.0, // Below 20% CPU
HighCPUThreshold: 60.0, // Above 60% CPU (lowered to be more responsive)
// Memory thresholds for 256MB total RAM
LowMemoryThreshold: GetConfig().LowMemoryThreshold * 100, // Below 35% memory usage
HighMemoryThreshold: GetConfig().HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response)
LowMemoryThreshold: 35.0, // Below 35% memory usage
HighMemoryThreshold: 75.0, // Above 75% memory usage (lowered for earlier response)
// Latency targets
TargetLatency: GetConfig().TargetLatency, // Target 20ms latency
MaxLatency: GetConfig().MaxLatencyTarget, // Max acceptable latency
TargetLatency: 20 * time.Millisecond, // Target 20ms latency
MaxLatency: 50 * time.Millisecond, // Max acceptable 50ms
// Adaptation settings
AdaptationInterval: GetConfig().BufferUpdateInterval, // Check every 500ms
SmoothingFactor: GetConfig().SmoothingFactor, // Moderate responsiveness
AdaptationInterval: 500 * time.Millisecond, // Check every 500ms
SmoothingFactor: 0.3, // Moderate responsiveness
}
}
@ -133,7 +133,7 @@ func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) {
atomic.StoreInt64(&abm.averageLatency, newLatency)
} else {
// Exponential moving average: 70% historical, 30% current
newAvg := int64(float64(currentAvg)*GetConfig().HistoricalWeight + float64(newLatency)*GetConfig().CurrentWeight)
newAvg := int64(float64(currentAvg)*0.7 + float64(newLatency)*0.3)
atomic.StoreInt64(&abm.averageLatency, newAvg)
}
}
@ -195,7 +195,7 @@ func (abm *AdaptiveBufferManager) adaptBufferSizes() {
latencyFactor := abm.calculateLatencyFactor(currentLatency)
// Combine factors with weights (CPU has highest priority for KVM coexistence)
combinedFactor := GetConfig().CPUMemoryWeight*cpuFactor + GetConfig().MemoryWeight*memoryFactor + GetConfig().LatencyWeight*latencyFactor
combinedFactor := 0.5*cpuFactor + 0.3*memoryFactor + 0.2*latencyFactor
// Apply adaptation with smoothing
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
@ -233,25 +233,8 @@ func (abm *AdaptiveBufferManager) adaptBufferSizes() {
UpdateAdaptiveBufferMetrics(currentInputSize, currentOutputSize, systemCPU, systemMemory, adjustmentMade)
}
// calculateCPUFactor returns adaptation factor based on CPU usage with threshold validation.
//
// Validation Rules:
// - CPU percentage must be within valid range [0.0, 100.0]
// - Uses LowCPUThreshold and HighCPUThreshold from config for decision boundaries
// - Default thresholds: Low=20.0%, High=80.0%
//
// Adaptation Logic:
// - CPU > HighCPUThreshold: Return -1.0 (decrease buffers to reduce CPU load)
// - CPU < LowCPUThreshold: Return +1.0 (increase buffers for better quality)
// - Between thresholds: Linear interpolation based on distance from midpoint
//
// Returns: Adaptation factor in range [-1.0, +1.0]
// - Negative values: Decrease buffer sizes to reduce CPU usage
// - Positive values: Increase buffer sizes for better audio quality
// - Zero: No adaptation needed
//
// The function ensures CPU-aware buffer management to balance audio quality
// with system performance, preventing CPU starvation of the KVM process.
// calculateCPUFactor returns adaptation factor based on CPU usage
// Returns: -1.0 (decrease buffers) to +1.0 (increase buffers)
func (abm *AdaptiveBufferManager) calculateCPUFactor(cpuPercent float64) float64 {
if cpuPercent > abm.config.HighCPUThreshold {
// High CPU: decrease buffers to reduce latency and give CPU to KVM
@ -265,25 +248,7 @@ func (abm *AdaptiveBufferManager) calculateCPUFactor(cpuPercent float64) float64
return (midpoint - cpuPercent) / (midpoint - abm.config.LowCPUThreshold)
}
// calculateMemoryFactor returns adaptation factor based on memory usage with threshold validation.
//
// Validation Rules:
// - Memory percentage must be within valid range [0.0, 100.0]
// - Uses LowMemoryThreshold and HighMemoryThreshold from config for decision boundaries
// - Default thresholds: Low=30.0%, High=85.0%
//
// Adaptation Logic:
// - Memory > HighMemoryThreshold: Return -1.0 (decrease buffers to free memory)
// - Memory < LowMemoryThreshold: Return +1.0 (increase buffers for performance)
// - Between thresholds: Linear interpolation based on distance from midpoint
//
// Returns: Adaptation factor in range [-1.0, +1.0]
// - Negative values: Decrease buffer sizes to reduce memory usage
// - Positive values: Increase buffer sizes for better performance
// - Zero: No adaptation needed
//
// The function prevents memory exhaustion while optimizing buffer sizes
// for audio processing performance and system stability.
// calculateMemoryFactor returns adaptation factor based on memory usage
func (abm *AdaptiveBufferManager) calculateMemoryFactor(memoryPercent float64) float64 {
if memoryPercent > abm.config.HighMemoryThreshold {
// High memory: decrease buffers to free memory
@ -297,25 +262,7 @@ func (abm *AdaptiveBufferManager) calculateMemoryFactor(memoryPercent float64) f
return (midpoint - memoryPercent) / (midpoint - abm.config.LowMemoryThreshold)
}
// calculateLatencyFactor returns adaptation factor based on latency with threshold validation.
//
// Validation Rules:
// - Latency must be non-negative duration
// - Uses TargetLatency and MaxLatency from config for decision boundaries
// - Default thresholds: Target=50ms, Max=200ms
//
// Adaptation Logic:
// - Latency > MaxLatency: Return -1.0 (decrease buffers to reduce latency)
// - Latency < TargetLatency: Return +1.0 (increase buffers for quality)
// - Between thresholds: Linear interpolation based on distance from midpoint
//
// Returns: Adaptation factor in range [-1.0, +1.0]
// - Negative values: Decrease buffer sizes to reduce audio latency
// - Positive values: Increase buffer sizes for better audio quality
// - Zero: Latency is at optimal level
//
// The function balances audio latency with quality, ensuring real-time
// performance while maintaining acceptable audio processing quality.
// calculateLatencyFactor returns adaptation factor based on latency
func (abm *AdaptiveBufferManager) calculateLatencyFactor(latency time.Duration) float64 {
if latency > abm.config.MaxLatency {
// High latency: decrease buffers
@ -359,8 +306,8 @@ func (abm *AdaptiveBufferManager) GetStats() map[string]interface{} {
"input_buffer_size": abm.GetInputBufferSize(),
"output_buffer_size": abm.GetOutputBufferSize(),
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / GetConfig().PercentageMultiplier,
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / GetConfig().PercentageMultiplier,
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / 100,
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / 100,
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
"last_adaptation": lastAdaptation,
}

View File

@ -42,9 +42,9 @@ type OptimizerConfig struct {
func DefaultOptimizerConfig() OptimizerConfig {
return OptimizerConfig{
MaxOptimizationLevel: 8,
CooldownPeriod: GetConfig().CooldownPeriod,
Aggressiveness: GetConfig().OptimizerAggressiveness,
RollbackThreshold: GetConfig().RollbackThreshold,
CooldownPeriod: 30 * time.Second,
Aggressiveness: 0.7,
RollbackThreshold: 300 * time.Millisecond,
StabilityPeriod: 10 * time.Second,
}
}
@ -109,7 +109,7 @@ func (ao *AdaptiveOptimizer) handleLatencyOptimization(metrics LatencyMetrics) e
// calculateTargetOptimizationLevel determines the appropriate optimization level
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
// Base calculation on current latency vs target
latencyRatio := float64(metrics.Current) / float64(GetConfig().LatencyTarget) // 50ms target
latencyRatio := float64(metrics.Current) / float64(50*time.Millisecond) // 50ms target
// Adjust based on trend
switch metrics.Trend {
@ -125,7 +125,7 @@ func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMet
latencyRatio *= ao.config.Aggressiveness
// Convert to optimization level
targetLevel := int64(latencyRatio * GetConfig().LatencyScalingFactor) // Scale to 0-10 range
targetLevel := int64(latencyRatio * 2) // Scale to 0-10 range
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
targetLevel = int64(ao.config.MaxOptimizationLevel)
}

View File

@ -1,99 +1,3 @@
// Package audio provides a comprehensive real-time audio processing system for JetKVM.
//
// # Architecture Overview
//
// The audio package implements a multi-component architecture designed for low-latency,
// high-quality audio streaming in embedded ARM environments. The system consists of:
//
// - Audio Output Pipeline: Receives compressed audio frames, decodes via Opus, and
// outputs to ALSA-compatible audio devices
// - Audio Input Pipeline: Captures microphone input, encodes via Opus, and streams
// to connected clients
// - Adaptive Buffer Management: Dynamically adjusts buffer sizes based on system
// load and latency requirements
// - Zero-Copy Frame Pool: Minimizes memory allocations through frame reuse
// - IPC Communication: Unix domain sockets for inter-process communication
// - Process Supervision: Automatic restart and health monitoring of audio subprocesses
//
// # Key Components
//
// ## Buffer Pool System (buffer_pool.go)
// Implements a two-tier buffer pool with separate pools for audio frames and control
// messages. Uses sync.Pool for efficient memory reuse and tracks allocation statistics.
//
// ## Zero-Copy Frame Management (zero_copy.go)
// Provides reference-counted audio frames that can be shared between components
// without copying data. Includes automatic cleanup and pool-based allocation.
//
// ## Adaptive Buffering Algorithm (adaptive_buffer.go)
// Dynamically adjusts buffer sizes based on:
// - System CPU and memory usage
// - Audio latency measurements
// - Frame drop rates
// - Network conditions
//
// The algorithm uses exponential smoothing and configurable thresholds to balance
// latency and stability. Buffer sizes are adjusted in discrete steps to prevent
// oscillation.
//
// ## Latency Monitoring (latency_monitor.go)
// Tracks end-to-end audio latency using high-resolution timestamps. Implements
// adaptive optimization that adjusts system parameters when latency exceeds
// configured thresholds.
//
// ## Process Supervision (supervisor.go)
// Manages audio subprocess lifecycle with automatic restart capabilities.
// Monitors process health and implements exponential backoff for restart attempts.
//
// # Quality Levels
//
// The system supports four quality presets optimized for different use cases:
// - Low: 32kbps output, 16kbps input - minimal bandwidth, voice-optimized
// - Medium: 96kbps output, 64kbps input - balanced quality and bandwidth
// - High: 192kbps output, 128kbps input - high quality for music
// - Ultra: 320kbps output, 256kbps input - maximum quality
//
// # Configuration System
//
// All configuration is centralized in config_constants.go, allowing runtime
// tuning of performance parameters. Key configuration areas include:
// - Opus codec parameters (bitrate, complexity, VBR settings)
// - Buffer sizes and pool configurations
// - Latency thresholds and optimization parameters
// - Process monitoring and restart policies
//
// # Thread Safety
//
// All public APIs are thread-safe. Internal synchronization uses:
// - atomic operations for performance counters
// - sync.RWMutex for configuration updates
// - sync.Pool for buffer management
// - channel-based communication for IPC
//
// # Error Handling
//
// The system implements comprehensive error handling with:
// - Graceful degradation on component failures
// - Automatic retry with exponential backoff
// - Detailed error context for debugging
// - Metrics collection for monitoring
//
// # Performance Characteristics
//
// Designed for embedded ARM systems with limited resources:
// - Sub-50ms end-to-end latency under normal conditions
// - Memory usage scales with buffer configuration
// - CPU usage optimized through zero-copy operations
// - Network bandwidth adapts to quality settings
//
// # Usage Example
//
// config := GetAudioConfig()
// SetAudioQuality(AudioQualityHigh)
//
// // Audio output will automatically start when frames are received
// metrics := GetAudioMetrics()
// fmt.Printf("Latency: %v, Frames: %d\n", metrics.AverageLatency, metrics.FramesReceived)
package audio
import (
@ -143,17 +47,17 @@ type AudioMetrics struct {
var (
currentConfig = AudioConfig{
Quality: AudioQualityMedium,
Bitrate: GetConfig().AudioQualityMediumOutputBitrate,
Bitrate: 64,
SampleRate: GetConfig().SampleRate,
Channels: GetConfig().Channels,
FrameSize: GetConfig().AudioQualityMediumFrameSize,
FrameSize: 20 * time.Millisecond,
}
currentMicrophoneConfig = AudioConfig{
Quality: AudioQualityMedium,
Bitrate: GetConfig().AudioQualityMediumInputBitrate,
Bitrate: 32,
SampleRate: GetConfig().SampleRate,
Channels: 1,
FrameSize: GetConfig().AudioQualityMediumFrameSize,
FrameSize: 20 * time.Millisecond,
}
metrics AudioMetrics
)
@ -165,24 +69,24 @@ var qualityPresets = map[AudioQuality]struct {
frameSize time.Duration
}{
AudioQualityLow: {
outputBitrate: GetConfig().AudioQualityLowOutputBitrate, inputBitrate: GetConfig().AudioQualityLowInputBitrate,
sampleRate: GetConfig().AudioQualityLowSampleRate, channels: GetConfig().AudioQualityLowChannels,
frameSize: GetConfig().AudioQualityLowFrameSize,
outputBitrate: 32, inputBitrate: 16,
sampleRate: 22050, channels: 1,
frameSize: 40 * time.Millisecond,
},
AudioQualityMedium: {
outputBitrate: GetConfig().AudioQualityMediumOutputBitrate, inputBitrate: GetConfig().AudioQualityMediumInputBitrate,
sampleRate: GetConfig().AudioQualityMediumSampleRate, channels: GetConfig().AudioQualityMediumChannels,
frameSize: GetConfig().AudioQualityMediumFrameSize,
outputBitrate: 64, inputBitrate: 32,
sampleRate: 44100, channels: 2,
frameSize: 20 * time.Millisecond,
},
AudioQualityHigh: {
outputBitrate: GetConfig().AudioQualityHighOutputBitrate, inputBitrate: GetConfig().AudioQualityHighInputBitrate,
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityHighChannels,
frameSize: GetConfig().AudioQualityHighFrameSize,
outputBitrate: 128, inputBitrate: 64,
sampleRate: GetConfig().SampleRate, channels: GetConfig().Channels,
frameSize: 20 * time.Millisecond,
},
AudioQualityUltra: {
outputBitrate: GetConfig().AudioQualityUltraOutputBitrate, inputBitrate: GetConfig().AudioQualityUltraInputBitrate,
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityUltraChannels,
frameSize: GetConfig().AudioQualityUltraFrameSize,
outputBitrate: 192, inputBitrate: 96,
sampleRate: GetConfig().SampleRate, channels: GetConfig().Channels,
frameSize: 10 * time.Millisecond,
},
}
@ -210,7 +114,7 @@ func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
Bitrate: preset.inputBitrate,
SampleRate: func() int {
if quality == AudioQualityLow {
return GetConfig().AudioQualityMicLowSampleRate
return 16000
}
return preset.sampleRate
}(),

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

@ -72,7 +72,7 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
readQueue: make(chan batchReadRequest, batchSize*2),
readBufPool: &sync.Pool{
New: func() interface{} {
return make([]byte, GetConfig().AudioFramePoolSize) // Max audio frame size
return make([]byte, 1500) // Max audio frame size
},
},
}
@ -110,7 +110,7 @@ func (bap *BatchAudioProcessor) Stop() {
bap.cancel()
// Wait for processing to complete
time.Sleep(bap.batchDuration + GetConfig().BatchProcessingDelay)
time.Sleep(bap.batchDuration + 10*time.Millisecond)
bap.logger.Info().Msg("batch audio processor stopped")
}
@ -134,7 +134,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
select {
case bap.readQueue <- request:
// Successfully queued
case <-time.After(GetConfig().ShortTimeout):
case <-time.After(5 * time.Millisecond):
// Queue is full or blocked, fallback to single operation
atomic.AddInt64(&bap.stats.SingleReads, 1)
atomic.AddInt64(&bap.stats.SingleFrames, 1)
@ -145,7 +145,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
select {
case result := <-resultChan:
return result.length, result.err
case <-time.After(GetConfig().MediumTimeout):
case <-time.After(50 * time.Millisecond):
// Timeout, fallback to single operation
atomic.AddInt64(&bap.stats.SingleReads, 1)
atomic.AddInt64(&bap.stats.SingleFrames, 1)
@ -274,8 +274,7 @@ func GetBatchAudioProcessor() *BatchAudioProcessor {
// Initialize on first use
if atomic.CompareAndSwapInt32(&batchProcessorInitialized, 0, 1) {
config := GetConfig()
processor := NewBatchAudioProcessor(config.BatchProcessorFramesPerBatch, config.BatchProcessorTimeout)
processor := NewBatchAudioProcessor(4, 5*time.Millisecond) // 4 frames per batch, 5ms timeout
atomic.StorePointer(&globalBatchProcessor, unsafe.Pointer(processor))
return processor
}
@ -287,8 +286,7 @@ func GetBatchAudioProcessor() *BatchAudioProcessor {
}
// Fallback: create a new processor (should rarely happen)
config := GetConfig()
return NewBatchAudioProcessor(config.BatchProcessorFramesPerBatch, config.BatchProcessorTimeout)
return NewBatchAudioProcessor(4, 5*time.Millisecond)
}
// EnableBatchAudioProcessing enables the global batch processor

View File

@ -3,7 +3,6 @@ package audio
import (
"sync"
"sync/atomic"
"time"
)
type AudioBufferPool struct {
@ -24,7 +23,7 @@ type AudioBufferPool struct {
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
// Pre-allocate 20% of max pool size for immediate availability
preallocSize := GetConfig().PreallocPercentage
preallocSize := 20
preallocated := make([]*[]byte, 0, preallocSize)
// Pre-allocate buffers to reduce initial allocation overhead
@ -35,7 +34,7 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
return &AudioBufferPool{
bufferSize: bufferSize,
maxPoolSize: GetConfig().MaxPoolSize, // Limit pool size to prevent excessive memory usage
maxPoolSize: 100, // Limit pool size to prevent excessive memory usage
preallocated: preallocated,
preallocSize: preallocSize,
pool: sync.Pool{
@ -47,17 +46,6 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
}
func (p *AudioBufferPool) Get() []byte {
start := time.Now()
defer func() {
latency := time.Since(start)
// Record metrics for frame pool (assuming this is the main usage)
if p.bufferSize >= GetConfig().AudioFramePoolSize {
GetGranularMetricsCollector().RecordFramePoolGet(latency, atomic.LoadInt64(&p.hitCount) > 0)
} else {
GetGranularMetricsCollector().RecordControlPoolGet(latency, atomic.LoadInt64(&p.hitCount) > 0)
}
}()
// First try pre-allocated buffers for fastest access
p.mutex.Lock()
if len(p.preallocated) > 0 {
@ -88,17 +76,6 @@ func (p *AudioBufferPool) Get() []byte {
}
func (p *AudioBufferPool) Put(buf []byte) {
start := time.Now()
defer func() {
latency := time.Since(start)
// Record metrics for frame pool (assuming this is the main usage)
if p.bufferSize >= GetConfig().AudioFramePoolSize {
GetGranularMetricsCollector().RecordFramePoolPut(latency, cap(buf))
} else {
GetGranularMetricsCollector().RecordControlPoolPut(latency, cap(buf))
}
}()
if cap(buf) < p.bufferSize {
return // Buffer too small, don't pool it
}
@ -134,8 +111,8 @@ func (p *AudioBufferPool) Put(buf []byte) {
}
var (
audioFramePool = NewAudioBufferPool(GetConfig().AudioFramePoolSize)
audioControlPool = NewAudioBufferPool(GetConfig().OutputHeaderSize)
audioFramePool = NewAudioBufferPool(1500)
audioControlPool = NewAudioBufferPool(64)
)
func GetAudioFrameBuffer() []byte {
@ -167,7 +144,7 @@ func (p *AudioBufferPool) GetPoolStats() AudioBufferPoolDetailedStats {
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
hitRate = float64(hitCount) / float64(totalRequests) * 100
}
return AudioBufferPoolDetailedStats{

View File

@ -4,7 +4,6 @@ package audio
import (
"errors"
"fmt"
"unsafe"
)
@ -23,37 +22,18 @@ static snd_pcm_t *pcm_handle = NULL;
static snd_pcm_t *pcm_playback_handle = NULL;
static OpusEncoder *encoder = NULL;
static OpusDecoder *decoder = NULL;
// Opus encoder settings - initialized from Go configuration
static int opus_bitrate = 96000; // Will be set from GetConfig().CGOOpusBitrate
static int opus_complexity = 3; // Will be set from GetConfig().CGOOpusComplexity
static int opus_vbr = 1; // Will be set from GetConfig().CGOOpusVBR
static int opus_vbr_constraint = 1; // Will be set from GetConfig().CGOOpusVBRConstraint
static int opus_signal_type = 3; // Will be set from GetConfig().CGOOpusSignalType
static int opus_bandwidth = 1105; // Will be set from GetConfig().CGOOpusBandwidth
static int opus_dtx = 0; // Will be set from GetConfig().CGOOpusDTX
static int sample_rate = 48000; // Will be set from GetConfig().CGOSampleRate
static int channels = 2; // Will be set from GetConfig().CGOChannels
static int frame_size = 960; // Will be set from GetConfig().CGOFrameSize
static int max_packet_size = 1500; // Will be set from GetConfig().CGOMaxPacketSize
static int sleep_microseconds = 1000; // Will be set from GetConfig().CGOUsleepMicroseconds
// Function to update constants from Go configuration
void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint,
int signal_type, int bandwidth, int dtx, int sr, int ch,
int fs, int max_pkt, int sleep_us) {
opus_bitrate = bitrate;
opus_complexity = complexity;
opus_vbr = vbr;
opus_vbr_constraint = vbr_constraint;
opus_signal_type = signal_type;
opus_bandwidth = bandwidth;
opus_dtx = dtx;
sample_rate = sr;
channels = ch;
frame_size = fs;
max_packet_size = max_pkt;
sleep_microseconds = sleep_us;
}
// Optimized Opus encoder settings for ARM Cortex-A7
static int opus_bitrate = 96000; // Increased for better quality
static int opus_complexity = 3; // Reduced for ARM performance
static int opus_vbr = 1; // Variable bitrate enabled
static int opus_vbr_constraint = 1; // Constrained VBR for consistent latency
static int opus_signal_type = OPUS_SIGNAL_MUSIC; // Optimized for general audio
static int opus_bandwidth = OPUS_BANDWIDTH_FULLBAND; // Full bandwidth
static int opus_dtx = 0; // Disable DTX for real-time audio
static int sample_rate = 48000;
static int channels = 2;
static int frame_size = 960; // 20ms for 48kHz
static int max_packet_size = 1500;
// State tracking to prevent race conditions during rapid start/stop
static volatile int capture_initializing = 0;
@ -76,7 +56,7 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
if (err == -EBUSY && attempts > 0) {
// Device busy, wait and retry
usleep(sleep_microseconds); // 50ms
usleep(50000); // 50ms
continue;
}
break;
@ -245,7 +225,7 @@ int jetkvm_audio_read_encode(void *opus_buf) {
} else if (pcm_rc == -ESTRPIPE) {
// Device suspended, try to resume
while ((err = snd_pcm_resume(pcm_handle)) == -EAGAIN) {
usleep(sleep_microseconds); // Use centralized constant
usleep(1000); // 1ms
}
if (err < 0) {
err = snd_pcm_prepare(pcm_handle);
@ -359,7 +339,7 @@ int jetkvm_audio_decode_write(void *opus_buf, int opus_size) {
} else if (pcm_rc == -ESTRPIPE) {
// Device suspended, try to resume
while ((err = snd_pcm_resume(pcm_playback_handle)) == -EAGAIN) {
usleep(sleep_microseconds); // Use centralized constant
usleep(1000); // 1ms
}
if (err < 0) {
err = snd_pcm_prepare(pcm_playback_handle);
@ -377,7 +357,7 @@ int jetkvm_audio_decode_write(void *opus_buf, int opus_size) {
void jetkvm_audio_playback_close() {
// Wait for any ongoing operations to complete
while (playback_initializing) {
usleep(sleep_microseconds); // Use centralized constant
usleep(1000); // 1ms
}
// Atomic check and set to prevent double cleanup
@ -400,7 +380,7 @@ void jetkvm_audio_playback_close() {
void jetkvm_audio_close() {
// Wait for any ongoing operations to complete
while (capture_initializing) {
usleep(sleep_microseconds); // Use centralized constant
usleep(1000); // 1ms
}
capture_initialized = 0;
@ -423,62 +403,21 @@ import "C"
// Optimized Go wrappers with reduced overhead
var (
// Base error types for wrapping with context
errAudioInitFailed = errors.New("failed to init ALSA/Opus")
errBufferTooSmall = errors.New("buffer too small")
errAudioReadEncode = errors.New("audio read/encode error")
errAudioDecodeWrite = errors.New("audio decode/write error")
errAudioPlaybackInit = errors.New("failed to init ALSA playback/Opus decoder")
errEmptyBuffer = errors.New("empty buffer")
errNilBuffer = errors.New("nil buffer")
errBufferTooLarge = errors.New("buffer too large")
errInvalidBufferPtr = errors.New("invalid buffer pointer")
)
// Error creation functions with context
func newBufferTooSmallError(actual, required int) error {
return fmt.Errorf("buffer too small: got %d bytes, need at least %d bytes", actual, required)
}
func newBufferTooLargeError(actual, max int) error {
return fmt.Errorf("buffer too large: got %d bytes, maximum allowed %d bytes", actual, max)
}
func newAudioInitError(cErrorCode int) error {
return fmt.Errorf("%w: C error code %d", errAudioInitFailed, cErrorCode)
}
func newAudioPlaybackInitError(cErrorCode int) error {
return fmt.Errorf("%w: C error code %d", errAudioPlaybackInit, cErrorCode)
}
func newAudioReadEncodeError(cErrorCode int) error {
return fmt.Errorf("%w: C error code %d", errAudioReadEncode, cErrorCode)
}
func newAudioDecodeWriteError(cErrorCode int) error {
return fmt.Errorf("%w: C error code %d", errAudioDecodeWrite, cErrorCode)
}
func cgoAudioInit() error {
// Update C constants from Go configuration
config := GetConfig()
C.update_audio_constants(
C.int(config.CGOOpusBitrate),
C.int(config.CGOOpusComplexity),
C.int(config.CGOOpusVBR),
C.int(config.CGOOpusVBRConstraint),
C.int(config.CGOOpusSignalType),
C.int(config.CGOOpusBandwidth),
C.int(config.CGOOpusDTX),
C.int(config.CGOSampleRate),
C.int(config.CGOChannels),
C.int(config.CGOFrameSize),
C.int(config.CGOMaxPacketSize),
C.int(config.CGOUsleepMicroseconds),
)
result := C.jetkvm_audio_init()
if result != 0 {
return newAudioInitError(int(result))
ret := C.jetkvm_audio_init()
if ret != 0 {
return errAudioInitFailed
}
return nil
}
@ -488,14 +427,13 @@ func cgoAudioClose() {
}
func cgoAudioReadEncode(buf []byte) (int, error) {
minRequired := GetConfig().MinReadEncodeBuffer
if len(buf) < minRequired {
return 0, newBufferTooSmallError(len(buf), minRequired)
if len(buf) < 1276 {
return 0, errBufferTooSmall
}
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
if n < 0 {
return 0, newAudioReadEncodeError(int(n))
return 0, errAudioReadEncode
}
if n == 0 {
return 0, nil // No data available
@ -507,7 +445,7 @@ func cgoAudioReadEncode(buf []byte) (int, error) {
func cgoAudioPlaybackInit() error {
ret := C.jetkvm_audio_playback_init()
if ret != 0 {
return newAudioPlaybackInitError(int(ret))
return errAudioPlaybackInit
}
return nil
}
@ -523,9 +461,8 @@ func cgoAudioDecodeWrite(buf []byte) (int, error) {
if buf == nil {
return 0, errNilBuffer
}
maxAllowed := GetConfig().MaxDecodeWriteBuffer
if len(buf) > maxAllowed {
return 0, newBufferTooLargeError(len(buf), maxAllowed)
if len(buf) > 4096 {
return 0, errBufferTooLarge
}
bufPtr := unsafe.Pointer(&buf[0])
@ -541,7 +478,7 @@ func cgoAudioDecodeWrite(buf []byte) (int, error) {
n := C.jetkvm_audio_decode_write(bufPtr, C.int(len(buf)))
if n < 0 {
return 0, newAudioDecodeWriteError(int(n))
return 0, errAudioDecodeWrite
}
return int(n), nil
}

File diff suppressed because it is too large Load Diff

View File

@ -109,9 +109,6 @@ func initializeBroadcaster() {
// Start metrics broadcasting goroutine
go audioEventBroadcaster.startMetricsBroadcasting()
// Start granular metrics logging with same interval as metrics broadcasting
StartGranularMetricsLogging(GetMetricsUpdateInterval())
}
// InitializeAudioEventBroadcaster initializes the global audio event broadcaster
@ -229,7 +226,7 @@ func convertAudioMetricsToEventDataWithLatencyMs(metrics AudioMetrics) AudioMetr
FramesReceived: metrics.FramesReceived,
FramesDropped: metrics.FramesDropped,
BytesProcessed: metrics.BytesProcessed,
LastFrameTime: metrics.LastFrameTime.Format(GetConfig().EventTimeFormatString),
LastFrameTime: metrics.LastFrameTime.Format("2006-01-02T15:04:05.000Z"),
ConnectionDrops: metrics.ConnectionDrops,
AverageLatency: fmt.Sprintf("%.1fms", float64(metrics.AverageLatency.Nanoseconds())/1e6),
}
@ -241,7 +238,7 @@ func convertAudioInputMetricsToEventDataWithLatencyMs(metrics AudioInputMetrics)
FramesSent: metrics.FramesSent,
FramesDropped: metrics.FramesDropped,
BytesProcessed: metrics.BytesProcessed,
LastFrameTime: metrics.LastFrameTime.Format(GetConfig().EventTimeFormatString),
LastFrameTime: metrics.LastFrameTime.Format("2006-01-02T15:04:05.000Z"),
ConnectionDrops: metrics.ConnectionDrops,
AverageLatency: fmt.Sprintf("%.1fms", float64(metrics.AverageLatency.Nanoseconds())/1e6),
}
@ -466,7 +463,7 @@ func (aeb *AudioEventBroadcaster) sendToSubscriber(subscriber *AudioEventSubscri
return false
}
ctx, cancel := context.WithTimeout(subscriber.ctx, time.Duration(GetConfig().EventTimeoutSeconds)*time.Second)
ctx, cancel := context.WithTimeout(subscriber.ctx, 2*time.Second)
defer cancel()
err := wsjson.Write(ctx, subscriber.conn, event)

View File

@ -1,419 +0,0 @@
package audio
import (
"sort"
"sync"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// LatencyHistogram tracks latency distribution with percentile calculations
type LatencyHistogram struct {
// Atomic fields MUST be first for ARM32 alignment
sampleCount int64 // Total number of samples (atomic)
totalLatency int64 // Sum of all latencies in nanoseconds (atomic)
// Latency buckets for histogram (in nanoseconds)
buckets []int64 // Bucket boundaries
counts []int64 // Count for each bucket (atomic)
// Recent samples for percentile calculation
recentSamples []time.Duration
samplesMutex sync.RWMutex
maxSamples int
logger zerolog.Logger
}
// LatencyPercentiles holds calculated percentile values
type LatencyPercentiles struct {
P50 time.Duration `json:"p50"`
P95 time.Duration `json:"p95"`
P99 time.Duration `json:"p99"`
Min time.Duration `json:"min"`
Max time.Duration `json:"max"`
Avg time.Duration `json:"avg"`
}
// BufferPoolEfficiencyMetrics tracks detailed buffer pool performance
type BufferPoolEfficiencyMetrics struct {
// Pool utilization metrics
HitRate float64 `json:"hit_rate"`
MissRate float64 `json:"miss_rate"`
UtilizationRate float64 `json:"utilization_rate"`
FragmentationRate float64 `json:"fragmentation_rate"`
// Memory efficiency metrics
MemoryEfficiency float64 `json:"memory_efficiency"`
AllocationOverhead float64 `json:"allocation_overhead"`
ReuseEffectiveness float64 `json:"reuse_effectiveness"`
// Performance metrics
AverageGetLatency time.Duration `json:"average_get_latency"`
AveragePutLatency time.Duration `json:"average_put_latency"`
Throughput float64 `json:"throughput"` // Operations per second
}
// GranularMetricsCollector aggregates all granular metrics
type GranularMetricsCollector struct {
// Latency histograms by source
inputLatencyHist *LatencyHistogram
outputLatencyHist *LatencyHistogram
processingLatencyHist *LatencyHistogram
// Buffer pool efficiency tracking
framePoolMetrics *BufferPoolEfficiencyTracker
controlPoolMetrics *BufferPoolEfficiencyTracker
zeroCopyMetrics *BufferPoolEfficiencyTracker
mutex sync.RWMutex
logger zerolog.Logger
}
// BufferPoolEfficiencyTracker tracks detailed efficiency metrics for a buffer pool
type BufferPoolEfficiencyTracker struct {
// Atomic counters
getOperations int64 // Total get operations (atomic)
putOperations int64 // Total put operations (atomic)
getLatencySum int64 // Sum of get latencies in nanoseconds (atomic)
putLatencySum int64 // Sum of put latencies in nanoseconds (atomic)
allocationBytes int64 // Total bytes allocated (atomic)
reuseCount int64 // Number of successful reuses (atomic)
// Recent operation times for throughput calculation
recentOps []time.Time
opsMutex sync.RWMutex
poolName string
logger zerolog.Logger
}
// NewLatencyHistogram creates a new latency histogram with predefined buckets
func NewLatencyHistogram(maxSamples int, logger zerolog.Logger) *LatencyHistogram {
// Define latency buckets: 1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2s+
buckets := []int64{
int64(1 * time.Millisecond),
int64(5 * time.Millisecond),
int64(10 * time.Millisecond),
int64(25 * time.Millisecond),
int64(50 * time.Millisecond),
int64(100 * time.Millisecond),
int64(250 * time.Millisecond),
int64(500 * time.Millisecond),
int64(1 * time.Second),
int64(2 * time.Second),
}
return &LatencyHistogram{
buckets: buckets,
counts: make([]int64, len(buckets)+1), // +1 for overflow bucket
recentSamples: make([]time.Duration, 0, maxSamples),
maxSamples: maxSamples,
logger: logger,
}
}
// RecordLatency adds a latency measurement to the histogram
func (lh *LatencyHistogram) RecordLatency(latency time.Duration) {
latencyNs := latency.Nanoseconds()
atomic.AddInt64(&lh.sampleCount, 1)
atomic.AddInt64(&lh.totalLatency, latencyNs)
// Find appropriate bucket
bucketIndex := len(lh.buckets) // Default to overflow bucket
for i, boundary := range lh.buckets {
if latencyNs <= boundary {
bucketIndex = i
break
}
}
atomic.AddInt64(&lh.counts[bucketIndex], 1)
// Store recent sample for percentile calculation
lh.samplesMutex.Lock()
if len(lh.recentSamples) >= lh.maxSamples {
// Remove oldest sample
lh.recentSamples = lh.recentSamples[1:]
}
lh.recentSamples = append(lh.recentSamples, latency)
lh.samplesMutex.Unlock()
}
// GetPercentiles calculates latency percentiles from recent samples
func (lh *LatencyHistogram) GetPercentiles() LatencyPercentiles {
lh.samplesMutex.RLock()
samples := make([]time.Duration, len(lh.recentSamples))
copy(samples, lh.recentSamples)
lh.samplesMutex.RUnlock()
if len(samples) == 0 {
return LatencyPercentiles{}
}
// Sort samples for percentile calculation
sort.Slice(samples, func(i, j int) bool {
return samples[i] < samples[j]
})
n := len(samples)
totalLatency := atomic.LoadInt64(&lh.totalLatency)
sampleCount := atomic.LoadInt64(&lh.sampleCount)
var avg time.Duration
if sampleCount > 0 {
avg = time.Duration(totalLatency / sampleCount)
}
return LatencyPercentiles{
P50: samples[n*50/100],
P95: samples[n*95/100],
P99: samples[n*99/100],
Min: samples[0],
Max: samples[n-1],
Avg: avg,
}
}
// NewBufferPoolEfficiencyTracker creates a new efficiency tracker
func NewBufferPoolEfficiencyTracker(poolName string, logger zerolog.Logger) *BufferPoolEfficiencyTracker {
return &BufferPoolEfficiencyTracker{
recentOps: make([]time.Time, 0, 1000), // Track last 1000 operations
poolName: poolName,
logger: logger,
}
}
// RecordGetOperation records a buffer get operation with its latency
func (bpet *BufferPoolEfficiencyTracker) RecordGetOperation(latency time.Duration, wasHit bool) {
atomic.AddInt64(&bpet.getOperations, 1)
atomic.AddInt64(&bpet.getLatencySum, latency.Nanoseconds())
if wasHit {
atomic.AddInt64(&bpet.reuseCount, 1)
}
// Record operation time for throughput calculation
bpet.opsMutex.Lock()
now := time.Now()
if len(bpet.recentOps) >= 1000 {
bpet.recentOps = bpet.recentOps[1:]
}
bpet.recentOps = append(bpet.recentOps, now)
bpet.opsMutex.Unlock()
}
// RecordPutOperation records a buffer put operation with its latency
func (bpet *BufferPoolEfficiencyTracker) RecordPutOperation(latency time.Duration, bufferSize int) {
atomic.AddInt64(&bpet.putOperations, 1)
atomic.AddInt64(&bpet.putLatencySum, latency.Nanoseconds())
atomic.AddInt64(&bpet.allocationBytes, int64(bufferSize))
}
// GetEfficiencyMetrics calculates current efficiency metrics
func (bpet *BufferPoolEfficiencyTracker) GetEfficiencyMetrics() BufferPoolEfficiencyMetrics {
getOps := atomic.LoadInt64(&bpet.getOperations)
putOps := atomic.LoadInt64(&bpet.putOperations)
reuseCount := atomic.LoadInt64(&bpet.reuseCount)
getLatencySum := atomic.LoadInt64(&bpet.getLatencySum)
putLatencySum := atomic.LoadInt64(&bpet.putLatencySum)
allocationBytes := atomic.LoadInt64(&bpet.allocationBytes)
var hitRate, missRate, avgGetLatency, avgPutLatency float64
var throughput float64
if getOps > 0 {
hitRate = float64(reuseCount) / float64(getOps) * 100
missRate = 100 - hitRate
avgGetLatency = float64(getLatencySum) / float64(getOps)
}
if putOps > 0 {
avgPutLatency = float64(putLatencySum) / float64(putOps)
}
// Calculate throughput from recent operations
bpet.opsMutex.RLock()
if len(bpet.recentOps) > 1 {
timeSpan := bpet.recentOps[len(bpet.recentOps)-1].Sub(bpet.recentOps[0])
if timeSpan > 0 {
throughput = float64(len(bpet.recentOps)) / timeSpan.Seconds()
}
}
bpet.opsMutex.RUnlock()
// Calculate efficiency metrics
utilizationRate := hitRate // Simplified: hit rate as utilization
memoryEfficiency := hitRate // Simplified: reuse rate as memory efficiency
reuseEffectiveness := hitRate
// Calculate fragmentation (simplified as inverse of hit rate)
fragmentationRate := missRate
// Calculate allocation overhead (simplified)
allocationOverhead := float64(0)
if getOps > 0 && allocationBytes > 0 {
allocationOverhead = float64(allocationBytes) / float64(getOps)
}
return BufferPoolEfficiencyMetrics{
HitRate: hitRate,
MissRate: missRate,
UtilizationRate: utilizationRate,
FragmentationRate: fragmentationRate,
MemoryEfficiency: memoryEfficiency,
AllocationOverhead: allocationOverhead,
ReuseEffectiveness: reuseEffectiveness,
AverageGetLatency: time.Duration(avgGetLatency),
AveragePutLatency: time.Duration(avgPutLatency),
Throughput: throughput,
}
}
// NewGranularMetricsCollector creates a new granular metrics collector
func NewGranularMetricsCollector(logger zerolog.Logger) *GranularMetricsCollector {
maxSamples := GetConfig().LatencyHistorySize
return &GranularMetricsCollector{
inputLatencyHist: NewLatencyHistogram(maxSamples, logger.With().Str("histogram", "input").Logger()),
outputLatencyHist: NewLatencyHistogram(maxSamples, logger.With().Str("histogram", "output").Logger()),
processingLatencyHist: NewLatencyHistogram(maxSamples, logger.With().Str("histogram", "processing").Logger()),
framePoolMetrics: NewBufferPoolEfficiencyTracker("frame_pool", logger.With().Str("pool", "frame").Logger()),
controlPoolMetrics: NewBufferPoolEfficiencyTracker("control_pool", logger.With().Str("pool", "control").Logger()),
zeroCopyMetrics: NewBufferPoolEfficiencyTracker("zero_copy_pool", logger.With().Str("pool", "zero_copy").Logger()),
logger: logger,
}
}
// RecordInputLatency records latency for input operations
func (gmc *GranularMetricsCollector) RecordInputLatency(latency time.Duration) {
gmc.inputLatencyHist.RecordLatency(latency)
}
// RecordOutputLatency records latency for output operations
func (gmc *GranularMetricsCollector) RecordOutputLatency(latency time.Duration) {
gmc.outputLatencyHist.RecordLatency(latency)
}
// RecordProcessingLatency records latency for processing operations
func (gmc *GranularMetricsCollector) RecordProcessingLatency(latency time.Duration) {
gmc.processingLatencyHist.RecordLatency(latency)
}
// RecordFramePoolOperation records frame pool operations
func (gmc *GranularMetricsCollector) RecordFramePoolGet(latency time.Duration, wasHit bool) {
gmc.framePoolMetrics.RecordGetOperation(latency, wasHit)
}
func (gmc *GranularMetricsCollector) RecordFramePoolPut(latency time.Duration, bufferSize int) {
gmc.framePoolMetrics.RecordPutOperation(latency, bufferSize)
}
// RecordControlPoolOperation records control pool operations
func (gmc *GranularMetricsCollector) RecordControlPoolGet(latency time.Duration, wasHit bool) {
gmc.controlPoolMetrics.RecordGetOperation(latency, wasHit)
}
func (gmc *GranularMetricsCollector) RecordControlPoolPut(latency time.Duration, bufferSize int) {
gmc.controlPoolMetrics.RecordPutOperation(latency, bufferSize)
}
// RecordZeroCopyOperation records zero-copy pool operations
func (gmc *GranularMetricsCollector) RecordZeroCopyGet(latency time.Duration, wasHit bool) {
gmc.zeroCopyMetrics.RecordGetOperation(latency, wasHit)
}
func (gmc *GranularMetricsCollector) RecordZeroCopyPut(latency time.Duration, bufferSize int) {
gmc.zeroCopyMetrics.RecordPutOperation(latency, bufferSize)
}
// GetLatencyPercentiles returns percentiles for all latency types
func (gmc *GranularMetricsCollector) GetLatencyPercentiles() map[string]LatencyPercentiles {
gmc.mutex.RLock()
defer gmc.mutex.RUnlock()
return map[string]LatencyPercentiles{
"input": gmc.inputLatencyHist.GetPercentiles(),
"output": gmc.outputLatencyHist.GetPercentiles(),
"processing": gmc.processingLatencyHist.GetPercentiles(),
}
}
// GetBufferPoolEfficiency returns efficiency metrics for all buffer pools
func (gmc *GranularMetricsCollector) GetBufferPoolEfficiency() map[string]BufferPoolEfficiencyMetrics {
gmc.mutex.RLock()
defer gmc.mutex.RUnlock()
return map[string]BufferPoolEfficiencyMetrics{
"frame_pool": gmc.framePoolMetrics.GetEfficiencyMetrics(),
"control_pool": gmc.controlPoolMetrics.GetEfficiencyMetrics(),
"zero_copy_pool": gmc.zeroCopyMetrics.GetEfficiencyMetrics(),
}
}
// LogGranularMetrics logs comprehensive granular metrics
func (gmc *GranularMetricsCollector) LogGranularMetrics() {
latencyPercentiles := gmc.GetLatencyPercentiles()
bufferEfficiency := gmc.GetBufferPoolEfficiency()
// Log latency percentiles
for source, percentiles := range latencyPercentiles {
gmc.logger.Info().
Str("source", source).
Dur("p50", percentiles.P50).
Dur("p95", percentiles.P95).
Dur("p99", percentiles.P99).
Dur("min", percentiles.Min).
Dur("max", percentiles.Max).
Dur("avg", percentiles.Avg).
Msg("Latency percentiles")
}
// Log buffer pool efficiency
for poolName, efficiency := range bufferEfficiency {
gmc.logger.Info().
Str("pool", poolName).
Float64("hit_rate", efficiency.HitRate).
Float64("miss_rate", efficiency.MissRate).
Float64("utilization_rate", efficiency.UtilizationRate).
Float64("memory_efficiency", efficiency.MemoryEfficiency).
Dur("avg_get_latency", efficiency.AverageGetLatency).
Dur("avg_put_latency", efficiency.AveragePutLatency).
Float64("throughput", efficiency.Throughput).
Msg("Buffer pool efficiency metrics")
}
}
// Global granular metrics collector instance
var (
granularMetricsCollector *GranularMetricsCollector
granularMetricsOnce sync.Once
)
// GetGranularMetricsCollector returns the global granular metrics collector
func GetGranularMetricsCollector() *GranularMetricsCollector {
granularMetricsOnce.Do(func() {
logger := logging.GetDefaultLogger().With().Str("component", "granular-metrics").Logger()
granularMetricsCollector = NewGranularMetricsCollector(logger)
})
return granularMetricsCollector
}
// StartGranularMetricsLogging starts periodic granular metrics logging
func StartGranularMetricsLogging(interval time.Duration) {
collector := GetGranularMetricsCollector()
logger := collector.logger
logger.Info().Dur("interval", interval).Msg("Starting granular metrics logging")
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
collector.LogGranularMetrics()
}
}()
}

View File

@ -80,7 +80,7 @@ func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error {
processingTime := time.Since(startTime)
// Log high latency warnings
if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
if processingTime > 10*time.Millisecond {
aim.logger.Warn().
Dur("latency_ms", processingTime).
Msg("High audio processing latency detected")
@ -116,7 +116,7 @@ func (aim *AudioInputManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame)
processingTime := time.Since(startTime)
// Log high latency warnings
if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
if processingTime > 10*time.Millisecond {
aim.logger.Warn().
Dur("latency_ms", processingTime).
Msg("High audio processing latency detected")

View File

@ -16,19 +16,14 @@ import (
"github.com/jetkvm/kvm/internal/logging"
)
var (
inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input)
inputSocketName = "audio_input.sock"
writeTimeout = GetConfig().WriteTimeout // Non-blocking write timeout
)
const (
headerSize = 17 // Fixed header size: 4+1+4+8 bytes - matches GetConfig().HeaderSize
)
var (
maxFrameSize = GetConfig().MaxFrameSize // Maximum Opus frame size
messagePoolSize = GetConfig().MessagePoolSize // Pre-allocated message pool size
inputMagicNumber uint32 = 0x4A4B4D49 // "JKMI" (JetKVM Microphone Input)
inputSocketName = "audio_input.sock"
maxFrameSize = 4096 // Maximum Opus frame size
writeTimeout = 15 * time.Millisecond // Non-blocking write timeout (increased for high load)
maxDroppedFrames = 100 // Maximum consecutive dropped frames before reconnect
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
messagePoolSize = 256 // Pre-allocated message pool size
)
// InputMessageType represents the type of IPC message
@ -84,9 +79,9 @@ var messagePoolInitOnce sync.Once
func initializeMessagePool() {
messagePoolInitOnce.Do(func() {
// Pre-allocate 30% of pool size for immediate availability
preallocSize := messagePoolSize * GetConfig().InputPreallocPercentage / 100
preallocSize := messagePoolSize * 30 / 100
globalMessagePool.preallocSize = preallocSize
globalMessagePool.maxPoolSize = messagePoolSize * GetConfig().PoolGrowthMultiplier // Allow growth up to 2x
globalMessagePool.maxPoolSize = messagePoolSize * 2 // Allow growth up to 2x
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
// Pre-allocate messages to reduce initial allocation overhead
@ -320,34 +315,12 @@ func (ais *AudioInputServer) handleConnection(conn net.Conn) {
if ais.conn == nil {
return
}
time.Sleep(GetConfig().DefaultSleepDuration)
time.Sleep(100 * time.Millisecond)
}
}
}
// readMessage reads a message from the connection using optimized pooled buffers with validation.
//
// Validation Rules:
// - Magic number must match InputMagicNumber ("JKMI" - JetKVM Microphone Input)
// - Message length must not exceed MaxFrameSize (default: 4096 bytes)
// - Header size is fixed at 17 bytes (4+1+4+8: Magic+Type+Length+Timestamp)
// - Data length validation prevents buffer overflow attacks
//
// Message Format:
// - Magic (4 bytes): Identifies valid JetKVM audio messages
// - Type (1 byte): InputMessageType (OpusFrame, Config, Stop, Heartbeat, Ack)
// - Length (4 bytes): Data payload size in bytes
// - Timestamp (8 bytes): Message timestamp for latency tracking
// - Data (variable): Message payload up to MaxFrameSize
//
// Error Conditions:
// - Invalid magic number: Rejects non-JetKVM messages
// - Message too large: Prevents memory exhaustion
// - Connection errors: Network/socket failures
// - Incomplete reads: Partial message reception
//
// The function uses pooled buffers for efficient memory management and
// ensures all messages conform to the JetKVM audio protocol specification.
// readMessage reads a complete message from the connection
func (ais *AudioInputServer) readMessage(conn net.Conn) (*InputIPCMessage, error) {
// Get optimized message from pool
optMsg := globalMessagePool.Get()
@ -368,12 +341,12 @@ func (ais *AudioInputServer) readMessage(conn net.Conn) (*InputIPCMessage, error
// Validate magic number
if msg.Magic != inputMagicNumber {
return nil, fmt.Errorf("invalid magic number: got 0x%x, expected 0x%x", msg.Magic, inputMagicNumber)
return nil, fmt.Errorf("invalid magic number: %x", msg.Magic)
}
// Validate message length
if msg.Length > uint32(maxFrameSize) {
return nil, fmt.Errorf("message too large: got %d bytes, maximum allowed %d bytes", msg.Length, maxFrameSize)
if msg.Length > maxFrameSize {
return nil, fmt.Errorf("message too large: %d bytes", msg.Length)
}
// Read data if present using pooled buffer
@ -525,12 +498,10 @@ func (aic *AudioInputClient) Connect() error {
aic.running = true
return nil
}
// Exponential backoff starting from config
backoffStart := GetConfig().BackoffStart
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
maxDelay := GetConfig().MaxRetryDelay
if delay > maxDelay {
delay = maxDelay
// Exponential backoff starting at 50ms
delay := time.Duration(50*(1<<uint(i/3))) * time.Millisecond
if delay > 500*time.Millisecond {
delay = 500 * time.Millisecond
}
time.Sleep(delay)
}
@ -570,7 +541,7 @@ func (aic *AudioInputClient) SendFrame(frame []byte) error {
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil {
return fmt.Errorf("not connected to audio input server")
return fmt.Errorf("not connected")
}
if len(frame) == 0 {
@ -578,7 +549,7 @@ func (aic *AudioInputClient) SendFrame(frame []byte) error {
}
if len(frame) > maxFrameSize {
return fmt.Errorf("frame too large: got %d bytes, maximum allowed %d bytes", len(frame), maxFrameSize)
return fmt.Errorf("frame too large: %d bytes", len(frame))
}
msg := &InputIPCMessage{
@ -598,7 +569,7 @@ func (aic *AudioInputClient) SendFrameZeroCopy(frame *ZeroCopyAudioFrame) error
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil {
return fmt.Errorf("not connected to audio input server")
return fmt.Errorf("not connected")
}
if frame == nil || frame.Length() == 0 {
@ -606,7 +577,7 @@ func (aic *AudioInputClient) SendFrameZeroCopy(frame *ZeroCopyAudioFrame) error
}
if frame.Length() > maxFrameSize {
return fmt.Errorf("frame too large: got %d bytes, maximum allowed %d bytes", frame.Length(), maxFrameSize)
return fmt.Errorf("frame too large: %d bytes", frame.Length())
}
// Use zero-copy data directly
@ -627,7 +598,7 @@ func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil {
return fmt.Errorf("not connected to audio input server")
return fmt.Errorf("not connected")
}
// Serialize config (simple binary format)
@ -653,7 +624,7 @@ func (aic *AudioInputClient) SendHeartbeat() error {
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil {
return fmt.Errorf("not connected to audio input server")
return fmt.Errorf("not connected")
}
msg := &InputIPCMessage{
@ -740,7 +711,7 @@ func (aic *AudioInputClient) GetDropRate() float64 {
if total == 0 {
return 0.0
}
return float64(dropped) / float64(total) * GetConfig().PercentageMultiplier
return float64(dropped) / float64(total) * 100.0
}
// ResetStats resets frame statistics
@ -849,11 +820,11 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
}()
defer ais.wg.Done()
ticker := time.NewTicker(GetConfig().DefaultTickerInterval)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
// Buffer size update ticker (less frequent)
bufferUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
bufferUpdateTicker := time.NewTicker(500 * time.Millisecond)
defer bufferUpdateTicker.Stop()
for {
@ -946,7 +917,7 @@ func (mp *MessagePool) GetMessagePoolStats() MessagePoolStats {
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
hitRate = float64(hitCount) / float64(totalRequests) * 100
}
// Calculate channel pool size

View File

@ -41,13 +41,13 @@ func (aim *AudioInputIPCManager) Start() error {
}
config := InputIPCConfig{
SampleRate: GetConfig().InputIPCSampleRate,
Channels: GetConfig().InputIPCChannels,
FrameSize: GetConfig().InputIPCFrameSize,
SampleRate: 48000,
Channels: 2,
FrameSize: 960,
}
// Wait for subprocess readiness
time.Sleep(GetConfig().LongSleepDuration)
time.Sleep(200 * time.Millisecond)
err = aim.supervisor.SendConfig(config)
if err != nil {

View File

@ -64,7 +64,7 @@ func RunAudioInputServer() error {
server.Stop()
// Give some time for cleanup
time.Sleep(GetConfig().DefaultSleepDuration)
time.Sleep(100 * time.Millisecond)
logger.Info().Msg("Audio input server subprocess stopped")
return nil

View File

@ -39,7 +39,7 @@ func (ais *AudioInputSupervisor) Start() error {
defer ais.mtx.Unlock()
if ais.running {
return fmt.Errorf("audio input supervisor already running with PID %d", ais.cmd.Process.Pid)
return fmt.Errorf("audio input supervisor already running")
}
// Create context for subprocess management
@ -71,7 +71,7 @@ func (ais *AudioInputSupervisor) Start() error {
if err != nil {
ais.running = false
cancel()
return fmt.Errorf("failed to start audio input server process: %w", err)
return fmt.Errorf("failed to start audio input server: %w", err)
}
ais.logger.Info().Int("pid", cmd.Process.Pid).Msg("Audio input server subprocess started")
@ -128,7 +128,7 @@ func (ais *AudioInputSupervisor) Stop() {
select {
case <-done:
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
case <-time.After(GetConfig().InputSupervisorTimeout):
case <-time.After(5 * time.Second):
// Force kill if graceful shutdown failed
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
err := ais.cmd.Process.Kill()
@ -199,9 +199,9 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
if ais.running {
// Unexpected exit
if err != nil {
ais.logger.Error().Err(err).Int("pid", pid).Msg("Audio input server subprocess exited unexpectedly")
ais.logger.Error().Err(err).Msg("Audio input server subprocess exited unexpectedly")
} else {
ais.logger.Warn().Int("pid", pid).Msg("Audio input server subprocess exited unexpectedly")
ais.logger.Warn().Msg("Audio input server subprocess exited unexpectedly")
}
// Disconnect client
@ -213,14 +213,14 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
ais.running = false
ais.cmd = nil
ais.logger.Info().Int("pid", pid).Msg("Audio input server subprocess monitoring stopped")
ais.logger.Info().Msg("Audio input server subprocess monitoring stopped")
}
}
// connectClient attempts to connect the client to the server
func (ais *AudioInputSupervisor) connectClient() {
// Wait briefly for the server to start (reduced from 500ms)
time.Sleep(GetConfig().DefaultSleepDuration)
time.Sleep(100 * time.Millisecond)
err := ais.client.Connect()
if err != nil {

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

@ -16,14 +16,16 @@ import (
"github.com/rs/zerolog"
)
var (
outputMagicNumber uint32 = GetConfig().OutputMagicNumber // "JKOU" (JetKVM Output)
outputSocketName = "audio_output.sock"
const (
outputMagicNumber uint32 = 0x4A4B4F55 // "JKOU" (JetKVM Output)
outputSocketName = "audio_output.sock"
outputMaxFrameSize = 4096 // Maximum Opus frame size
outputWriteTimeout = 10 * time.Millisecond // Non-blocking write timeout (increased for high load)
outputMaxDroppedFrames = 50 // Maximum consecutive dropped frames
outputHeaderSize = 17 // Fixed header size: 4+1+4+8 bytes
outputMessagePoolSize = 128 // Pre-allocated message pool size
)
// Output IPC constants are now centralized in config_constants.go
// outputMaxFrameSize, outputWriteTimeout, outputMaxDroppedFrames, outputHeaderSize, outputMessagePoolSize
// OutputMessageType represents the type of IPC message
type OutputMessageType uint8
@ -46,8 +48,8 @@ type OutputIPCMessage struct {
// OutputOptimizedMessage represents a pre-allocated message for zero-allocation operations
type OutputOptimizedMessage struct {
header [17]byte // Pre-allocated header buffer (using constant value since array size must be compile-time constant)
data []byte // Reusable data buffer
header [outputHeaderSize]byte // Pre-allocated header buffer
data []byte // Reusable data buffer
}
// OutputMessagePool manages pre-allocated messages for zero-allocation IPC
@ -64,7 +66,7 @@ func NewOutputMessagePool(size int) *OutputMessagePool {
// Pre-allocate messages
for i := 0; i < size; i++ {
msg := &OutputOptimizedMessage{
data: make([]byte, GetConfig().OutputMaxFrameSize),
data: make([]byte, outputMaxFrameSize),
}
pool.pool <- msg
}
@ -80,7 +82,7 @@ func (p *OutputMessagePool) Get() *OutputOptimizedMessage {
default:
// Pool exhausted, create new message
return &OutputOptimizedMessage{
data: make([]byte, GetConfig().OutputMaxFrameSize),
data: make([]byte, outputMaxFrameSize),
}
}
}
@ -96,7 +98,7 @@ func (p *OutputMessagePool) Put(msg *OutputOptimizedMessage) {
}
// Global message pool for output IPC
var globalOutputMessagePool = NewOutputMessagePool(GetConfig().OutputMessagePoolSize)
var globalOutputMessagePool = NewOutputMessagePool(outputMessagePoolSize)
type AudioServer struct {
// Atomic fields must be first for proper alignment on ARM
@ -133,7 +135,7 @@ func NewAudioServer() (*AudioServer, error) {
}
// Initialize with adaptive buffer size (start with 500 frames)
initialBufferSize := int64(GetConfig().InitialBufferFrames)
initialBufferSize := int64(500)
// Initialize latency monitoring
latencyConfig := DefaultLatencyConfig()
@ -282,9 +284,8 @@ func (s *AudioServer) Close() error {
}
func (s *AudioServer) SendFrame(frame []byte) error {
maxFrameSize := GetConfig().OutputMaxFrameSize
if len(frame) > maxFrameSize {
return fmt.Errorf("output frame size validation failed: got %d bytes, maximum allowed %d bytes", len(frame), maxFrameSize)
if len(frame) > outputMaxFrameSize {
return fmt.Errorf("frame size %d exceeds maximum %d", len(frame), outputMaxFrameSize)
}
start := time.Now()
@ -313,7 +314,7 @@ func (s *AudioServer) SendFrame(frame []byte) error {
default:
// Channel full, drop frame to prevent blocking
atomic.AddInt64(&s.droppedFrames, 1)
return fmt.Errorf("output message channel full (capacity: %d) - frame dropped to prevent blocking", cap(s.messageChan))
return fmt.Errorf("message channel full - frame dropped")
}
}
@ -323,7 +324,7 @@ func (s *AudioServer) sendFrameToClient(frame []byte) error {
defer s.mtx.Unlock()
if s.conn == nil {
return fmt.Errorf("no audio output client connected to server")
return fmt.Errorf("no client connected")
}
start := time.Now()
@ -339,7 +340,7 @@ func (s *AudioServer) sendFrameToClient(frame []byte) error {
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(start.UnixNano()))
// Use non-blocking write with timeout
ctx, cancel := context.WithTimeout(context.Background(), GetConfig().OutputWriteTimeout)
ctx, cancel := context.WithTimeout(context.Background(), outputWriteTimeout)
defer cancel()
// Create a channel to signal write completion
@ -379,7 +380,7 @@ func (s *AudioServer) sendFrameToClient(frame []byte) error {
case <-ctx.Done():
// Timeout occurred - drop frame to prevent blocking
atomic.AddInt64(&s.droppedFrames, 1)
return fmt.Errorf("write timeout after %v - frame dropped to prevent blocking", GetConfig().OutputWriteTimeout)
return fmt.Errorf("write timeout - frame dropped")
}
}
@ -423,17 +424,15 @@ func (c *AudioClient) Connect() error {
c.running = true
return nil
}
// Exponential backoff starting from config
backoffStart := GetConfig().BackoffStart
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
maxDelay := GetConfig().MaxRetryDelay
if delay > maxDelay {
delay = maxDelay
// Exponential backoff starting at 50ms
delay := time.Duration(50*(1<<uint(i/3))) * time.Millisecond
if delay > 400*time.Millisecond {
delay = 400 * time.Millisecond
}
time.Sleep(delay)
}
return fmt.Errorf("failed to connect to audio output server at %s after %d retries", socketPath, 8)
return fmt.Errorf("failed to connect to audio output server")
}
// Disconnect disconnects from the audio output server
@ -469,7 +468,7 @@ func (c *AudioClient) ReceiveFrame() ([]byte, error) {
defer c.mtx.Unlock()
if !c.running || c.conn == nil {
return nil, fmt.Errorf("not connected to audio output server")
return nil, fmt.Errorf("not connected")
}
// Get optimized message from pool for header reading
@ -478,13 +477,13 @@ func (c *AudioClient) ReceiveFrame() ([]byte, error) {
// Read header
if _, err := io.ReadFull(c.conn, optMsg.header[:]); err != nil {
return nil, fmt.Errorf("failed to read IPC message header from audio output server: %w", err)
return nil, fmt.Errorf("failed to read header: %w", err)
}
// Parse header
magic := binary.LittleEndian.Uint32(optMsg.header[0:4])
if magic != outputMagicNumber {
return nil, fmt.Errorf("invalid magic number in IPC message: got 0x%x, expected 0x%x", magic, outputMagicNumber)
return nil, fmt.Errorf("invalid magic number: %x", magic)
}
msgType := OutputMessageType(optMsg.header[4])
@ -493,9 +492,8 @@ func (c *AudioClient) ReceiveFrame() ([]byte, error) {
}
size := binary.LittleEndian.Uint32(optMsg.header[5:9])
maxFrameSize := GetConfig().OutputMaxFrameSize
if int(size) > maxFrameSize {
return nil, fmt.Errorf("received frame size validation failed: got %d bytes, maximum allowed %d bytes", size, maxFrameSize)
if size > outputMaxFrameSize {
return nil, fmt.Errorf("frame size %d exceeds maximum %d", size, outputMaxFrameSize)
}
// Read frame data

View File

@ -81,14 +81,13 @@ const (
// DefaultLatencyConfig returns a sensible default configuration
func DefaultLatencyConfig() LatencyConfig {
config := GetConfig()
return LatencyConfig{
TargetLatency: config.LatencyMonitorTarget,
MaxLatency: config.MaxLatencyThreshold,
OptimizationInterval: config.LatencyOptimizationInterval,
HistorySize: config.LatencyHistorySize,
JitterThreshold: config.JitterThreshold,
AdaptiveThreshold: config.LatencyAdaptiveThreshold,
TargetLatency: 50 * time.Millisecond,
MaxLatency: 200 * time.Millisecond,
OptimizationInterval: 5 * time.Second,
HistorySize: 100,
JitterThreshold: 20 * time.Millisecond,
AdaptiveThreshold: 0.8, // Trigger optimization when 80% above target
}
}
@ -125,9 +124,6 @@ func (lm *LatencyMonitor) RecordLatency(latency time.Duration, source string) {
now := time.Now()
latencyNanos := latency.Nanoseconds()
// Record in granular metrics histogram
GetGranularMetricsCollector().RecordProcessingLatency(latency)
// Update atomic counters
atomic.StoreInt64(&lm.currentLatency, latencyNanos)
atomic.AddInt64(&lm.latencySamples, 1)
@ -227,26 +223,7 @@ func (lm *LatencyMonitor) monitoringLoop() {
}
}
// runOptimization checks if optimization is needed and triggers callbacks with threshold validation.
//
// Validation Rules:
// - Current latency must not exceed MaxLatency (default: 200ms)
// - Average latency checked against adaptive threshold: TargetLatency * (1 + AdaptiveThreshold)
// - Jitter must not exceed JitterThreshold (default: 20ms)
// - All latency values must be non-negative durations
//
// Optimization Triggers:
// - Current latency > MaxLatency: Immediate optimization needed
// - Average latency > adaptive threshold: Gradual optimization needed
// - Jitter > JitterThreshold: Stability optimization needed
//
// Threshold Calculations:
// - Adaptive threshold = TargetLatency * (1.0 + AdaptiveThreshold)
// - Default: 50ms * (1.0 + 0.8) = 90ms adaptive threshold
// - Provides buffer above target before triggering optimization
//
// The function ensures real-time audio performance by monitoring multiple
// latency metrics and triggering optimization callbacks when thresholds are exceeded.
// runOptimization checks if optimization is needed and triggers callbacks
func (lm *LatencyMonitor) runOptimization() {
metrics := lm.GetMetrics()

View File

@ -171,8 +171,8 @@ func LogMemoryMetrics() {
metrics := CollectMemoryMetrics()
logger.Info().
Uint64("heap_alloc_mb", metrics.RuntimeStats.HeapAlloc/uint64(GetConfig().BytesToMBDivisor)).
Uint64("heap_sys_mb", metrics.RuntimeStats.HeapSys/uint64(GetConfig().BytesToMBDivisor)).
Uint64("heap_alloc_mb", metrics.RuntimeStats.HeapAlloc/1024/1024).
Uint64("heap_sys_mb", metrics.RuntimeStats.HeapSys/1024/1024).
Uint64("heap_objects", metrics.RuntimeStats.HeapObjects).
Uint32("num_gc", metrics.RuntimeStats.NumGC).
Float64("gc_cpu_fraction", metrics.RuntimeStats.GCCPUFraction).

View File

@ -451,7 +451,7 @@ func GetLastMetricsUpdate() time.Time {
// StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics
func StartMetricsUpdater() {
go func() {
ticker := time.NewTicker(GetConfig().StatsUpdateInterval) // Update every 5 seconds
ticker := time.NewTicker(5 * time.Second) // Update every 5 seconds
defer ticker.Stop()
for range ticker.C {

View File

@ -105,7 +105,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
}
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
manager := NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
manager := NewMicrophoneContentionManager(200 * time.Millisecond)
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
return manager
}
@ -115,7 +115,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
return (*MicrophoneContentionManager)(ptr)
}
return NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
return NewMicrophoneContentionManager(200 * time.Millisecond)
}
func TryMicrophoneOperation() OperationResult {

View File

@ -64,7 +64,7 @@ func RunAudioOutputServer() error {
StopNonBlockingAudioStreaming()
// Give some time for cleanup
time.Sleep(GetConfig().DefaultSleepDuration)
time.Sleep(100 * time.Millisecond)
logger.Info().Msg("Audio output server subprocess stopped")
return nil

View File

@ -61,9 +61,9 @@ func NewOutputStreamer() (*OutputStreamer, error) {
bufferPool: NewAudioBufferPool(GetMaxAudioFrameSize()), // Use existing buffer pool
ctx: ctx,
cancel: cancel,
batchSize: initialBatchSize, // Use adaptive batch size
processingChan: make(chan []byte, GetConfig().ChannelBufferSize), // Large buffer for smooth processing
statsInterval: GetConfig().StatsUpdateInterval, // Statistics interval from config
batchSize: initialBatchSize, // Use adaptive batch size
processingChan: make(chan []byte, 500), // Large buffer for smooth processing
statsInterval: 5 * time.Second, // Statistics every 5 seconds
lastStatsTime: time.Now().UnixNano(),
}, nil
}
@ -78,7 +78,7 @@ func (s *OutputStreamer) Start() error {
// Connect to audio output server
if err := s.client.Connect(); err != nil {
return fmt.Errorf("failed to connect to audio output server at %s: %w", getOutputSocketPath(), err)
return fmt.Errorf("failed to connect to audio output server: %w", err)
}
s.running = true
@ -122,12 +122,12 @@ func (s *OutputStreamer) streamLoop() {
defer runtime.UnlockOSThread()
// Adaptive timing for frame reading
frameInterval := time.Duration(GetConfig().OutputStreamingFrameIntervalMS) * time.Millisecond // 50 FPS base rate
frameInterval := time.Duration(20) * time.Millisecond // 50 FPS base rate
ticker := time.NewTicker(frameInterval)
defer ticker.Stop()
// Batch size update ticker
batchUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
batchUpdateTicker := time.NewTicker(500 * time.Millisecond)
defer batchUpdateTicker.Stop()
for {
@ -196,7 +196,7 @@ func (s *OutputStreamer) processingLoop() {
// Process frame (currently just receiving, but can be extended)
if _, err := s.client.ReceiveFrame(); err != nil {
if s.client.IsConnected() {
getOutputStreamingLogger().Warn().Err(err).Msg("Error reading audio frame from output server")
getOutputStreamingLogger().Warn().Err(err).Msg("Failed to receive frame")
atomic.AddInt64(&s.droppedFrames, 1)
}
// Try to reconnect if disconnected
@ -233,7 +233,7 @@ func (s *OutputStreamer) reportStatistics() {
processingTime := atomic.LoadInt64(&s.processingTime)
if processed > 0 {
dropRate := float64(dropped) / float64(processed+dropped) * GetConfig().PercentageMultiplier
dropRate := float64(dropped) / float64(processed+dropped) * 100
avgProcessingTime := time.Duration(processingTime)
getOutputStreamingLogger().Info().Int64("processed", processed).Int64("dropped", dropped).Float64("drop_rate", dropRate).Dur("avg_processing", avgProcessingTime).Msg("Output Audio Stats")
@ -270,7 +270,7 @@ func (s *OutputStreamer) GetDetailedStats() map[string]interface{} {
}
if processed+dropped > 0 {
stats["drop_rate_percent"] = float64(dropped) / float64(processed+dropped) * GetConfig().PercentageMultiplier
stats["drop_rate_percent"] = float64(dropped) / float64(processed+dropped) * 100
}
// Add client statistics
@ -318,7 +318,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
getOutputStreamingLogger().Info().Msg("Audio output streaming stopped")
}()
getOutputStreamingLogger().Info().Str("socket_path", getOutputSocketPath()).Msg("Audio output streaming started, connected to output server")
getOutputStreamingLogger().Info().Msg("Audio output streaming started")
buffer := make([]byte, GetMaxAudioFrameSize())
for {
@ -343,7 +343,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
RecordFrameReceived(n)
}
// Small delay to prevent busy waiting
time.Sleep(GetConfig().ShortSleepDuration)
time.Sleep(10 * time.Millisecond)
}
}
}()
@ -364,6 +364,6 @@ func StopAudioOutputStreaming() {
// Wait for streaming to stop
for atomic.LoadInt32(&outputStreamingRunning) == 1 {
time.Sleep(GetConfig().ShortSleepDuration)
time.Sleep(10 * time.Millisecond)
}
}

View File

@ -16,17 +16,23 @@ type SchedParam struct {
Priority int32
}
// getPriorityConstants returns priority levels from centralized config
func getPriorityConstants() (audioHigh, audioMedium, audioLow, normal int) {
config := GetConfig()
return config.AudioHighPriority, config.AudioMediumPriority, config.AudioLowPriority, config.NormalPriority
}
// Priority levels for audio processing
const (
// SCHED_FIFO priorities (1-99, higher = more priority)
AudioHighPriority = 80 // High priority for critical audio processing
AudioMediumPriority = 60 // Medium priority for regular audio processing
AudioLowPriority = 40 // Low priority for background audio tasks
// getSchedulingPolicies returns scheduling policies from centralized config
func getSchedulingPolicies() (schedNormal, schedFIFO, schedRR int) {
config := GetConfig()
return config.SchedNormal, config.SchedFIFO, config.SchedRR
}
// SCHED_NORMAL is the default (priority 0)
NormalPriority = 0
)
// Scheduling policies
const (
SCHED_NORMAL = 0
SCHED_FIFO = 1
SCHED_RR = 2
)
// PriorityScheduler manages thread priorities for audio processing
type PriorityScheduler struct {
@ -67,8 +73,7 @@ func (ps *PriorityScheduler) SetThreadPriority(priority int, policy int) error {
if errno != 0 {
// If we can't set real-time priority, try nice value instead
schedNormal, _, _ := getSchedulingPolicies()
if policy != schedNormal {
if policy != SCHED_NORMAL {
ps.logger.Warn().Int("errno", int(errno)).Msg("Failed to set real-time priority, falling back to nice")
return ps.setNicePriority(priority)
}
@ -84,11 +89,11 @@ func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
// Convert real-time priority to nice value (inverse relationship)
// RT priority 80 -> nice -10, RT priority 40 -> nice 0
niceValue := (40 - rtPriority) / 4
if niceValue < GetConfig().MinNiceValue {
niceValue = GetConfig().MinNiceValue
if niceValue < -20 {
niceValue = -20
}
if niceValue > GetConfig().MaxNiceValue {
niceValue = GetConfig().MaxNiceValue
if niceValue > 19 {
niceValue = 19
}
err := syscall.Setpriority(syscall.PRIO_PROCESS, 0, niceValue)
@ -103,30 +108,22 @@ func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
// SetAudioProcessingPriority sets high priority for audio processing threads
func (ps *PriorityScheduler) SetAudioProcessingPriority() error {
audioHigh, _, _, _ := getPriorityConstants()
_, schedFIFO, _ := getSchedulingPolicies()
return ps.SetThreadPriority(audioHigh, schedFIFO)
return ps.SetThreadPriority(AudioHighPriority, SCHED_FIFO)
}
// SetAudioIOPriority sets medium priority for audio I/O threads
func (ps *PriorityScheduler) SetAudioIOPriority() error {
_, audioMedium, _, _ := getPriorityConstants()
_, schedFIFO, _ := getSchedulingPolicies()
return ps.SetThreadPriority(audioMedium, schedFIFO)
return ps.SetThreadPriority(AudioMediumPriority, SCHED_FIFO)
}
// SetAudioBackgroundPriority sets low priority for background audio tasks
func (ps *PriorityScheduler) SetAudioBackgroundPriority() error {
_, _, audioLow, _ := getPriorityConstants()
_, schedFIFO, _ := getSchedulingPolicies()
return ps.SetThreadPriority(audioLow, schedFIFO)
return ps.SetThreadPriority(AudioLowPriority, SCHED_FIFO)
}
// ResetPriority resets thread to normal scheduling
func (ps *PriorityScheduler) ResetPriority() error {
_, _, _, normal := getPriorityConstants()
schedNormal, _, _ := getSchedulingPolicies()
return ps.SetThreadPriority(normal, schedNormal)
return ps.SetThreadPriority(NormalPriority, SCHED_NORMAL)
}
// Disable disables priority scheduling (useful for testing or fallback)

View File

@ -13,29 +13,26 @@ import (
"github.com/rs/zerolog"
)
// Variables for process monitoring (using configuration)
var (
// Constants for process monitoring
const (
// System constants
maxCPUPercent = GetConfig().MaxCPUPercent
minCPUPercent = GetConfig().MinCPUPercent
defaultClockTicks = GetConfig().DefaultClockTicks
defaultMemoryGB = GetConfig().DefaultMemoryGB
pageSize = 4096
maxCPUPercent = 100.0
minCPUPercent = 0.01
defaultClockTicks = 250.0 // Common for embedded ARM systems
defaultMemoryGB = 8
// Monitoring thresholds
maxWarmupSamples = GetConfig().MaxWarmupSamples
warmupCPUSamples = GetConfig().WarmupCPUSamples
maxWarmupSamples = 3
warmupCPUSamples = 2
logThrottleInterval = 10
// Channel buffer size
metricsChannelBuffer = GetConfig().MetricsChannelBuffer
metricsChannelBuffer = 100
// Clock tick detection ranges
minValidClockTicks = float64(GetConfig().MinValidClockTicks)
maxValidClockTicks = float64(GetConfig().MaxValidClockTicks)
)
// Variables for process monitoring
var (
pageSize = GetConfig().PageSize
minValidClockTicks = 50
maxValidClockTicks = 1000
)
// ProcessMetrics represents CPU and memory usage metrics for a process
@ -205,12 +202,12 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
statPath := fmt.Sprintf("/proc/%d/stat", pid)
statData, err := os.ReadFile(statPath)
if err != nil {
return metric, fmt.Errorf("failed to read process statistics from /proc/%d/stat: %w", pid, err)
return metric, err
}
fields := strings.Fields(string(statData))
if len(fields) < 24 {
return metric, fmt.Errorf("invalid process stat format: expected at least 24 fields, got %d from /proc/%d/stat", len(fields), pid)
return metric, fmt.Errorf("invalid stat format")
}
utime, _ := strconv.ParseInt(fields[13], 10, 64)
@ -220,7 +217,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
rss, _ := strconv.ParseInt(fields[23], 10, 64)
metric.MemoryRSS = rss * int64(pageSize)
metric.MemoryRSS = rss * pageSize
metric.MemoryVMS = vsize
// Calculate CPU percentage
@ -233,7 +230,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
// Calculate memory percentage (RSS / total system memory)
if totalMem := pm.getTotalMemory(); totalMem > 0 {
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * GetConfig().PercentageMultiplier
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * 100.0
}
// Update state for next calculation
@ -245,26 +242,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
return metric, nil
}
// calculateCPUPercent calculates CPU percentage for a process with validation and bounds checking.
//
// Validation Rules:
// - Returns 0.0 for first sample (no baseline for comparison)
// - Requires positive time delta between samples
// - Applies CPU percentage bounds: [MinCPUPercent, MaxCPUPercent]
// - Uses system clock ticks for accurate CPU time conversion
// - Validates clock ticks within range [MinValidClockTicks, MaxValidClockTicks]
//
// Bounds Applied:
// - CPU percentage clamped to [0.01%, 100.0%] (default values)
// - Clock ticks validated within [50, 1000] range (default values)
// - Time delta must be > 0 to prevent division by zero
//
// Warmup Behavior:
// - During warmup period (< WarmupCPUSamples), returns MinCPUPercent for idle processes
// - This indicates process is alive but not consuming significant CPU
//
// The function ensures accurate CPU percentage calculation while preventing
// invalid measurements that could affect system monitoring and adaptive algorithms.
// calculateCPUPercent calculates CPU percentage for a process
func (pm *ProcessMonitor) calculateCPUPercent(totalCPUTime int64, state *processState, now time.Time) float64 {
if state.lastSample.IsZero() {
// First sample - initialize baseline
@ -283,7 +261,7 @@ func (pm *ProcessMonitor) calculateCPUPercent(totalCPUTime int64, state *process
// Convert from clock ticks to seconds using actual system clock ticks
clockTicks := pm.getClockTicks()
cpuSeconds := cpuDelta / clockTicks
cpuPercent := (cpuSeconds / timeDelta) * GetConfig().PercentageMultiplier
cpuPercent := (cpuSeconds / timeDelta) * 100.0
// Apply bounds
if cpuPercent > maxCPUPercent {
@ -335,7 +313,7 @@ func (pm *ProcessMonitor) getClockTicks() float64 {
if len(fields) >= 2 {
if period, err := strconv.ParseInt(fields[1], 10, 64); err == nil && period > 0 {
// Convert nanoseconds to Hz
hz := GetConfig().CGONanosecondsPerSecond / float64(period)
hz := 1000000000.0 / float64(period)
if hz >= minValidClockTicks && hz <= maxValidClockTicks {
pm.clockTicks = hz
return
@ -363,7 +341,7 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
pm.memoryOnce.Do(func() {
file, err := os.Open("/proc/meminfo")
if err != nil {
pm.totalMemory = int64(defaultMemoryGB) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes)
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024
return
}
defer file.Close()
@ -375,14 +353,14 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
fields := strings.Fields(line)
if len(fields) >= 2 {
if kb, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
pm.totalMemory = kb * int64(GetConfig().ProcessMonitorKBToBytes)
pm.totalMemory = kb * 1024
return
}
}
break
}
}
pm.totalMemory = int64(defaultMemoryGB) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) // Fallback
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024 // Fallback
})
return pm.totalMemory
}

View File

@ -75,7 +75,7 @@ func (r *AudioRelay) Start(audioTrack AudioTrackWriter, config AudioConfig) erro
go r.relayLoop()
r.running = true
r.logger.Info().Msg("Audio relay connected to output server")
r.logger.Info().Msg("Audio relay started")
return nil
}
@ -97,7 +97,7 @@ func (r *AudioRelay) Stop() {
}
r.running = false
r.logger.Info().Msgf("Audio relay stopped after relaying %d frames", r.framesRelayed)
r.logger.Info().Msg("Audio relay stopped")
}
// SetMuted sets the mute state
@ -132,7 +132,7 @@ func (r *AudioRelay) relayLoop() {
defer r.wg.Done()
r.logger.Debug().Msg("Audio relay loop started")
var maxConsecutiveErrors = GetConfig().MaxConsecutiveErrors
const maxConsecutiveErrors = 10
consecutiveErrors := 0
for {
@ -144,14 +144,14 @@ func (r *AudioRelay) relayLoop() {
frame, err := r.client.ReceiveFrame()
if err != nil {
consecutiveErrors++
r.logger.Error().Err(err).Int("consecutive_errors", consecutiveErrors).Msg("Error reading frame from audio output server")
r.logger.Error().Err(err).Int("consecutive_errors", consecutiveErrors).Msg("Failed to receive audio frame")
r.incrementDropped()
if consecutiveErrors >= maxConsecutiveErrors {
r.logger.Error().Msgf("Too many consecutive read errors (%d/%d), stopping audio relay", consecutiveErrors, maxConsecutiveErrors)
r.logger.Error().Msg("Too many consecutive errors, stopping relay")
return
}
time.Sleep(GetConfig().ShortSleepDuration)
time.Sleep(10 * time.Millisecond)
continue
}

View File

@ -6,7 +6,12 @@ import (
"syscall"
)
// Socket buffer sizes are now centralized in config_constants.go
const (
// Socket buffer sizes optimized for JetKVM's audio workload
OptimalSocketBuffer = 128 * 1024 // 128KB (32 frames @ 4KB each)
MaxSocketBuffer = 256 * 1024 // 256KB for high-load scenarios
MinSocketBuffer = 32 * 1024 // 32KB minimum for basic functionality
)
// SocketBufferConfig holds socket buffer configuration
type SocketBufferConfig struct {
@ -18,8 +23,8 @@ type SocketBufferConfig struct {
// DefaultSocketBufferConfig returns the default socket buffer configuration
func DefaultSocketBufferConfig() SocketBufferConfig {
return SocketBufferConfig{
SendBufferSize: GetConfig().SocketOptimalBuffer,
RecvBufferSize: GetConfig().SocketOptimalBuffer,
SendBufferSize: OptimalSocketBuffer,
RecvBufferSize: OptimalSocketBuffer,
Enabled: true,
}
}
@ -27,8 +32,8 @@ func DefaultSocketBufferConfig() SocketBufferConfig {
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
func HighLoadSocketBufferConfig() SocketBufferConfig {
return SocketBufferConfig{
SendBufferSize: GetConfig().SocketMaxBuffer,
RecvBufferSize: GetConfig().SocketMaxBuffer,
SendBufferSize: MaxSocketBuffer,
RecvBufferSize: MaxSocketBuffer,
Enabled: true,
}
}
@ -101,49 +106,26 @@ func GetSocketBufferSizes(conn net.Conn) (sendSize, recvSize int, err error) {
return sendSize, recvSize, nil
}
// ValidateSocketBufferConfig validates socket buffer configuration parameters.
//
// Validation Rules:
// - If config.Enabled is false, no validation is performed (returns nil)
// - SendBufferSize must be >= SocketMinBuffer (default: 8192 bytes)
// - RecvBufferSize must be >= SocketMinBuffer (default: 8192 bytes)
// - SendBufferSize must be <= SocketMaxBuffer (default: 1048576 bytes)
// - RecvBufferSize must be <= SocketMaxBuffer (default: 1048576 bytes)
//
// Error Conditions:
// - Returns error if send buffer size is below minimum threshold
// - Returns error if receive buffer size is below minimum threshold
// - Returns error if send buffer size exceeds maximum threshold
// - Returns error if receive buffer size exceeds maximum threshold
//
// The validation ensures socket buffers are sized appropriately for audio streaming
// performance while preventing excessive memory usage.
// ValidateSocketBufferConfig validates socket buffer configuration
func ValidateSocketBufferConfig(config SocketBufferConfig) error {
if !config.Enabled {
return nil
}
minBuffer := GetConfig().SocketMinBuffer
maxBuffer := GetConfig().SocketMaxBuffer
if config.SendBufferSize < minBuffer {
return fmt.Errorf("send buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)",
config.SendBufferSize, minBuffer, minBuffer, maxBuffer)
if config.SendBufferSize < MinSocketBuffer {
return fmt.Errorf("send buffer size %d is below minimum %d", config.SendBufferSize, MinSocketBuffer)
}
if config.RecvBufferSize < minBuffer {
return fmt.Errorf("receive buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)",
config.RecvBufferSize, minBuffer, minBuffer, maxBuffer)
if config.RecvBufferSize < MinSocketBuffer {
return fmt.Errorf("receive buffer size %d is below minimum %d", config.RecvBufferSize, MinSocketBuffer)
}
if config.SendBufferSize > maxBuffer {
return fmt.Errorf("send buffer size validation failed: got %d bytes, maximum allowed %d bytes (configured range: %d-%d)",
config.SendBufferSize, maxBuffer, minBuffer, maxBuffer)
if config.SendBufferSize > MaxSocketBuffer {
return fmt.Errorf("send buffer size %d exceeds maximum %d", config.SendBufferSize, MaxSocketBuffer)
}
if config.RecvBufferSize > maxBuffer {
return fmt.Errorf("receive buffer size validation failed: got %d bytes, maximum allowed %d bytes (configured range: %d-%d)",
config.RecvBufferSize, maxBuffer, minBuffer, maxBuffer)
if config.RecvBufferSize > MaxSocketBuffer {
return fmt.Errorf("receive buffer size %d exceeds maximum %d", config.RecvBufferSize, MaxSocketBuffer)
}
return nil

View File

@ -131,7 +131,7 @@ func (s *AudioServerSupervisor) Stop() error {
select {
case <-s.processDone:
s.logger.Info().Msg("audio server process stopped gracefully")
case <-time.After(GetConfig().SupervisorTimeout):
case <-time.After(10 * time.Second):
s.logger.Warn().Msg("audio server process did not stop gracefully, forcing termination")
s.forceKillProcess()
}
@ -268,7 +268,7 @@ func (s *AudioServerSupervisor) startProcess() error {
// Start the process
if err := s.cmd.Start(); err != nil {
return fmt.Errorf("failed to start audio output server process: %w", err)
return fmt.Errorf("failed to start process: %w", err)
}
s.processPID = s.cmd.Process.Pid
@ -365,7 +365,7 @@ func (s *AudioServerSupervisor) terminateProcess() {
select {
case <-done:
s.logger.Info().Int("pid", pid).Msg("audio server process terminated gracefully")
case <-time.After(GetConfig().InputSupervisorTimeout):
case <-time.After(5 * time.Second):
s.logger.Warn().Int("pid", pid).Msg("process did not terminate gracefully, sending SIGKILL")
s.forceKillProcess()
}

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

@ -3,7 +3,6 @@ package audio
import (
"sync"
"sync/atomic"
"time"
"unsafe"
)
@ -21,10 +20,9 @@ type ZeroCopyAudioFrame struct {
// ZeroCopyFramePool manages reusable zero-copy audio frames
type ZeroCopyFramePool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
counter int64 // Frame counter (atomic)
hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic)
allocationCount int64 // Total allocations counter (atomic)
counter int64 // Frame counter (atomic)
hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic)
// Other fields
pool sync.Pool
@ -38,23 +36,13 @@ type ZeroCopyFramePool struct {
// NewZeroCopyFramePool creates a new zero-copy frame pool
func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
// Pre-allocate frames for immediate availability
preallocSizeBytes := GetConfig().PreallocSize
maxPoolSize := GetConfig().MaxPoolSize // Limit total pool size
// Calculate number of frames based on memory budget, not frame count
preallocFrameCount := preallocSizeBytes / maxFrameSize
if preallocFrameCount > maxPoolSize {
preallocFrameCount = maxPoolSize
}
if preallocFrameCount < 1 {
preallocFrameCount = 1 // Always preallocate at least one frame
}
preallocated := make([]*ZeroCopyAudioFrame, 0, preallocFrameCount)
// Pre-allocate 15 frames for immediate availability
preallocSize := 15
maxPoolSize := 50 // Limit total pool size
preallocated := make([]*ZeroCopyAudioFrame, 0, preallocSize)
// Pre-allocate frames to reduce initial allocation overhead
for i := 0; i < preallocFrameCount; i++ {
for i := 0; i < preallocSize; i++ {
frame := &ZeroCopyAudioFrame{
data: make([]byte, 0, maxFrameSize),
capacity: maxFrameSize,
@ -66,7 +54,7 @@ func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
return &ZeroCopyFramePool{
maxSize: maxFrameSize,
preallocated: preallocated,
preallocSize: preallocFrameCount,
preallocSize: preallocSize,
maxPoolSize: maxPoolSize,
pool: sync.Pool{
New: func() interface{} {
@ -82,32 +70,9 @@ func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
// Get retrieves a zero-copy frame from the pool
func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
start := time.Now()
var wasHit bool
defer func() {
latency := time.Since(start)
GetGranularMetricsCollector().RecordZeroCopyGet(latency, wasHit)
}()
// 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
atomic.AddInt64(&p.missCount, 1)
wasHit = true // Pool reuse counts as hit
frame := p.pool.Get().(*ZeroCopyAudioFrame)
frame.mutex.Lock()
frame.refCount = 1
frame.length = 0
frame.data = frame.data[:0]
frame.mutex.Unlock()
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()
@ -123,8 +88,7 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
}
p.mutex.Unlock()
// Try sync.Pool next and track allocation
atomic.AddInt64(&p.allocationCount, 1)
// Try sync.Pool next
frame := p.pool.Get().(*ZeroCopyAudioFrame)
frame.mutex.Lock()
frame.refCount = 1
@ -138,11 +102,6 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
// Put returns a zero-copy frame to the pool
func (p *ZeroCopyFramePool) Put(frame *ZeroCopyAudioFrame) {
start := time.Now()
defer func() {
latency := time.Since(start)
GetGranularMetricsCollector().RecordZeroCopyPut(latency, frame.capacity)
}()
if frame == nil || !frame.pooled {
return
}
@ -271,12 +230,11 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
hitCount := atomic.LoadInt64(&p.hitCount)
missCount := atomic.LoadInt64(&p.missCount)
allocationCount := atomic.LoadInt64(&p.allocationCount)
totalRequests := hitCount + missCount
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
hitRate = float64(hitCount) / float64(totalRequests) * 100
}
return ZeroCopyFramePoolStats{
@ -287,7 +245,6 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
PreallocatedMax: int64(p.preallocSize),
HitCount: hitCount,
MissCount: missCount,
AllocationCount: allocationCount,
HitRate: hitRate,
}
}
@ -301,7 +258,6 @@ type ZeroCopyFramePoolStats struct {
PreallocatedMax int64
HitCount int64
MissCount int64
AllocationCount int64
HitRate float64 // Percentage
}

View File

@ -0,0 +1,115 @@
//go:build arm && linux
package usbgadget
import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
var (
usbConfig = &Config{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
strictMode: true,
}
usbDevices = &Devices{
AbsoluteMouse: true,
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
}
usbGadgetName = "jetkvm"
usbGadget *UsbGadget
)
var oldAbsoluteMouseCombinedReportDesc = []byte{
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
// Report ID 1: Absolute Mouse Movement
0x85, 0x01, // Report ID (1)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x03, // Usage Maximum (0x03)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x03, // Report Count (3)
0x81, 0x02, // Input (Data, Var, Abs)
0x95, 0x01, // Report Count (1)
0x75, 0x05, // Report Size (5)
0x81, 0x03, // Input (Cnst, Var, Abs)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x16, 0x00, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x36, 0x00, 0x00, // Physical Minimum (0)
0x46, 0xFF, 0x7F, // Physical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data, Var, Abs)
0xC0, // End Collection
// Report ID 2: Relative Wheel Movement
0x85, 0x02, // Report ID (2)
0x09, 0x38, // Usage (Wheel)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x06, // Input (Data, Var, Rel)
0xC0, // End Collection
}
func TestUsbGadgetInit(t *testing.T) {
assert := assert.New(t)
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
assert.NotNil(usbGadget)
}
func TestUsbGadgetStrictModeInitFail(t *testing.T) {
usbConfig.strictMode = true
u := NewUsbGadget("test", usbDevices, usbConfig, nil)
assert.Nil(t, u, "should be nil")
}
func TestUsbGadgetUDCNotBoundAfterReportDescrChanged(t *testing.T) {
assert := assert.New(t)
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
assert.NotNil(usbGadget)
// release the usb gadget and create a new one
usbGadget = nil
altGadgetConfig := defaultGadgetConfig
oldAbsoluteMouseConfig := altGadgetConfig["absolute_mouse"]
oldAbsoluteMouseConfig.reportDesc = oldAbsoluteMouseCombinedReportDesc
altGadgetConfig["absolute_mouse"] = oldAbsoluteMouseConfig
usbGadget = newUsbGadget(usbGadgetName, altGadgetConfig, usbDevices, usbConfig, nil)
assert.NotNil(usbGadget)
udcs := getUdcs()
assert.Equal(1, len(udcs), "should be only one UDC")
// check if the UDC is bound
udc := udcs[0]
assert.NotNil(udc, "UDC should exist")
udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/jetkvm/UDC")
assert.Nil(err, "usb_gadget/UDC should exist")
assert.Equal(strings.TrimSpace(udc), strings.TrimSpace(string(udcStr)), "UDC should be the same")
}

View File

@ -1,293 +0,0 @@
package usbgadget
import (
"context"
"fmt"
"time"
"github.com/rs/zerolog"
)
// UsbGadgetInterface defines the interface for USB gadget operations
// This allows for mocking in tests and separating hardware operations from business logic
type UsbGadgetInterface interface {
// Configuration methods
Init() error
UpdateGadgetConfig() error
SetGadgetConfig(config *Config)
SetGadgetDevices(devices *Devices)
OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool)
// Hardware control methods
RebindUsb(ignoreUnbindError bool) error
IsUDCBound() (bool, error)
BindUDC() error
UnbindUDC() error
// HID file management
PreOpenHidFiles()
CloseHidFiles()
// Transaction methods
WithTransaction(fn func() error) error
WithTransactionTimeout(fn func() error, timeout time.Duration) error
// Path methods
GetConfigPath(itemKey string) (string, error)
GetPath(itemKey string) (string, error)
// Input methods (matching actual UsbGadget implementation)
KeyboardReport(modifier uint8, keys []uint8) error
AbsMouseReport(x, y int, buttons uint8) error
AbsMouseWheelReport(wheelY int8) error
RelMouseReport(mx, my int8, buttons uint8) error
}
// Ensure UsbGadget implements the interface
var _ UsbGadgetInterface = (*UsbGadget)(nil)
// MockUsbGadget provides a mock implementation for testing
type MockUsbGadget struct {
name string
enabledDevices Devices
customConfig Config
log *zerolog.Logger
// Mock state
initCalled bool
updateConfigCalled bool
rebindCalled bool
udcBound bool
hidFilesOpen bool
transactionCount int
// Mock behavior controls
ShouldFailInit bool
ShouldFailUpdateConfig bool
ShouldFailRebind bool
ShouldFailUDCBind bool
InitDelay time.Duration
UpdateConfigDelay time.Duration
RebindDelay time.Duration
}
// NewMockUsbGadget creates a new mock USB gadget for testing
func NewMockUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *MockUsbGadget {
if enabledDevices == nil {
enabledDevices = &defaultUsbGadgetDevices
}
if config == nil {
config = &Config{isEmpty: true}
}
if logger == nil {
logger = defaultLogger
}
return &MockUsbGadget{
name: name,
enabledDevices: *enabledDevices,
customConfig: *config,
log: logger,
udcBound: false,
hidFilesOpen: false,
}
}
// Init mocks USB gadget initialization
func (m *MockUsbGadget) Init() error {
if m.InitDelay > 0 {
time.Sleep(m.InitDelay)
}
if m.ShouldFailInit {
return m.logError("mock init failure", nil)
}
m.initCalled = true
m.udcBound = true
m.log.Info().Msg("mock USB gadget initialized")
return nil
}
// UpdateGadgetConfig mocks gadget configuration update
func (m *MockUsbGadget) UpdateGadgetConfig() error {
if m.UpdateConfigDelay > 0 {
time.Sleep(m.UpdateConfigDelay)
}
if m.ShouldFailUpdateConfig {
return m.logError("mock update config failure", nil)
}
m.updateConfigCalled = true
m.log.Info().Msg("mock USB gadget config updated")
return nil
}
// SetGadgetConfig mocks setting gadget configuration
func (m *MockUsbGadget) SetGadgetConfig(config *Config) {
if config != nil {
m.customConfig = *config
}
}
// SetGadgetDevices mocks setting enabled devices
func (m *MockUsbGadget) SetGadgetDevices(devices *Devices) {
if devices != nil {
m.enabledDevices = *devices
}
}
// OverrideGadgetConfig mocks gadget config override
func (m *MockUsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) {
m.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("mock override gadget config")
return nil, true
}
// RebindUsb mocks USB rebinding
func (m *MockUsbGadget) RebindUsb(ignoreUnbindError bool) error {
if m.RebindDelay > 0 {
time.Sleep(m.RebindDelay)
}
if m.ShouldFailRebind {
return m.logError("mock rebind failure", nil)
}
m.rebindCalled = true
m.log.Info().Msg("mock USB gadget rebound")
return nil
}
// IsUDCBound mocks UDC binding status check
func (m *MockUsbGadget) IsUDCBound() (bool, error) {
return m.udcBound, nil
}
// BindUDC mocks UDC binding
func (m *MockUsbGadget) BindUDC() error {
if m.ShouldFailUDCBind {
return m.logError("mock UDC bind failure", nil)
}
m.udcBound = true
m.log.Info().Msg("mock UDC bound")
return nil
}
// UnbindUDC mocks UDC unbinding
func (m *MockUsbGadget) UnbindUDC() error {
m.udcBound = false
m.log.Info().Msg("mock UDC unbound")
return nil
}
// PreOpenHidFiles mocks HID file pre-opening
func (m *MockUsbGadget) PreOpenHidFiles() {
m.hidFilesOpen = true
m.log.Info().Msg("mock HID files pre-opened")
}
// CloseHidFiles mocks HID file closing
func (m *MockUsbGadget) CloseHidFiles() {
m.hidFilesOpen = false
m.log.Info().Msg("mock HID files closed")
}
// WithTransaction mocks transaction execution
func (m *MockUsbGadget) WithTransaction(fn func() error) error {
return m.WithTransactionTimeout(fn, 60*time.Second)
}
// WithTransactionTimeout mocks transaction execution with timeout
func (m *MockUsbGadget) WithTransactionTimeout(fn func() error, timeout time.Duration) error {
m.transactionCount++
m.log.Info().Int("transactionCount", m.transactionCount).Msg("mock transaction started")
// Execute the function in a mock transaction context
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- fn()
}()
select {
case err := <-done:
if err != nil {
m.log.Error().Err(err).Msg("mock transaction failed")
} else {
m.log.Info().Msg("mock transaction completed")
}
return err
case <-ctx.Done():
m.log.Error().Dur("timeout", timeout).Msg("mock transaction timed out")
return ctx.Err()
}
}
// GetConfigPath mocks getting configuration path
func (m *MockUsbGadget) GetConfigPath(itemKey string) (string, error) {
return "/mock/config/path/" + itemKey, nil
}
// GetPath mocks getting path
func (m *MockUsbGadget) GetPath(itemKey string) (string, error) {
return "/mock/path/" + itemKey, nil
}
// KeyboardReport mocks keyboard input
func (m *MockUsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
m.log.Debug().Uint8("modifier", modifier).Int("keyCount", len(keys)).Msg("mock keyboard input sent")
return nil
}
// AbsMouseReport mocks absolute mouse input
func (m *MockUsbGadget) AbsMouseReport(x, y int, buttons uint8) error {
m.log.Debug().Int("x", x).Int("y", y).Uint8("buttons", buttons).Msg("mock absolute mouse input sent")
return nil
}
// AbsMouseWheelReport mocks absolute mouse wheel input
func (m *MockUsbGadget) AbsMouseWheelReport(wheelY int8) error {
m.log.Debug().Int8("wheelY", wheelY).Msg("mock absolute mouse wheel input sent")
return nil
}
// RelMouseReport mocks relative mouse input
func (m *MockUsbGadget) RelMouseReport(mx, my int8, buttons uint8) error {
m.log.Debug().Int8("mx", mx).Int8("my", my).Uint8("buttons", buttons).Msg("mock relative mouse input sent")
return nil
}
// Helper methods for mock
func (m *MockUsbGadget) logError(msg string, err error) error {
if err == nil {
err = fmt.Errorf("%s", msg)
}
m.log.Error().Err(err).Msg(msg)
return err
}
// Mock state inspection methods for testing
func (m *MockUsbGadget) IsInitCalled() bool {
return m.initCalled
}
func (m *MockUsbGadget) IsUpdateConfigCalled() bool {
return m.updateConfigCalled
}
func (m *MockUsbGadget) IsRebindCalled() bool {
return m.rebindCalled
}
func (m *MockUsbGadget) IsHidFilesOpen() bool {
return m.hidFilesOpen
}
func (m *MockUsbGadget) GetTransactionCount() int {
return m.transactionCount
}
func (m *MockUsbGadget) GetEnabledDevices() Devices {
return m.enabledDevices
}
func (m *MockUsbGadget) GetCustomConfig() Config {
return m.customConfig
}

View File

@ -1,330 +0,0 @@
//go:build arm && linux
package usbgadget
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Hardware integration tests for USB gadget operations
// These tests perform real hardware operations with proper cleanup and timeout handling
var (
testConfig = &Config{
VendorId: "0x1d6b", // The Linux Foundation
ProductId: "0x0104", // Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
strictMode: false, // Disable strict mode for hardware tests
}
testDevices = &Devices{
AbsoluteMouse: true,
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
}
testGadgetName = "jetkvm-test"
)
func TestUsbGadgetHardwareInit(t *testing.T) {
if testing.Short() {
t.Skip("Skipping hardware test in short mode")
}
// Create context with timeout to prevent hanging
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Ensure clean state before test
cleanupUsbGadget(t, testGadgetName)
// Test USB gadget initialization with timeout
var gadget *UsbGadget
done := make(chan bool, 1)
var initErr error
go func() {
defer func() {
if r := recover(); r != nil {
t.Logf("USB gadget initialization panicked: %v", r)
initErr = assert.AnError
}
done <- true
}()
gadget = NewUsbGadget(testGadgetName, testDevices, testConfig, nil)
if gadget == nil {
initErr = assert.AnError
}
}()
// Wait for initialization or timeout
select {
case <-done:
if initErr != nil {
t.Fatalf("USB gadget initialization failed: %v", initErr)
}
assert.NotNil(t, gadget, "USB gadget should be initialized")
case <-ctx.Done():
t.Fatal("USB gadget initialization timed out")
}
// Cleanup after test
defer func() {
if gadget != nil {
gadget.CloseHidFiles()
}
cleanupUsbGadget(t, testGadgetName)
}()
// Validate gadget state
assert.NotNil(t, gadget, "USB gadget should not be nil")
// Test UDC binding state
bound, err := gadget.IsUDCBound()
assert.NoError(t, err, "Should be able to check UDC binding state")
t.Logf("UDC bound state: %v", bound)
}
func TestUsbGadgetHardwareReconfiguration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping hardware test in short mode")
}
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
// Ensure clean state
cleanupUsbGadget(t, testGadgetName)
// Initialize first gadget
gadget1 := createUsbGadgetWithTimeout(t, ctx, testGadgetName, testDevices, testConfig)
defer func() {
if gadget1 != nil {
gadget1.CloseHidFiles()
}
}()
// Validate initial state
assert.NotNil(t, gadget1, "First USB gadget should be initialized")
// Close first gadget properly
gadget1.CloseHidFiles()
gadget1 = nil
// Wait for cleanup to complete
time.Sleep(500 * time.Millisecond)
// Test reconfiguration with different report descriptor
altGadgetConfig := make(map[string]gadgetConfigItem)
for k, v := range defaultGadgetConfig {
altGadgetConfig[k] = v
}
// Modify absolute mouse configuration
oldAbsoluteMouseConfig := altGadgetConfig["absolute_mouse"]
oldAbsoluteMouseConfig.reportDesc = absoluteMouseCombinedReportDesc
altGadgetConfig["absolute_mouse"] = oldAbsoluteMouseConfig
// Create second gadget with modified configuration
gadget2 := createUsbGadgetWithTimeoutAndConfig(t, ctx, testGadgetName, altGadgetConfig, testDevices, testConfig)
defer func() {
if gadget2 != nil {
gadget2.CloseHidFiles()
}
cleanupUsbGadget(t, testGadgetName)
}()
assert.NotNil(t, gadget2, "Second USB gadget should be initialized")
// Validate UDC binding after reconfiguration
udcs := getUdcs()
assert.NotEmpty(t, udcs, "Should have at least one UDC")
if len(udcs) > 0 {
udc := udcs[0]
t.Logf("Available UDC: %s", udc)
// Check UDC binding state
udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/" + testGadgetName + "/UDC")
if err == nil {
t.Logf("UDC binding: %s", strings.TrimSpace(string(udcStr)))
} else {
t.Logf("Could not read UDC binding: %v", err)
}
}
}
func TestUsbGadgetHardwareStressTest(t *testing.T) {
if testing.Short() {
t.Skip("Skipping stress test in short mode")
}
// Create context with longer timeout for stress test
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
// Ensure clean state
cleanupUsbGadget(t, testGadgetName)
// Perform multiple rapid reconfigurations
for i := 0; i < 3; i++ {
t.Logf("Stress test iteration %d", i+1)
// Create gadget
gadget := createUsbGadgetWithTimeout(t, ctx, testGadgetName, testDevices, testConfig)
if gadget == nil {
t.Fatalf("Failed to create USB gadget in iteration %d", i+1)
}
// Validate gadget
assert.NotNil(t, gadget, "USB gadget should be created in iteration %d", i+1)
// Test basic operations
bound, err := gadget.IsUDCBound()
assert.NoError(t, err, "Should be able to check UDC state in iteration %d", i+1)
t.Logf("Iteration %d: UDC bound = %v", i+1, bound)
// Cleanup
gadget.CloseHidFiles()
gadget = nil
// Wait between iterations
time.Sleep(1 * time.Second)
// Check for timeout
select {
case <-ctx.Done():
t.Fatal("Stress test timed out")
default:
// Continue
}
}
// Final cleanup
cleanupUsbGadget(t, testGadgetName)
}
// Helper functions for hardware tests
// createUsbGadgetWithTimeout creates a USB gadget with timeout protection
func createUsbGadgetWithTimeout(t *testing.T, ctx context.Context, name string, devices *Devices, config *Config) *UsbGadget {
return createUsbGadgetWithTimeoutAndConfig(t, ctx, name, defaultGadgetConfig, devices, config)
}
// createUsbGadgetWithTimeoutAndConfig creates a USB gadget with custom config and timeout protection
func createUsbGadgetWithTimeoutAndConfig(t *testing.T, ctx context.Context, name string, gadgetConfig map[string]gadgetConfigItem, devices *Devices, config *Config) *UsbGadget {
var gadget *UsbGadget
done := make(chan bool, 1)
var createErr error
go func() {
defer func() {
if r := recover(); r != nil {
t.Logf("USB gadget creation panicked: %v", r)
createErr = assert.AnError
}
done <- true
}()
gadget = newUsbGadget(name, gadgetConfig, devices, config, nil)
if gadget == nil {
createErr = assert.AnError
}
}()
// Wait for creation or timeout
select {
case <-done:
if createErr != nil {
t.Logf("USB gadget creation failed: %v", createErr)
return nil
}
return gadget
case <-ctx.Done():
t.Logf("USB gadget creation timed out")
return nil
}
}
// cleanupUsbGadget ensures clean state by removing any existing USB gadget configuration
func cleanupUsbGadget(t *testing.T, name string) {
t.Logf("Cleaning up USB gadget: %s", name)
// Try to unbind UDC first
udcPath := "/sys/kernel/config/usb_gadget/" + name + "/UDC"
if _, err := os.Stat(udcPath); err == nil {
// Read current UDC binding
if udcData, err := os.ReadFile(udcPath); err == nil && len(strings.TrimSpace(string(udcData))) > 0 {
// Unbind UDC
if err := os.WriteFile(udcPath, []byte(""), 0644); err != nil {
t.Logf("Failed to unbind UDC: %v", err)
} else {
t.Logf("Successfully unbound UDC")
// Wait for unbinding to complete
time.Sleep(200 * time.Millisecond)
}
}
}
// Remove gadget directory if it exists
gadgetPath := "/sys/kernel/config/usb_gadget/" + name
if _, err := os.Stat(gadgetPath); err == nil {
// Try to remove configuration links first
configPath := gadgetPath + "/configs/c.1"
if entries, err := os.ReadDir(configPath); err == nil {
for _, entry := range entries {
if entry.Type()&os.ModeSymlink != 0 {
linkPath := configPath + "/" + entry.Name()
if err := os.Remove(linkPath); err != nil {
t.Logf("Failed to remove config link %s: %v", linkPath, err)
}
}
}
}
// Remove the gadget directory (this should cascade remove everything)
if err := os.RemoveAll(gadgetPath); err != nil {
t.Logf("Failed to remove gadget directory: %v", err)
} else {
t.Logf("Successfully removed gadget directory")
}
}
// Wait for cleanup to complete
time.Sleep(300 * time.Millisecond)
}
// validateHardwareState checks the current hardware state
func validateHardwareState(t *testing.T, gadget *UsbGadget) {
if gadget == nil {
return
}
// Check UDC binding state
bound, err := gadget.IsUDCBound()
if err != nil {
t.Logf("Warning: Could not check UDC binding state: %v", err)
} else {
t.Logf("UDC bound: %v", bound)
}
// Check available UDCs
udcs := getUdcs()
t.Logf("Available UDCs: %v", udcs)
// Check configfs mount
if _, err := os.Stat("/sys/kernel/config"); err != nil {
t.Logf("Warning: configfs not available: %v", err)
} else {
t.Logf("configfs is available")
}
}

View File

@ -1,437 +0,0 @@
package usbgadget
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Unit tests for USB gadget configuration logic without hardware dependencies
// These tests follow the pattern of audio tests - testing business logic and validation
func TestUsbGadgetConfigValidation(t *testing.T) {
tests := []struct {
name string
config *Config
devices *Devices
expected bool
}{
{
name: "ValidConfig",
config: &Config{
VendorId: "0x1d6b",
ProductId: "0x0104",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
},
devices: &Devices{
Keyboard: true,
AbsoluteMouse: true,
RelativeMouse: true,
MassStorage: true,
},
expected: true,
},
{
name: "InvalidVendorId",
config: &Config{
VendorId: "invalid",
ProductId: "0x0104",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
},
devices: &Devices{
Keyboard: true,
},
expected: false,
},
{
name: "EmptyManufacturer",
config: &Config{
VendorId: "0x1d6b",
ProductId: "0x0104",
Manufacturer: "",
Product: "USB Emulation Device",
},
devices: &Devices{
Keyboard: true,
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateUsbGadgetConfiguration(tt.config, tt.devices)
if tt.expected {
assert.NoError(t, err, "Configuration should be valid")
} else {
assert.Error(t, err, "Configuration should be invalid")
}
})
}
}
func TestUsbGadgetDeviceConfiguration(t *testing.T) {
tests := []struct {
name string
devices *Devices
expectedConfigs []string
}{
{
name: "AllDevicesEnabled",
devices: &Devices{
Keyboard: true,
AbsoluteMouse: true,
RelativeMouse: true,
MassStorage: true,
Audio: true,
},
expectedConfigs: []string{"keyboard", "absolute_mouse", "relative_mouse", "mass_storage_base", "audio"},
},
{
name: "OnlyKeyboard",
devices: &Devices{
Keyboard: true,
},
expectedConfigs: []string{"keyboard"},
},
{
name: "MouseOnly",
devices: &Devices{
AbsoluteMouse: true,
RelativeMouse: true,
},
expectedConfigs: []string{"absolute_mouse", "relative_mouse"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configs := getEnabledGadgetConfigs(tt.devices)
assert.ElementsMatch(t, tt.expectedConfigs, configs, "Enabled configs should match expected")
})
}
}
func TestUsbGadgetStateTransition(t *testing.T) {
if testing.Short() {
t.Skip("Skipping state transition test in short mode")
}
tests := []struct {
name string
initialDevices *Devices
newDevices *Devices
expectedTransition string
}{
{
name: "EnableAudio",
initialDevices: &Devices{
Keyboard: true,
AbsoluteMouse: true,
Audio: false,
},
newDevices: &Devices{
Keyboard: true,
AbsoluteMouse: true,
Audio: true,
},
expectedTransition: "audio_enabled",
},
{
name: "DisableKeyboard",
initialDevices: &Devices{
Keyboard: true,
AbsoluteMouse: true,
},
newDevices: &Devices{
Keyboard: false,
AbsoluteMouse: true,
},
expectedTransition: "keyboard_disabled",
},
{
name: "NoChange",
initialDevices: &Devices{
Keyboard: true,
AbsoluteMouse: true,
},
newDevices: &Devices{
Keyboard: true,
AbsoluteMouse: true,
},
expectedTransition: "no_change",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
transition := simulateUsbGadgetStateTransition(ctx, tt.initialDevices, tt.newDevices)
assert.Equal(t, tt.expectedTransition, transition, "State transition should match expected")
})
}
}
func TestUsbGadgetConfigurationTimeout(t *testing.T) {
if testing.Short() {
t.Skip("Skipping timeout test in short mode")
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Test that configuration validation completes within reasonable time
start := time.Now()
// Simulate multiple rapid configuration changes
for i := 0; i < 20; i++ {
devices := &Devices{
Keyboard: i%2 == 0,
AbsoluteMouse: i%3 == 0,
RelativeMouse: i%4 == 0,
MassStorage: i%5 == 0,
Audio: i%6 == 0,
}
config := &Config{
VendorId: "0x1d6b",
ProductId: "0x0104",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
}
err := validateUsbGadgetConfiguration(config, devices)
assert.NoError(t, err, "Configuration validation should not fail")
// Ensure we don't timeout
select {
case <-ctx.Done():
t.Fatal("USB gadget configuration test timed out")
default:
// Continue
}
}
elapsed := time.Since(start)
t.Logf("USB gadget configuration test completed in %v", elapsed)
assert.Less(t, elapsed, 2*time.Second, "Configuration validation should complete quickly")
}
func TestReportDescriptorValidation(t *testing.T) {
tests := []struct {
name string
reportDesc []byte
expected bool
}{
{
name: "ValidKeyboardReportDesc",
reportDesc: keyboardReportDesc,
expected: true,
},
{
name: "ValidAbsoluteMouseReportDesc",
reportDesc: absoluteMouseCombinedReportDesc,
expected: true,
},
{
name: "ValidRelativeMouseReportDesc",
reportDesc: relativeMouseCombinedReportDesc,
expected: true,
},
{
name: "EmptyReportDesc",
reportDesc: []byte{},
expected: false,
},
{
name: "InvalidReportDesc",
reportDesc: []byte{0xFF, 0xFF, 0xFF},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateReportDescriptor(tt.reportDesc)
if tt.expected {
assert.NoError(t, err, "Report descriptor should be valid")
} else {
assert.Error(t, err, "Report descriptor should be invalid")
}
})
}
}
// Helper functions for simulation (similar to audio tests)
// validateUsbGadgetConfiguration simulates the validation that happens in production
func validateUsbGadgetConfiguration(config *Config, devices *Devices) error {
if config == nil {
return assert.AnError
}
// Validate vendor ID format
if config.VendorId == "" || len(config.VendorId) < 4 {
return assert.AnError
}
if config.VendorId != "" && config.VendorId[:2] != "0x" {
return assert.AnError
}
// Validate product ID format
if config.ProductId == "" || len(config.ProductId) < 4 {
return assert.AnError
}
if config.ProductId != "" && config.ProductId[:2] != "0x" {
return assert.AnError
}
// Validate required fields
if config.Manufacturer == "" {
return assert.AnError
}
if config.Product == "" {
return assert.AnError
}
// Note: Allow configurations with no devices enabled for testing purposes
// In production, this would typically be validated at a higher level
return nil
}
// getEnabledGadgetConfigs returns the list of enabled gadget configurations
func getEnabledGadgetConfigs(devices *Devices) []string {
var configs []string
if devices.Keyboard {
configs = append(configs, "keyboard")
}
if devices.AbsoluteMouse {
configs = append(configs, "absolute_mouse")
}
if devices.RelativeMouse {
configs = append(configs, "relative_mouse")
}
if devices.MassStorage {
configs = append(configs, "mass_storage_base")
}
if devices.Audio {
configs = append(configs, "audio")
}
return configs
}
// simulateUsbGadgetStateTransition simulates the state management during USB reconfiguration
func simulateUsbGadgetStateTransition(ctx context.Context, initial, new *Devices) string {
// Check for audio changes
if initial.Audio != new.Audio {
if new.Audio {
// Simulate enabling audio device
time.Sleep(5 * time.Millisecond)
return "audio_enabled"
} else {
// Simulate disabling audio device
time.Sleep(5 * time.Millisecond)
return "audio_disabled"
}
}
// Check for keyboard changes
if initial.Keyboard != new.Keyboard {
if new.Keyboard {
time.Sleep(5 * time.Millisecond)
return "keyboard_enabled"
} else {
time.Sleep(5 * time.Millisecond)
return "keyboard_disabled"
}
}
// Check for mouse changes
if initial.AbsoluteMouse != new.AbsoluteMouse || initial.RelativeMouse != new.RelativeMouse {
time.Sleep(5 * time.Millisecond)
return "mouse_changed"
}
// Check for mass storage changes
if initial.MassStorage != new.MassStorage {
time.Sleep(5 * time.Millisecond)
return "mass_storage_changed"
}
return "no_change"
}
// validateReportDescriptor simulates HID report descriptor validation
func validateReportDescriptor(reportDesc []byte) error {
if len(reportDesc) == 0 {
return assert.AnError
}
// Basic HID report descriptor validation
// Check for valid usage page (0x05)
found := false
for i := 0; i < len(reportDesc)-1; i++ {
if reportDesc[i] == 0x05 {
found = true
break
}
}
if !found {
return assert.AnError
}
return nil
}
// Benchmark tests
func BenchmarkValidateUsbGadgetConfiguration(b *testing.B) {
config := &Config{
VendorId: "0x1d6b",
ProductId: "0x0104",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
}
devices := &Devices{
Keyboard: true,
AbsoluteMouse: true,
RelativeMouse: true,
MassStorage: true,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validateUsbGadgetConfiguration(config, devices)
}
}
func BenchmarkGetEnabledGadgetConfigs(b *testing.B) {
devices := &Devices{
Keyboard: true,
AbsoluteMouse: true,
RelativeMouse: true,
MassStorage: true,
Audio: true,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = getEnabledGadgetConfigs(devices)
}
}
func BenchmarkValidateReportDescriptor(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validateReportDescriptor(keyboardReportDesc)
}
}

Binary file not shown.

15
usb.go
View File

@ -38,37 +38,22 @@ func initUsbGadget() {
}
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
if gadget == nil {
return nil // Gracefully handle uninitialized gadget (e.g., in tests)
}
return gadget.KeyboardReport(modifier, keys)
}
func rpcAbsMouseReport(x, y int, buttons uint8) error {
if gadget == nil {
return nil // Gracefully handle uninitialized gadget (e.g., in tests)
}
return gadget.AbsMouseReport(x, y, buttons)
}
func rpcRelMouseReport(dx, dy int8, buttons uint8) error {
if gadget == nil {
return nil // Gracefully handle uninitialized gadget (e.g., in tests)
}
return gadget.RelMouseReport(dx, dy, buttons)
}
func rpcWheelReport(wheelY int8) error {
if gadget == nil {
return nil // Gracefully handle uninitialized gadget (e.g., in tests)
}
return gadget.AbsMouseWheelReport(wheelY)
}
func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) {
if gadget == nil {
return usbgadget.KeyboardState{} // Return empty state for uninitialized gadget
}
return gadget.GetKeyboardState()
}