Files
smartproxy/ts/proxies/smart-proxy/throughput-tracker.ts
2025-06-22 22:28:37 +00:00

144 lines
4.1 KiB
TypeScript

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