/** * Ring buffer of message-processing RTTs (ms) for planning percentiles. * @module rtt */ export class RttWindow { /** * @param {number} [cap=400] */ constructor(cap = 400) { this.cap = cap; /** @type {number[]} */ this.samples = []; } add(ms) { const n = Number(ms); if (!Number.isFinite(n) || n < 0) return; this.samples.push(n); if (this.samples.length > this.cap) this.samples.splice(0, this.samples.length - this.cap); } stats() { return summarizeRtt(this.samples); } } /** * @param {number[]} samples */ export function summarizeRtt(samples) { const xs = samples.filter((n) => Number.isFinite(n) && n >= 0).slice().sort((a, b) => a - b); if (!xs.length) { return { count: 0, minMs: null, avgMs: null, p50Ms: null, p90Ms: null }; } const sum = xs.reduce((a, b) => a + b, 0); return { count: xs.length, minMs: round1(xs[0]), avgMs: round1(sum / xs.length), p50Ms: round1(percentile(xs, 0.5)), p90Ms: round1(percentile(xs, 0.9)), }; } function percentile(sorted, p) { if (sorted.length === 1) return sorted[0]; const idx = (sorted.length - 1) * p; const lo = Math.floor(idx); const hi = Math.ceil(idx); if (lo === hi) return sorted[lo]; return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo); } function round1(n) { return Math.round(n * 10) / 10; }