mirror of
https://github.com/logsol/chuck.js.git
synced 2026-05-11 18:47:35 +00:00
Self-contained test under poc-webrtc/ that does not touch the game. Spins up an Express + WebSocket signaling + node-datachannel server alongside a Socket.IO server, serves a simple browser client that runs the same game-like traffic pattern (14Hz worldUpdates, input events, ping/pong) over either transport based on a URL flag. Captures per-session stats to a JSONL file and ships an analyze.js that prints a per-(transport, phase) summary of RTT percentiles, receive rate, and seq-gap counts so the TCP-vs-UDP-style comparison becomes quantitative rather than eyeball. Confirms node-datachannel installs and works on this platform and that the dual-channel (reliable + unreliable) pattern is feasible to maintain — both prerequisites for the real integration.
100 lines
3.4 KiB
JavaScript
100 lines
3.4 KiB
JavaScript
// Reads a run JSONL file and prints a per-(transport, phase) summary.
|
|
// Usage: node analyze.js [runs/<file>.jsonl]
|
|
// If no file given, uses the most recent file in runs/.
|
|
|
|
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
function pickFile() {
|
|
if (process.argv[2]) return process.argv[2];
|
|
const dir = path.join(__dirname, "runs");
|
|
const files = fs.readdirSync(dir)
|
|
.filter((f) => f.endsWith(".jsonl"))
|
|
.map((f) => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
.sort((a, b) => b.t - a.t);
|
|
if (!files.length) {
|
|
console.error("No run files in " + dir);
|
|
process.exit(1);
|
|
}
|
|
return path.join(dir, files[0].f);
|
|
}
|
|
|
|
const file = pickFile();
|
|
console.log("Analyzing: " + file + "\n");
|
|
|
|
const lines = fs.readFileSync(file, "utf8").trim().split("\n");
|
|
const events = lines.map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
|
|
|
|
// Group stats events by (session, transport) → phase
|
|
// Phase is the most recent "phase" event for that session before this stats event.
|
|
const sessionPhase = new Map();
|
|
const bucket = new Map(); // key: transport|phase → samples
|
|
|
|
function bucketKey(transport, phase) { return transport + "|" + phase; }
|
|
|
|
for (const e of events) {
|
|
if (e.type === "session_start") {
|
|
sessionPhase.set(e.session, "unmarked");
|
|
} else if (e.type === "phase") {
|
|
sessionPhase.set(e.session, e.label || "unmarked");
|
|
} else if (e.type === "stats") {
|
|
const phase = sessionPhase.get(e.session) || "unmarked";
|
|
const k = bucketKey(e.transport, phase);
|
|
if (!bucket.has(k)) {
|
|
bucket.set(k, {
|
|
transport: e.transport, phase,
|
|
rtt: [], recvRate: [], sendRate: [], seqGaps: [], reports: 0
|
|
});
|
|
}
|
|
const b = bucket.get(k);
|
|
b.reports++;
|
|
for (const r of e.rttSamples) b.rtt.push(r);
|
|
b.recvRate.push(e.recvRate);
|
|
b.sendRate.push(e.sendRate);
|
|
b.seqGaps.push(e.seqGaps);
|
|
}
|
|
}
|
|
|
|
function pct(arr, p) {
|
|
if (!arr.length) return NaN;
|
|
const s = arr.slice().sort((a, b) => a - b);
|
|
return s[Math.min(s.length - 1, Math.floor(s.length * p))];
|
|
}
|
|
function mean(arr) { return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : NaN; }
|
|
function fmt(n) { return Number.isFinite(n) ? n.toFixed(1) : " - "; }
|
|
function pad(s, n) { s = String(s); return s + " ".repeat(Math.max(0, n - s.length)); }
|
|
|
|
if (!bucket.size) {
|
|
console.log("No stats events found in run.");
|
|
process.exit(0);
|
|
}
|
|
|
|
const header = [
|
|
pad("transport", 10), pad("phase", 18),
|
|
pad("samples", 8), pad("rtt p50", 10), pad("rtt p95", 10),
|
|
pad("rtt p99", 10), pad("rtt max", 10),
|
|
pad("recvHz", 8), pad("gapsΔ", 8)
|
|
].join(" ");
|
|
console.log(header);
|
|
console.log("-".repeat(header.length));
|
|
|
|
const keys = [...bucket.keys()].sort();
|
|
for (const k of keys) {
|
|
const b = bucket.get(k);
|
|
const gapsDelta = b.seqGaps.length ? b.seqGaps[b.seqGaps.length - 1] - b.seqGaps[0] : 0;
|
|
console.log([
|
|
pad(b.transport, 10),
|
|
pad(b.phase, 18),
|
|
pad(b.rtt.length, 8),
|
|
pad(fmt(pct(b.rtt, 0.5)), 10),
|
|
pad(fmt(pct(b.rtt, 0.95)), 10),
|
|
pad(fmt(pct(b.rtt, 0.99)), 10),
|
|
pad(fmt(Math.max(0, ...b.rtt)), 10),
|
|
pad(fmt(mean(b.recvRate)), 8),
|
|
pad(gapsDelta, 8)
|
|
].join(" "));
|
|
}
|
|
|
|
console.log("\nLegend: rtt in ms, recvHz = worldUpdates/sec, gapsΔ = seq gaps observed during phase");
|