Files
clash-verge-rev/src/utils/traffic-sampler.ts
T

145 lines
4.1 KiB
TypeScript

interface ICompressedDataPoint {
up: number;
down: number;
timestamp: number;
samples: number;
}
export const formatTrafficName = (timestamp: number) =>
new Date(timestamp).toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
export class TrafficDataSampler {
private rawBuffer: ITrafficDataPoint[] = [];
private rawHead = 0;
private compressedBuffer: ICompressedDataPoint[] = [];
private compressedHead = 0;
private compressionQueue: ITrafficDataPoint[] = [];
constructor(private config: ISamplingConfig) {}
addDataPoint(point: ITrafficDataPoint) {
this.rawBuffer.push(point);
const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000;
// O(1) amortized trimming using moving head; compact occasionally
while (
this.rawHead < this.rawBuffer.length &&
this.rawBuffer[this.rawHead]?.timestamp <= rawCutoff
) {
this.rawHead++;
}
if (this.rawHead > 512 && this.rawHead > this.rawBuffer.length / 2) {
this.rawBuffer = this.rawBuffer.slice(this.rawHead);
this.rawHead = 0;
}
this.compressionQueue.push(point);
if (this.compressionQueue.length >= this.config.compressionRatio) {
this.compressData();
}
const compressedCutoff =
Date.now() - this.config.compressedDataMinutes * 60 * 1000;
while (
this.compressedHead < this.compressedBuffer.length &&
this.compressedBuffer[this.compressedHead]?.timestamp <= compressedCutoff
) {
this.compressedHead++;
}
if (
this.compressedHead > 256 &&
this.compressedHead > this.compressedBuffer.length / 2
) {
this.compressedBuffer = this.compressedBuffer.slice(this.compressedHead);
this.compressedHead = 0;
}
}
private compressData() {
if (this.compressionQueue.length === 0) return;
const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0);
const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0);
const avgTimestamp =
this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) /
this.compressionQueue.length;
const compressedPoint: ICompressedDataPoint = {
up: totalUp / this.compressionQueue.length,
down: totalDown / this.compressionQueue.length,
timestamp: avgTimestamp,
samples: this.compressionQueue.length,
};
this.compressedBuffer.push(compressedPoint);
this.compressionQueue = [];
}
getDataForTimeRange(minutes: number): ITrafficDataPoint[] {
const cutoff = Date.now() - minutes * 60 * 1000;
let rawStart = this.rawHead;
while (
rawStart < this.rawBuffer.length &&
this.rawBuffer[rawStart]?.timestamp <= cutoff
) {
rawStart++;
}
const rawData = this.rawBuffer.slice(rawStart);
if (minutes <= this.config.rawDataMinutes) {
return rawData;
}
const compressedCutoffUpper =
Date.now() - this.config.rawDataMinutes * 60 * 1000;
let compressedStart = this.compressedHead;
while (
compressedStart < this.compressedBuffer.length &&
this.compressedBuffer[compressedStart]?.timestamp <= cutoff
) {
compressedStart++;
}
const compressedData = this.compressedBuffer
.slice(compressedStart)
.filter((p) => p.timestamp <= compressedCutoffUpper)
.map((p) => ({
up: p.up,
down: p.down,
timestamp: p.timestamp,
name: formatTrafficName(p.timestamp),
}));
return [...compressedData, ...rawData].sort(
(a, b) => a.timestamp - b.timestamp,
);
}
getStats(): ISamplerStats {
return {
rawBufferSize: this.rawBuffer.length - this.rawHead,
compressedBufferSize: this.compressedBuffer.length - this.compressedHead,
compressionQueueSize: this.compressionQueue.length,
totalMemoryPoints:
this.rawBuffer.length -
this.rawHead +
(this.compressedBuffer.length - this.compressedHead),
};
}
clear() {
this.rawBuffer = [];
this.rawHead = 0;
this.compressedBuffer = [];
this.compressedHead = 0;
this.compressionQueue = [];
}
}