0.2.6-dev: traffic counters — HTTP body bytes by category (assets / audio / code / data) and WebSocket wire / payload bytes since the start at GET /healthz?traffic=1, and the page's own downloads (Resource Timing transferSize) as a traffic line in the 问题反馈 diagnostics (host bandwidth layer 0.4); golden: unchanged
This commit is contained in:
1 parent
05b0dba8b5
commit
be8d382c05
14 files changed
+516
-23
No files matched your search
@@ -0,0 +1,102 @@
|
||||
// server/http/traffic.js — what this server has sent and received since it started (host bandwidth, layer 0):
|
||||
//
|
||||
// * HTTP: per category of shared/assetPaths.js trafficCategory (assets / audio / code / data / other) the responses
|
||||
// and the body bytes actually written (after gzip / brotli; headers not counted), and how many were 304s;
|
||||
// * WebSocket: the connections, the bytes on the wire both ways (the sockets' own counters, after permessage-deflate,
|
||||
// frame headers included, the HTTP upgrade not) and the message payloads before compression — their ratio is what
|
||||
// the compression saves.
|
||||
//
|
||||
// routes.js meters every response (`meter`), websocket.js every socket (`wsOpen` / `wsClose` / `wsSent` / `wsReceived`);
|
||||
// `snapshot()` is the `traffic` object of GET /healthz?traffic=1 (docs/DEPLOY.md §5). Nothing is stored or sent
|
||||
// anywhere else; the counters start at zero with the process.
|
||||
|
||||
import { TRAFFIC_CATEGORIES, trafficCategory } from '../../shared/assetPaths.js';
|
||||
|
||||
/** Byte length of a chunk handed to res.write / res.end (a string in `encoding`, utf8 by default). */
|
||||
function chunkBytes(chunk, encoding) {
|
||||
if (chunk == null || typeof chunk === 'function') return 0;
|
||||
if (typeof chunk === 'string') return Buffer.byteLength(chunk, typeof encoding === 'string' && Buffer.isEncoding(encoding) ? encoding : 'utf8');
|
||||
return typeof chunk.byteLength === 'number' ? chunk.byteLength : 0;
|
||||
}
|
||||
|
||||
/** Length of a ws 'message' payload (a Buffer, an ArrayBuffer or, fragmented, a Buffer list). */
|
||||
function payloadBytes(data) {
|
||||
if (Array.isArray(data)) return data.reduce((n, b) => n + payloadBytes(b), 0);
|
||||
return data && typeof data.byteLength === 'number' ? data.byteLength : typeof data === 'string' ? Buffer.byteLength(data) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A traffic counter for one server.
|
||||
* @param {{ now?: () => number }} [opts]
|
||||
*/
|
||||
export function createTrafficCounter({ now = Date.now } = {}) {
|
||||
const startedAt = now();
|
||||
const http = Object.fromEntries(TRAFFIC_CATEGORIES.map((c) => [c, { requests: 0, bytes: 0, notModified: 0 }]));
|
||||
const ws = { connections: 0, wireOut: 0, wireIn: 0, payloadOut: 0, payloadIn: 0, messagesOut: 0, messagesIn: 0 };
|
||||
/** @type {Map<import('node:net').Socket, { out: number, in: number }>} live sockets and their counters at the upgrade */
|
||||
const live = new Map();
|
||||
|
||||
/** One finished HTTP response. */
|
||||
function record(path, status, bytes) {
|
||||
const c = http[trafficCategory(path)] || http.other;
|
||||
c.requests++;
|
||||
c.bytes += bytes;
|
||||
if (status === 304) c.notModified++;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Count the body bytes of `res` (every res.write / res.end) under the category of `path`, once the response is
|
||||
* done or its connection gone.
|
||||
* @param {import('node:http').ServerResponse} res
|
||||
* @param {string} path the request's URL path
|
||||
*/
|
||||
meter(res, path) {
|
||||
let bytes = 0;
|
||||
const { write, end } = res;
|
||||
res.write = function meteredWrite(chunk, encoding, cb) {
|
||||
bytes += chunkBytes(chunk, encoding);
|
||||
return write.call(this, chunk, encoding, cb);
|
||||
};
|
||||
res.end = function meteredEnd(chunk, encoding, cb) {
|
||||
bytes += chunkBytes(chunk, encoding);
|
||||
return end.call(this, chunk, encoding, cb);
|
||||
};
|
||||
res.once('close', () => record(path, res.statusCode, bytes));
|
||||
},
|
||||
/** A WebSocket connection opened on `socket` (after its upgrade answer was written). */
|
||||
wsOpen(socket) {
|
||||
ws.connections++;
|
||||
if (socket && !live.has(socket)) live.set(socket, { out: socket.bytesWritten || 0, in: socket.bytesRead || 0 });
|
||||
},
|
||||
/** …and closed: its wire bytes move to the totals. */
|
||||
wsClose(socket) {
|
||||
const base = live.get(socket);
|
||||
if (!base) return;
|
||||
live.delete(socket);
|
||||
ws.wireOut += Math.max(0, (socket.bytesWritten || 0) - base.out);
|
||||
ws.wireIn += Math.max(0, (socket.bytesRead || 0) - base.in);
|
||||
},
|
||||
/** A message handed to ws.send (its payload before compression). */
|
||||
wsSent(data) { ws.messagesOut++; ws.payloadOut += payloadBytes(data); },
|
||||
/** A message received (its payload after inflating). */
|
||||
wsReceived(data) { ws.messagesIn++; ws.payloadIn += payloadBytes(data); },
|
||||
/** The counters now (plain JSON). */
|
||||
snapshot() {
|
||||
let wireOut = ws.wireOut;
|
||||
let wireIn = ws.wireIn;
|
||||
for (const [socket, base] of live) {
|
||||
wireOut += Math.max(0, (socket.bytesWritten || 0) - base.out);
|
||||
wireIn += Math.max(0, (socket.bytesRead || 0) - base.in);
|
||||
}
|
||||
const byCat = Object.fromEntries(Object.entries(http).map(([k, v]) => [k, { ...v }]));
|
||||
const total = Object.values(http).reduce((t, v) => ({ requests: t.requests + v.requests, bytes: t.bytes + v.bytes }), { requests: 0, bytes: 0 });
|
||||
return {
|
||||
since: new Date(startedAt).toISOString(),
|
||||
seconds: Math.max(0, Math.round((now() - startedAt) / 1000)),
|
||||
http: { ...byCat, total },
|
||||
ws: { ...ws, open: live.size, wireOut, wireIn },
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user