pw-client
Version:
Node.js wrapper for developing PipeWire clients
263 lines (262 loc) • 10.3 kB
JavaScript
/**
* 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);
}
}
}
}
}