UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

156 lines (140 loc) 5.09 kB
import { assert } from "../../../core/assert.js"; /** * Aggregates `getStats()` snapshots from one or more named sources (typically * a `Transport`, `Channel`, or `NetworkPeer.channel_for(peer)`) and reports * recent throughput rates. * * Per-source stats are queried via the source's `getStats()` method. A "sample" * captures the current totals at a wall-clock moment; the meter retains a * sliding window of samples and computes bytes-per-second / packets-per-second * over the window. * * No timer of its own — caller invokes {@link sample} per tick (or whenever). * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class BandwidthMeter { #sources; #samples; /** * @param {{ window_seconds?: number }} [options] */ constructor({ window_seconds = 5 } = {}) { assert.isNumber(window_seconds, 'window_seconds'); if (window_seconds <= 0) throw new Error('BandwidthMeter: window_seconds must be > 0'); /** @readonly */ this.window_seconds = window_seconds; /** * Registered sources. Each is { name: string, source: { getStats() } }. * @type {{name: string, source: any}[]} * @private */ this.#sources = []; /** * Per-window samples (oldest first). Each entry: { time_ms, totals }. * @type {{time_ms: number, totals: {bytes_in: number, bytes_out: number, packets_in: number, packets_out: number}}[]} * @private */ this.#samples = []; } /** * @param {string} name human-readable label * @param {{ getStats(): {bytes_in: number, bytes_out: number, packets_in: number, packets_out: number} }} source */ add_source(name, source) { assert.isString(name, 'name'); assert.ok(source && typeof source.getStats === 'function', 'source must implement getStats()'); this.#sources.push({ name, source }); } /** * Capture a fresh sample at the given wall-clock time (caller-supplied so * tests can use a deterministic clock; in production typically `Date.now()`). * * @param {number} now_ms */ sample(now_ms) { assert.isNumber(now_ms, 'now_ms'); const totals = this.#sum_totals(); this.#samples.push({ time_ms: now_ms, totals }); // Drop samples older than the window, but keep one straddling expired // sample if it's the only thing standing between us and "only the // just-pushed sample is left" (which would silently make __rate return // 0 after a long quiet stretch). Once we have 2+ in-window samples the // straddle is no longer needed and would bias the rate, so drop it. const cutoff = now_ms - this.window_seconds * 1000; while (this.#samples.length >= 2 && this.#samples[1].time_ms < cutoff) { this.#samples.shift(); } if (this.#samples.length >= 3 && this.#samples[0].time_ms < cutoff) { this.#samples.shift(); } } /** * Cumulative totals across all sources, right now (re-queries getStats()). * Independent of the sample window. */ cumulative() { return this.#sum_totals(); } /** * Bytes-per-second received, averaged over the sample window. * Returns 0 if fewer than 2 samples have been taken. */ rate_bytes_in() { return this.#rate('bytes_in'); } rate_bytes_out() { return this.#rate('bytes_out'); } rate_packets_in() { return this.#rate('packets_in'); } rate_packets_out() { return this.#rate('packets_out'); } /** * Per-source breakdown of cumulative totals (re-queries getStats()). * @returns {Array<{name: string, totals: object}>} */ per_source() { const result = []; for (let i = 0; i < this.#sources.length; i++) { const s = this.#sources[i]; result.push({ name: s.name, totals: s.source.getStats() }); } return result; } /** * Drop all retained samples. Source registrations are preserved. */ reset_samples() { this.#samples.length = 0; } /** * @private */ #sum_totals() { let bytes_in = 0, bytes_out = 0, packets_in = 0, packets_out = 0; for (let i = 0; i < this.#sources.length; i++) { const s = this.#sources[i].source.getStats(); bytes_in += s.bytes_in; bytes_out += s.bytes_out; packets_in += s.packets_in; packets_out += s.packets_out; } return { bytes_in, bytes_out, packets_in, packets_out }; } /** * @private */ #rate(key) { const n = this.#samples.length; if (n < 2) return 0; const oldest = this.#samples[0]; const newest = this.#samples[n - 1]; const dt_seconds = (newest.time_ms - oldest.time_ms) / 1000; if (dt_seconds <= 0) return 0; return (newest.totals[key] - oldest.totals[key]) / dt_seconds; } }