node/lib/internal/quic/stats.js

676 lines
20 KiB
JavaScript

'use strict';
const {
BigUint64Array,
JSONStringify,
} = primordials;
const {
isArrayBuffer,
} = require('util/types');
const {
codes: {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
},
} = require('internal/errors');
const { inspect } = require('internal/util/inspect');
const assert = require('internal/assert');
const {
kFinishClose,
kInspect,
kPrivateConstructor,
} = require('internal/quic/symbols');
// This file defines the helper objects for accessing statistics collected
// by various QUIC objects. Each of these wraps a BigUint64Array. Every
// stats object is read-only via the API, and the underlying buffer is
// only updated by the QUIC internals. When the stats object is no longer
// needed, it is closed to prevent further updates to the buffer.
const {
// All of the IDX_STATS_* constants are the index positions of the stats
// fields in the relevant BigUint64Array's that underlie the *Stats objects.
// These are not exposed to end users.
IDX_STATS_ENDPOINT_CREATED_AT,
IDX_STATS_ENDPOINT_DESTROYED_AT,
IDX_STATS_ENDPOINT_BYTES_RECEIVED,
IDX_STATS_ENDPOINT_BYTES_SENT,
IDX_STATS_ENDPOINT_PACKETS_RECEIVED,
IDX_STATS_ENDPOINT_PACKETS_SENT,
IDX_STATS_ENDPOINT_SERVER_SESSIONS,
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
IDX_STATS_ENDPOINT_RETRY_COUNT,
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
IDX_STATS_SESSION_CREATED_AT,
IDX_STATS_SESSION_CLOSING_AT,
IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT,
IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT,
IDX_STATS_SESSION_BYTES_RECEIVED,
IDX_STATS_SESSION_BYTES_SENT,
IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT,
IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT,
IDX_STATS_SESSION_UNI_IN_STREAM_COUNT,
IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT,
IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT,
IDX_STATS_SESSION_BYTES_IN_FLIGHT,
IDX_STATS_SESSION_BLOCK_COUNT,
IDX_STATS_SESSION_CWND,
IDX_STATS_SESSION_LATEST_RTT,
IDX_STATS_SESSION_MIN_RTT,
IDX_STATS_SESSION_RTTVAR,
IDX_STATS_SESSION_SMOOTHED_RTT,
IDX_STATS_SESSION_SSTHRESH,
IDX_STATS_SESSION_DATAGRAMS_RECEIVED,
IDX_STATS_SESSION_DATAGRAMS_SENT,
IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED,
IDX_STATS_SESSION_DATAGRAMS_LOST,
IDX_STATS_STREAM_CREATED_AT,
IDX_STATS_STREAM_OPENED_AT,
IDX_STATS_STREAM_RECEIVED_AT,
IDX_STATS_STREAM_ACKED_AT,
IDX_STATS_STREAM_DESTROYED_AT,
IDX_STATS_STREAM_BYTES_RECEIVED,
IDX_STATS_STREAM_BYTES_SENT,
IDX_STATS_STREAM_MAX_OFFSET,
IDX_STATS_STREAM_MAX_OFFSET_ACK,
IDX_STATS_STREAM_MAX_OFFSET_RECV,
IDX_STATS_STREAM_FINAL_SIZE,
} = internalBinding('quic');
assert(IDX_STATS_ENDPOINT_CREATED_AT !== undefined);
assert(IDX_STATS_ENDPOINT_DESTROYED_AT !== undefined);
assert(IDX_STATS_ENDPOINT_BYTES_RECEIVED !== undefined);
assert(IDX_STATS_ENDPOINT_BYTES_SENT !== undefined);
assert(IDX_STATS_ENDPOINT_PACKETS_RECEIVED !== undefined);
assert(IDX_STATS_ENDPOINT_PACKETS_SENT !== undefined);
assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS !== undefined);
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT !== undefined);
assert(IDX_STATS_ENDPOINT_RETRY_COUNT !== undefined);
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT !== undefined);
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT !== undefined);
assert(IDX_STATS_SESSION_CREATED_AT !== undefined);
assert(IDX_STATS_SESSION_CLOSING_AT !== undefined);
assert(IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT !== undefined);
assert(IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT !== undefined);
assert(IDX_STATS_SESSION_BYTES_RECEIVED !== undefined);
assert(IDX_STATS_SESSION_BYTES_SENT !== undefined);
assert(IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT !== undefined);
assert(IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT !== undefined);
assert(IDX_STATS_SESSION_UNI_IN_STREAM_COUNT !== undefined);
assert(IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT !== undefined);
assert(IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT !== undefined);
assert(IDX_STATS_SESSION_BYTES_IN_FLIGHT !== undefined);
assert(IDX_STATS_SESSION_BLOCK_COUNT !== undefined);
assert(IDX_STATS_SESSION_CWND !== undefined);
assert(IDX_STATS_SESSION_LATEST_RTT !== undefined);
assert(IDX_STATS_SESSION_MIN_RTT !== undefined);
assert(IDX_STATS_SESSION_RTTVAR !== undefined);
assert(IDX_STATS_SESSION_SMOOTHED_RTT !== undefined);
assert(IDX_STATS_SESSION_SSTHRESH !== undefined);
assert(IDX_STATS_SESSION_DATAGRAMS_RECEIVED !== undefined);
assert(IDX_STATS_SESSION_DATAGRAMS_SENT !== undefined);
assert(IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED !== undefined);
assert(IDX_STATS_SESSION_DATAGRAMS_LOST !== undefined);
assert(IDX_STATS_STREAM_CREATED_AT !== undefined);
assert(IDX_STATS_STREAM_OPENED_AT !== undefined);
assert(IDX_STATS_STREAM_RECEIVED_AT !== undefined);
assert(IDX_STATS_STREAM_ACKED_AT !== undefined);
assert(IDX_STATS_STREAM_DESTROYED_AT !== undefined);
assert(IDX_STATS_STREAM_BYTES_RECEIVED !== undefined);
assert(IDX_STATS_STREAM_BYTES_SENT !== undefined);
assert(IDX_STATS_STREAM_MAX_OFFSET !== undefined);
assert(IDX_STATS_STREAM_MAX_OFFSET_ACK !== undefined);
assert(IDX_STATS_STREAM_MAX_OFFSET_RECV !== undefined);
assert(IDX_STATS_STREAM_FINAL_SIZE !== undefined);
class QuicEndpointStats {
/** @type {BigUint64Array} */
#handle;
/** @type {boolean} */
#disconnected = false;
/**
* @param {symbol} privateSymbol
* @param {ArrayBuffer} buffer
*/
constructor(privateSymbol, buffer) {
// We use the kPrivateConstructor symbol to restrict the ability to
// create new instances of QuicEndpointStats to internal code.
if (privateSymbol !== kPrivateConstructor) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
if (!isArrayBuffer(buffer)) {
throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer);
}
this.#handle = new BigUint64Array(buffer);
}
/** @type {bigint} */
get createdAt() {
return this.#handle[IDX_STATS_ENDPOINT_CREATED_AT];
}
/** @type {bigint} */
get destroyedAt() {
return this.#handle[IDX_STATS_ENDPOINT_DESTROYED_AT];
}
/** @type {bigint} */
get bytesReceived() {
return this.#handle[IDX_STATS_ENDPOINT_BYTES_RECEIVED];
}
/** @type {bigint} */
get bytesSent() {
return this.#handle[IDX_STATS_ENDPOINT_BYTES_SENT];
}
/** @type {bigint} */
get packetsReceived() {
return this.#handle[IDX_STATS_ENDPOINT_PACKETS_RECEIVED];
}
/** @type {bigint} */
get packetsSent() {
return this.#handle[IDX_STATS_ENDPOINT_PACKETS_SENT];
}
/** @type {bigint} */
get serverSessions() {
return this.#handle[IDX_STATS_ENDPOINT_SERVER_SESSIONS];
}
/** @type {bigint} */
get clientSessions() {
return this.#handle[IDX_STATS_ENDPOINT_CLIENT_SESSIONS];
}
/** @type {bigint} */
get serverBusyCount() {
return this.#handle[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT];
}
/** @type {bigint} */
get retryCount() {
return this.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
}
/** @type {bigint} */
get versionNegotiationCount() {
return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
}
/** @type {bigint} */
get statelessResetCount() {
return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
}
/** @type {bigint} */
get immediateCloseCount() {
return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
}
toString() {
return JSONStringify(this.toJSON());
}
toJSON() {
return {
__proto__: null,
connected: this.isConnected,
// We need to convert the values to strings because JSON does not
// support BigInts.
createdAt: `${this.createdAt}`,
destroyedAt: `${this.destroyedAt}`,
bytesReceived: `${this.bytesReceived}`,
bytesSent: `${this.bytesSent}`,
packetsReceived: `${this.packetsReceived}`,
packetsSent: `${this.packetsSent}`,
serverSessions: `${this.serverSessions}`,
clientSessions: `${this.clientSessions}`,
serverBusyCount: `${this.serverBusyCount}`,
retryCount: `${this.retryCount}`,
versionNegotiationCount: `${this.versionNegotiationCount}`,
statelessResetCount: `${this.statelessResetCount}`,
immediateCloseCount: `${this.immediateCloseCount}`,
};
}
[kInspect](depth, options) {
if (depth < 0)
return this;
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
return `QuicEndpointStats ${inspect({
connected: this.isConnected,
createdAt: this.createdAt,
destroyedAt: this.destroyedAt,
bytesReceived: this.bytesReceived,
bytesSent: this.bytesSent,
packetsReceived: this.packetsReceived,
packetsSent: this.packetsSent,
serverSessions: this.serverSessions,
clientSessions: this.clientSessions,
serverBusyCount: this.serverBusyCount,
retryCount: this.retryCount,
versionNegotiationCount: this.versionNegotiationCount,
statelessResetCount: this.statelessResetCount,
immediateCloseCount: this.immediateCloseCount,
}, opts)}`;
}
/**
* True if this QuicEndpointStats object is still connected to the underlying
* Endpoint stats source. If this returns false, then the stats object is
* no longer being updated and should be considered stale.
* @returns {boolean}
*/
get isConnected() {
return !this.#disconnected;
}
[kFinishClose]() {
// Snapshot the stats into a new BigUint64Array since the underlying
// buffer will be destroyed.
this.#handle = new BigUint64Array(this.#handle);
this.#disconnected = true;
}
}
class QuicSessionStats {
/** @type {BigUint64Array} */
#handle;
/** @type {boolean} */
#disconnected = false;
/**
* @param {symbol} privateSynbol
* @param {BigUint64Array} buffer
*/
constructor(privateSynbol, buffer) {
// We use the kPrivateConstructor symbol to restrict the ability to
// create new instances of QuicSessionStats to internal code.
if (privateSynbol !== kPrivateConstructor) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
if (!isArrayBuffer(buffer)) {
throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer);
}
this.#handle = new BigUint64Array(buffer);
}
/** @type {bigint} */
get createdAt() {
return this.#handle[IDX_STATS_SESSION_CREATED_AT];
}
/** @type {bigint} */
get closingAt() {
return this.#handle[IDX_STATS_SESSION_CLOSING_AT];
}
/** @type {bigint} */
get handshakeCompletedAt() {
return this.#handle[IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT];
}
/** @type {bigint} */
get handshakeConfirmedAt() {
return this.#handle[IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT];
}
/** @type {bigint} */
get bytesReceived() {
return this.#handle[IDX_STATS_SESSION_BYTES_RECEIVED];
}
/** @type {bigint} */
get bytesSent() {
return this.#handle[IDX_STATS_SESSION_BYTES_SENT];
}
/** @type {bigint} */
get bidiInStreamCount() {
return this.#handle[IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT];
}
/** @type {bigint} */
get bidiOutStreamCount() {
return this.#handle[IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT];
}
/** @type {bigint} */
get uniInStreamCount() {
return this.#handle[IDX_STATS_SESSION_UNI_IN_STREAM_COUNT];
}
/** @type {bigint} */
get uniOutStreamCount() {
return this.#handle[IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT];
}
/** @type {bigint} */
get maxBytesInFlights() {
return this.#handle[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
}
/** @type {bigint} */
get bytesInFlight() {
return this.#handle[IDX_STATS_SESSION_BYTES_IN_FLIGHT];
}
/** @type {bigint} */
get blockCount() {
return this.#handle[IDX_STATS_SESSION_BLOCK_COUNT];
}
/** @type {bigint} */
get cwnd() {
return this.#handle[IDX_STATS_SESSION_CWND];
}
/** @type {bigint} */
get latestRtt() {
return this.#handle[IDX_STATS_SESSION_LATEST_RTT];
}
/** @type {bigint} */
get minRtt() {
return this.#handle[IDX_STATS_SESSION_MIN_RTT];
}
/** @type {bigint} */
get rttVar() {
return this.#handle[IDX_STATS_SESSION_RTTVAR];
}
/** @type {bigint} */
get smoothedRtt() {
return this.#handle[IDX_STATS_SESSION_SMOOTHED_RTT];
}
/** @type {bigint} */
get ssthresh() {
return this.#handle[IDX_STATS_SESSION_SSTHRESH];
}
/** @type {bigint} */
get datagramsReceived() {
return this.#handle[IDX_STATS_SESSION_DATAGRAMS_RECEIVED];
}
/** @type {bigint} */
get datagramsSent() {
return this.#handle[IDX_STATS_SESSION_DATAGRAMS_SENT];
}
/** @type {bigint} */
get datagramsAcknowledged() {
return this.#handle[IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED];
}
/** @type {bigint} */
get datagramsLost() {
return this.#handle[IDX_STATS_SESSION_DATAGRAMS_LOST];
}
toString() {
return JSONStringify(this.toJSON());
}
toJSON() {
return {
__proto__: null,
connected: this.isConnected,
// We need to convert the values to strings because JSON does not
// support BigInts.
createdAt: `${this.createdAt}`,
closingAt: `${this.closingAt}`,
destroyedAt: `${this.destroyedAt}`,
handshakeCompletedAt: `${this.handshakeCompletedAt}`,
handshakeConfirmedAt: `${this.handshakeConfirmedAt}`,
gracefulClosingAt: `${this.gracefulClosingAt}`,
bytesReceived: `${this.bytesReceived}`,
bytesSent: `${this.bytesSent}`,
bidiInStreamCount: `${this.bidiInStreamCount}`,
bidiOutStreamCount: `${this.bidiOutStreamCount}`,
uniInStreamCount: `${this.uniInStreamCount}`,
uniOutStreamCount: `${this.uniOutStreamCount}`,
maxBytesInFlights: `${this.maxBytesInFlights}`,
bytesInFlight: `${this.bytesInFlight}`,
blockCount: `${this.blockCount}`,
cwnd: `${this.cwnd}`,
latestRtt: `${this.latestRtt}`,
minRtt: `${this.minRtt}`,
rttVar: `${this.rttVar}`,
smoothedRtt: `${this.smoothedRtt}`,
ssthresh: `${this.ssthresh}`,
datagramsReceived: `${this.datagramsReceived}`,
datagramsSent: `${this.datagramsSent}`,
datagramsAcknowledged: `${this.datagramsAcknowledged}`,
datagramsLost: `${this.datagramsLost}`,
};
}
[kInspect](depth, options) {
if (depth < 0)
return this;
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
return `QuicSessionStats ${inspect({
connected: this.isConnected,
createdAt: this.createdAt,
closingAt: this.closingAt,
destroyedAt: this.destroyedAt,
handshakeCompletedAt: this.handshakeCompletedAt,
handshakeConfirmedAt: this.handshakeConfirmedAt,
gracefulClosingAt: this.gracefulClosingAt,
bytesReceived: this.bytesReceived,
bytesSent: this.bytesSent,
bidiInStreamCount: this.bidiInStreamCount,
bidiOutStreamCount: this.bidiOutStreamCount,
uniInStreamCount: this.uniInStreamCount,
uniOutStreamCount: this.uniOutStreamCount,
maxBytesInFlights: this.maxBytesInFlights,
bytesInFlight: this.bytesInFlight,
blockCount: this.blockCount,
cwnd: this.cwnd,
latestRtt: this.latestRtt,
minRtt: this.minRtt,
rttVar: this.rttVar,
smoothedRtt: this.smoothedRtt,
ssthresh: this.ssthresh,
datagramsReceived: this.datagramsReceived,
datagramsSent: this.datagramsSent,
datagramsAcknowledged: this.datagramsAcknowledged,
datagramsLost: this.datagramsLost,
}, opts)}`;
}
/**
* True if this QuicSessionStats object is still connected to the underlying
* Session stats source. If this returns false, then the stats object is
* no longer being updated and should be considered stale.
* @returns {boolean}
*/
get isConnected() {
return !this.#disconnected;
}
[kFinishClose]() {
// Snapshot the stats into a new BigUint64Array since the underlying
// buffer will be destroyed.
this.#handle = new BigUint64Array(this.#handle);
this.#disconnected = true;
}
}
class QuicStreamStats {
/** @type {BigUint64Array} */
#handle;
/** type {boolean} */
#disconnected = false;
/**
* @param {symbol} privateSymbol
* @param {ArrayBuffer} buffer
*/
constructor(privateSymbol, buffer) {
// We use the kPrivateConstructor symbol to restrict the ability to
// create new instances of QuicStreamStats to internal code.
if (privateSymbol !== kPrivateConstructor) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
if (!isArrayBuffer(buffer)) {
throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer);
}
this.#handle = new BigUint64Array(buffer);
}
/** @type {bigint} */
get createdAt() {
return this.#handle[IDX_STATS_STREAM_CREATED_AT];
}
/** @type {bigint} */
get openedAt() {
return this.#handle[IDX_STATS_STREAM_OPENED_AT];
}
/** @type {bigint} */
get receivedAt() {
return this.#handle[IDX_STATS_STREAM_RECEIVED_AT];
}
/** @type {bigint} */
get ackedAt() {
return this.#handle[IDX_STATS_STREAM_ACKED_AT];
}
/** @type {bigint} */
get destroyedAt() {
return this.#handle[IDX_STATS_STREAM_DESTROYED_AT];
}
/** @type {bigint} */
get bytesReceived() {
return this.#handle[IDX_STATS_STREAM_BYTES_RECEIVED];
}
/** @type {bigint} */
get bytesSent() {
return this.#handle[IDX_STATS_STREAM_BYTES_SENT];
}
/** @type {bigint} */
get maxOffset() {
return this.#handle[IDX_STATS_STREAM_MAX_OFFSET];
}
/** @type {bigint} */
get maxOffsetAcknowledged() {
return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_ACK];
}
/** @type {bigint} */
get maxOffsetReceived() {
return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_RECV];
}
/** @type {bigint} */
get finalSize() {
return this.#handle[IDX_STATS_STREAM_FINAL_SIZE];
}
toString() {
return JSONStringify(this.toJSON());
}
toJSON() {
return {
__proto__: null,
connected: this.isConnected,
// We need to convert the values to strings because JSON does not
// support BigInts.
createdAt: `${this.createdAt}`,
openedAt: `${this.openedAt}`,
receivedAt: `${this.receivedAt}`,
ackedAt: `${this.ackedAt}`,
destroyedAt: `${this.destroyedAt}`,
bytesReceived: `${this.bytesReceived}`,
bytesSent: `${this.bytesSent}`,
maxOffset: `${this.maxOffset}`,
maxOffsetAcknowledged: `${this.maxOffsetAcknowledged}`,
maxOffsetReceived: `${this.maxOffsetReceived}`,
finalSize: `${this.finalSize}`,
};
}
[kInspect](depth, options) {
if (depth < 0)
return this;
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
return `StreamStats ${inspect({
connected: this.isConnected,
createdAt: this.createdAt,
openedAt: this.openedAt,
receivedAt: this.receivedAt,
ackedAt: this.ackedAt,
destroyedAt: this.destroyedAt,
bytesReceived: this.bytesReceived,
bytesSent: this.bytesSent,
maxOffset: this.maxOffset,
maxOffsetAcknowledged: this.maxOffsetAcknowledged,
maxOffsetReceived: this.maxOffsetReceived,
finalSize: this.finalSize,
}, opts)}`;
}
/**
* True if this QuicStreamStats object is still connected to the underlying
* Stream stats source. If this returns false, then the stats object is
* no longer being updated and should be considered stale.
* @returns {boolean}
*/
get isConnected() {
return !this.#disconnected;
}
[kFinishClose]() {
// Snapshot the stats into a new BigUint64Array since the underlying
// buffer will be destroyed.
this.#handle = new BigUint64Array(this.#handle);
this.#disconnected = true;
}
}
module.exports = {
QuicEndpointStats,
QuicSessionStats,
QuicStreamStats,
};