UNPKG

pw-client

Version:

Node.js wrapper for developing PipeWire clients

263 lines (262 loc) 10.3 kB
/** * Performance monitoring and metrics collection for audio streams. * Tracks buffer health, processing times, and diagnostic events. */ /** * Diagnostic event types for stream health monitoring. */ export var DiagnosticEvent; (function (DiagnosticEvent) { /** Buffer underrun detected - increase buffer size */ DiagnosticEvent["BufferUnderrun"] = "buffer-underrun"; /** Buffer overrun detected - reduce input rate */ DiagnosticEvent["BufferOverrun"] = "buffer-overrun"; /** Processing taking too long - optimize or increase buffer */ DiagnosticEvent["ProcessingDelay"] = "processing-delay"; /** Format negotiation failed */ DiagnosticEvent["FormatMismatch"] = "format-mismatch"; /** Latency spike detected */ DiagnosticEvent["LatencySpike"] = "latency-spike"; /** Buffer size adjusted automatically */ DiagnosticEvent["BufferAdjusted"] = "buffer-adjusted"; /** Performance degradation detected */ DiagnosticEvent["PerformanceDrop"] = "performance-drop"; })(DiagnosticEvent || (DiagnosticEvent = {})); /** * Performance monitor for audio streams. * Collects metrics and emits diagnostic events. */ export class StreamPerformanceMonitor { rate; channels; bufferSize; metrics; maxSamples = 100; processingTimes = []; bufferFills = []; latencies = []; lastProcessingTime = 0; eventCallbacks = new Map(); constructor(rate, channels, bufferSize) { this.rate = rate; this.channels = channels; this.bufferSize = bufferSize; this.metrics = { writeCount: 0, framesProcessed: 0, underrunCount: 0, overrunCount: 0, processingTime: { min: Infinity, max: 0, average: 0, recent: 0 }, bufferFill: { min: Infinity, max: 0, average: 0, current: 0 }, latency: { min: Infinity, max: 0, average: 0, current: 0 }, realTimeRatio: 1.0, lastUpdate: Date.now(), }; } /** * Record a write operation with performance data. */ recordWrite(frameCount, processingTimeMs, bufferFillRatio) { this.metrics.writeCount++; this.metrics.framesProcessed += frameCount; this.lastProcessingTime = processingTimeMs; // Update processing time statistics this.processingTimes.push(processingTimeMs); if (this.processingTimes.length > this.maxSamples) { this.processingTimes.shift(); } this.metrics.processingTime.recent = processingTimeMs; this.metrics.processingTime.min = Math.min(this.metrics.processingTime.min, processingTimeMs); this.metrics.processingTime.max = Math.max(this.metrics.processingTime.max, processingTimeMs); this.metrics.processingTime.average = this.processingTimes.reduce((a, b) => a + b, 0) / this.processingTimes.length; // Update buffer fill statistics this.bufferFills.push(bufferFillRatio); if (this.bufferFills.length > this.maxSamples) { this.bufferFills.shift(); } this.metrics.bufferFill.current = bufferFillRatio; this.metrics.bufferFill.min = Math.min(this.metrics.bufferFill.min, bufferFillRatio); this.metrics.bufferFill.max = Math.max(this.metrics.bufferFill.max, bufferFillRatio); this.metrics.bufferFill.average = this.bufferFills.reduce((a, b) => a + b, 0) / this.bufferFills.length; // Calculate real-time ratio const expectedTimeMs = (frameCount / this.rate / this.channels) * 1000; this.metrics.realTimeRatio = expectedTimeMs / processingTimeMs; this.metrics.lastUpdate = Date.now(); // Check for diagnostic conditions this.checkDiagnostics(bufferFillRatio, processingTimeMs, expectedTimeMs); } /** * Record a buffer underrun event. */ recordUnderrun() { this.metrics.underrunCount++; this.emitDiagnostic({ event: DiagnosticEvent.BufferUnderrun, timestamp: Date.now(), severity: "warning", message: "Buffer underrun detected - audio may stutter", data: { count: this.metrics.underrunCount, bufferFill: this.metrics.bufferFill.current, }, suggestion: "Consider increasing buffer size or optimizing processing", }); } /** * Record a buffer overrun event. */ recordOverrun() { this.metrics.overrunCount++; this.emitDiagnostic({ event: DiagnosticEvent.BufferOverrun, timestamp: Date.now(), severity: "warning", message: "Buffer overrun detected - data may be lost", data: { count: this.metrics.overrunCount, bufferFill: this.metrics.bufferFill.current, }, suggestion: "Consider reducing input rate or increasing buffer processing speed", }); } /** * Update latency measurement. */ recordLatency(latencyMs) { this.latencies.push(latencyMs); if (this.latencies.length > this.maxSamples) { this.latencies.shift(); } this.metrics.latency.current = latencyMs; this.metrics.latency.min = Math.min(this.metrics.latency.min, latencyMs); this.metrics.latency.max = Math.max(this.metrics.latency.max, latencyMs); this.metrics.latency.average = this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length; // Check for latency spikes if (this.latencies.length > 10) { const recentAverage = this.latencies.slice(-10).reduce((a, b) => a + b, 0) / 10; if (latencyMs > recentAverage * 2) { this.emitDiagnostic({ event: DiagnosticEvent.LatencySpike, timestamp: Date.now(), severity: "warning", message: `Latency spike detected: ${latencyMs.toFixed(2)}ms (avg: ${recentAverage.toFixed(2)}ms)`, data: { latency: latencyMs, average: recentAverage }, suggestion: "Check system load and consider buffer adjustments", }); } } } /** * Get current performance metrics. */ getMetrics() { return { ...this.metrics }; } /** * Subscribe to diagnostic events. */ onDiagnostic(event, callback) { if (!this.eventCallbacks.has(event)) { this.eventCallbacks.set(event, []); } const callbacks = this.eventCallbacks.get(event); if (callbacks) { callbacks.push(callback); } } /** * Unsubscribe from diagnostic events. */ offDiagnostic(event, callback) { const callbacks = this.eventCallbacks.get(event); if (callbacks) { const index = callbacks.indexOf(callback); if (index >= 0) { callbacks.splice(index, 1); } } } /** * Get health assessment of the stream. */ getHealthAssessment() { const issues = []; const suggestions = []; // Check buffer health if (this.metrics.bufferFill.current < 0.1) { issues.push("Buffer critically low"); suggestions.push("Increase buffer size or reduce processing complexity"); } else if (this.metrics.bufferFill.current < 0.3) { issues.push("Buffer running low"); suggestions.push("Consider increasing buffer size"); } // Check processing performance if (this.metrics.realTimeRatio < 0.8) { issues.push("Processing not keeping up with real-time"); suggestions.push("Optimize processing code or increase buffer size"); } // Check error rates if (this.metrics.underrunCount > 0) { issues.push(`${this.metrics.underrunCount} buffer underruns detected`); suggestions.push("Increase buffer size or optimize processing"); } if (this.metrics.overrunCount > 0) { issues.push(`${this.metrics.overrunCount} buffer overruns detected`); suggestions.push("Reduce input rate or increase processing speed"); } let overall = "good"; if (issues.length > 0) { overall = this.metrics.bufferFill.current < 0.1 || this.metrics.realTimeRatio < 0.5 ? "critical" : "warning"; } return { overall, issues, suggestions }; } checkDiagnostics(bufferFillRatio, processingTimeMs, expectedTimeMs) { // Check for processing delays if (processingTimeMs > expectedTimeMs * 1.5) { this.emitDiagnostic({ event: DiagnosticEvent.ProcessingDelay, timestamp: Date.now(), severity: "warning", message: `Processing taking ${(processingTimeMs / expectedTimeMs).toFixed(2)}x expected time`, data: { processingTime: processingTimeMs, expectedTime: expectedTimeMs, }, suggestion: "Optimize processing code or increase buffer size", }); } // Check for performance drops if (this.metrics.realTimeRatio < 0.8) { this.emitDiagnostic({ event: DiagnosticEvent.PerformanceDrop, timestamp: Date.now(), severity: "warning", message: `Performance dropped to ${(this.metrics.realTimeRatio * 100).toFixed(1)}% of real-time`, data: { realTimeRatio: this.metrics.realTimeRatio }, suggestion: "Check system load and optimize processing", }); } } emitDiagnostic(info) { const callbacks = this.eventCallbacks.get(info.event); if (callbacks) { for (const callback of callbacks) { try { callback(info); } catch (error) { console.error("Error in diagnostic event callback:", error); } } } } }