import type { IThroughputSample, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js'; /** * Tracks throughput data using time-series sampling */ export class ThroughputTracker { private samples: IThroughputSample[] = []; private readonly maxSamples: number; private accumulatedBytesIn: number = 0; private accumulatedBytesOut: number = 0; private lastSampleTime: number = 0; constructor(retentionSeconds: number = 3600) { // Keep samples for the retention period at 1 sample per second this.maxSamples = retentionSeconds; } /** * Record bytes transferred (called on every data transfer) */ public recordBytes(bytesIn: number, bytesOut: number): void { this.accumulatedBytesIn += bytesIn; this.accumulatedBytesOut += bytesOut; } /** * Take a sample of accumulated bytes (called every second) */ public takeSample(): void { const now = Date.now(); // Record accumulated bytes since last sample this.samples.push({ timestamp: now, bytesIn: this.accumulatedBytesIn, bytesOut: this.accumulatedBytesOut }); // Reset accumulators this.accumulatedBytesIn = 0; this.accumulatedBytesOut = 0; this.lastSampleTime = now; // Maintain circular buffer - remove oldest samples if (this.samples.length > this.maxSamples) { this.samples.shift(); } } /** * Get throughput rate over specified window (bytes per second) */ public getRate(windowSeconds: number): IThroughputData { if (this.samples.length === 0) { return { in: 0, out: 0 }; } const now = Date.now(); const windowStart = now - (windowSeconds * 1000); // Find samples within the window const relevantSamples = this.samples.filter(s => s.timestamp > windowStart); if (relevantSamples.length === 0) { return { in: 0, out: 0 }; } // Sum bytes in the window const totalBytesIn = relevantSamples.reduce((sum, s) => sum + s.bytesIn, 0); const totalBytesOut = relevantSamples.reduce((sum, s) => sum + s.bytesOut, 0); // Calculate actual window duration (might be less than requested if not enough data) const actualWindowSeconds = Math.min( windowSeconds, (now - relevantSamples[0].timestamp) / 1000 ); // Avoid division by zero if (actualWindowSeconds === 0) { return { in: 0, out: 0 }; } return { in: Math.round(totalBytesIn / actualWindowSeconds), out: Math.round(totalBytesOut / actualWindowSeconds) }; } /** * Get throughput history for specified duration */ public getHistory(durationSeconds: number): IThroughputHistoryPoint[] { const now = Date.now(); const startTime = now - (durationSeconds * 1000); // Filter samples within duration const relevantSamples = this.samples.filter(s => s.timestamp > startTime); // Convert to history points with per-second rates const history: IThroughputHistoryPoint[] = []; for (let i = 0; i < relevantSamples.length; i++) { const sample = relevantSamples[i]; // For the first sample or samples after gaps, we can't calculate rate if (i === 0 || sample.timestamp - relevantSamples[i - 1].timestamp > 2000) { history.push({ timestamp: sample.timestamp, in: sample.bytesIn, out: sample.bytesOut }); } else { // Calculate rate based on time since previous sample const prevSample = relevantSamples[i - 1]; const timeDelta = (sample.timestamp - prevSample.timestamp) / 1000; history.push({ timestamp: sample.timestamp, in: Math.round(sample.bytesIn / timeDelta), out: Math.round(sample.bytesOut / timeDelta) }); } } return history; } /** * Clear all samples */ public clear(): void { this.samples = []; this.accumulatedBytesIn = 0; this.accumulatedBytesOut = 0; this.lastSampleTime = 0; } /** * Get sample count for debugging */ public getSampleCount(): number { return this.samples.length; } }