Files
James M Snell 2ce7a73fec perf_hooks: fix truncation of monitorEventLoopDelay() resolution
`IntervalHistogram` stored the interval as `int32_t`, so a resolution
above 2^31 - 1 ms wrapped: `resolution: 2 ** 32 + 1` sampled every
millisecond.

Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: OpenCode
PR-URL: https://github.com/nodejs/node/pull/66115
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
2026-09-27 08:10:15 +00:00

1051 lines
30 KiB
JavaScript

'use strict';
const {
BigInt,
Float64Array,
Map,
MapPrototypeEntries,
NumberIsNaN,
NumberMAX_SAFE_INTEGER,
ObjectFromEntries,
Symbol,
} = primordials;
const {
Histogram: _Histogram,
SlidingWindowHistogram: _SlidingWindowHistogram,
} = internalBinding('performance');
const {
customInspectSymbol: kInspect,
kEmptyObject,
} = require('internal/util');
const { inspect } = require('util');
const {
codes: {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_THIS,
ERR_OUT_OF_RANGE,
},
} = require('internal/errors');
const {
validateArray,
validateBoolean,
validateInteger,
validateNumber,
validateObject,
validateOneOf,
} = require('internal/validators');
const {
isUint8Array,
} = require('internal/util/types');
const kDestroy = Symbol('kDestroy');
const kHandle = Symbol('kHandle');
const kRecordable = Symbol('kRecordable');
const kSlidingWindowHandle = Symbol('kSlidingWindowHandle');
const kQrdeDequantizationModes = ['none', 'hdr', 'all'];
const kMaxSlidingWindowHistogramChunks = 1024;
const kMaxChunkDuration = 18_446_744_073_709;
const kMaxInt64 = 9_223_372_036_854_775_807n;
const {
kClone,
kDeserialize,
markTransferMode,
} = require('internal/worker/js_transferable');
function isHistogram(object) {
return object?.[kHandle] !== undefined;
}
const kSkipThrow = Symbol('kSkipThrow');
class Histogram {
constructor(skipThrowSymbol = undefined) {
if (skipThrowSymbol !== kSkipThrow) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
}
[kInspect](depth, options) {
if (depth < 0)
return this;
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
return `Histogram ${inspect({
min: this.min,
max: this.max,
mean: this.mean,
exceeds: this.exceeds,
stddev: this.stddev,
skewness: this.skewness,
kurtosis: this.kurtosis,
count: this.count,
percentiles: this.percentiles,
}, opts)}`;
}
/**
* @readonly
* @type {number}
*/
get count() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.count();
}
/**
* @readonly
* @type {bigint}
*/
get countBigInt() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.countBigInt();
}
/**
* Returns the probability that a recorded value will exceed `value`
* (the complement of the cumulative distribution function).
* @param {number} value
* @returns {number} A value between 0.0 and 1.0.
*/
ccdf(value) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateNumber(value, 'value');
return 1 - this[kHandle]?.cdf(value);
}
/**
* Returns the cumulative distribution function (CDF) value for the
* given value, representing the probability that a recorded value
* will be less than or equal to `value`.
* @param {number} value
* @returns {number} A value between 0.0 and 1.0.
*/
cdf(value) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateNumber(value, 'value');
return this[kHandle]?.cdf(value);
}
/**
* Returns the number of recorded values that fall within the
* equivalent value range of the given value.
* @param {number} value
* @returns {number}
*/
countAt(value) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateNumber(value, 'value');
return this[kHandle]?.countAt(value);
}
/**
* @readonly
* @type {number}
*/
get min() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.min();
}
/**
* @readonly
* @type {bigint}
*/
get minBigInt() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.minBigInt();
}
/**
* @readonly
* @type {number}
*/
get max() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.max();
}
/**
* @readonly
* @type {bigint}
*/
get maxBigInt() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.maxBigInt();
}
/**
* @readonly
* @type {number}
*/
get mean() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.mean();
}
/**
* Returns a confidence interval for the mean using Student's t-distribution.
* @param {{ confidence?: number }} [options]
* @returns {{ mean: number, lower: number, upper: number }}
*/
meanCI(options = kEmptyObject) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateObject(options, 'options');
const { confidence = 0.95 } = options;
validateNumber(confidence, 'options.confidence');
if (NumberIsNaN(confidence) || confidence <= 0 || confidence >= 1)
throw new ERR_OUT_OF_RANGE('options.confidence',
'> 0 && < 1', confidence);
const result = this[kHandle]?.meanCI(confidence);
if (result === undefined) return undefined;
return {
__proto__: null,
mean: result[0],
lower: result[1],
upper: result[2],
};
}
/**
* @readonly
* @type {number}
*/
get exceeds() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.exceeds();
}
/**
* @readonly
* @type {bigint}
*/
get exceedsBigInt() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.exceedsBigInt();
}
/**
* Serializes the histogram to a CBOR-encoded Uint8Array suitable for
* transmission or storage. The data can be reconstructed into a new
* histogram using `importHistogram()`.
* @returns {Uint8Array}
*/
export() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.export();
}
/**
* Returns the exponentially weighted moving average of recorded values.
* Only active when the histogram was created with a `halfLife` option.
* Returns 0 when EWMA is not enabled or no values have been recorded.
* @readonly
* @type {number}
*/
get ewmaMean() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.ewmaMean();
}
/**
* Returns the exponentially weighted moving standard deviation.
* Only active when the histogram was created with a `halfLife` option.
* Returns 0 when EWMA is not enabled or no values have been recorded.
* @readonly
* @type {number}
*/
get ewmaStddev() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.ewmaStddev();
}
/**
* Returns the EWMA-smoothed error rate: the probability of a recorded
* value exceeding the configured `threshold`. Only active when the
* histogram was created with both `halfLife` and `threshold` options.
* Returns 0 when not enabled or no values have been recorded.
* @readonly
* @type {number}
*/
get ewmaErrorRate() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.ewmaErrorRate();
}
/**
* Returns the SLO burn rate: how fast the error budget is being consumed.
* A burn rate of 1 means the budget will be exactly exhausted over the
* SLO window. A burn rate of 10 means it is being consumed 10x faster.
* Requires `halfLife` and `threshold` to be configured.
* @param {number} sloTarget - The SLO target as a fraction (e.g. 0.999
* for 99.9%).
* @returns {number}
*/
burnRate(sloTarget) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateNumber(sloTarget, 'sloTarget');
if (NumberIsNaN(sloTarget) || sloTarget <= 0 || sloTarget >= 1)
throw new ERR_OUT_OF_RANGE('sloTarget', '> 0 && < 1', sloTarget);
const errorRate = this[kHandle]?.ewmaErrorRate();
if (errorRate === undefined) return undefined;
const errorBudget = 1 - sloTarget;
return errorRate / errorBudget;
}
/**
* Returns the Kolmogorov-Smirnov test statistic comparing this
* histogram's distribution to another's. Returns a value between
* 0.0 (identical distributions) and 1.0 (completely disjoint).
* @param {Histogram} other
* @returns {number}
*/
ksTest(other) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
if (!isHistogram(other))
throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other);
return this[kHandle]?.ksTest(other[kHandle]);
}
/**
* Performs Welch's t-test comparing this histogram to another.
* Returns an object with the t-statistic, degrees of freedom,
* two-tailed p-value, and confidence interval on the difference
* of means.
* @param {Histogram} other
* @param {{ confidence?: number }} [options]
* @returns {{ tStatistic: number, degreesOfFreedom: number,
* pValue: number,
* confidenceInterval: { lower: number, upper: number } }}
*/
welchTest(other, options = kEmptyObject) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
if (!isHistogram(other))
throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other);
validateObject(options, 'options');
const { confidence = 0.95 } = options;
validateNumber(confidence, 'options.confidence');
if (NumberIsNaN(confidence) || confidence <= 0 || confidence >= 1)
throw new ERR_OUT_OF_RANGE('options.confidence',
'> 0 && < 1', confidence);
const result = this[kHandle]?.welchTest(other[kHandle], confidence);
if (result === undefined) return undefined;
return {
__proto__: null,
tStatistic: result[0],
degreesOfFreedom: result[1],
pValue: result[2],
confidenceInterval: {
__proto__: null,
lower: result[3],
upper: result[4],
},
};
}
/**
* Performs a Mann-Whitney U test comparing this histogram to
* another. Returns an object with the U statistic, z-score,
* and two-tailed p-value (normal approximation).
* @param {Histogram} other
* @returns {{ uStatistic: number, zScore: number, pValue: number }}
*/
mannWhitneyTest(other) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
if (!isHistogram(other))
throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other);
const result = this[kHandle]?.mannWhitneyTest(other[kHandle]);
if (result === undefined) return undefined;
return {
__proto__: null,
uStatistic: result[0],
zScore: result[1],
pValue: result[2],
};
}
/**
* Computes Cohen's d effect size comparing this histogram to
* another. Uses the pooled standard deviation. Positive values
* indicate this histogram has a higher mean.
* @param {Histogram} other
* @returns {number}
*/
cohensD(other) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
if (!isHistogram(other))
throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other);
return this[kHandle]?.cohensD(other[kHandle]);
}
/**
* Computes Cliff's delta comparing this histogram to another.
* Returns a value between -1 and 1. Positive values indicate
* this histogram tends to produce larger values.
* @param {Histogram} other
* @returns {number}
*/
cliffsD(other) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
if (!isHistogram(other))
throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other);
return this[kHandle]?.cliffsD(other[kHandle]);
}
/**
* Returns the excess kurtosis of the recorded values, a measure of
* the heaviness of the distribution's tails. A positive value indicates
* heavier tails (more outliers) than a normal distribution.
* @readonly
* @type {number}
*/
get kurtosis() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.kurtosis();
}
/**
* Returns a {Map} containing the histogram data bucketed into
* linearly-spaced intervals of `stepSize`.
* @param {number} stepSize The width of each linear bucket.
* @returns {Map<number,number>}
*/
linearBuckets(stepSize) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateInteger(stepSize, 'stepSize', 1);
const map = new Map();
this[kHandle]?.linearBuckets(stepSize, map);
return map;
}
/**
* Returns a {Map} containing the histogram data bucketed into
* logarithmically-spaced intervals.
* @param {number} firstBucket The value of the first bucket boundary.
* @param {number} base The logarithmic base for bucket width growth.
* @returns {Map<number,number>}
*/
logBuckets(firstBucket, base) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateInteger(firstBucket, 'firstBucket', 1);
validateNumber(base, 'base');
if (base <= 1)
throw new ERR_OUT_OF_RANGE('base', '> 1', base);
const map = new Map();
this[kHandle]?.logBuckets(firstBucket, base, map);
return map;
}
/**
* Returns the skewness of the recorded values, a measure of the
* asymmetry of the distribution. A positive value indicates a
* right-skewed distribution (longer right tail).
* @readonly
* @type {number}
*/
get skewness() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.skewness();
}
/**
* @readonly
* @type {number}
*/
get stddev() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.stddev();
}
/**
* @param {number} percentile
* @returns {number}
*/
percentile(percentile) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateNumber(percentile, 'percentile');
if (NumberIsNaN(percentile) || percentile <= 0 || percentile > 100)
throw new ERR_OUT_OF_RANGE('percentile', '> 0 && <= 100', percentile);
return this[kHandle]?.percentile(percentile);
}
/**
* @param {number} percentile
* @returns {bigint}
*/
percentileBigInt(percentile) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateNumber(percentile, 'percentile');
if (NumberIsNaN(percentile) || percentile <= 0 || percentile > 100)
throw new ERR_OUT_OF_RANGE('percentile', '> 0 && <= 100', percentile);
return this[kHandle]?.percentileBigInt(percentile);
}
/**
* Returns a confidence interval for the given percentile using the
* exact binomial method. The result contains the point estimate and
* the lower/upper bounds of the interval.
* @param {number} percentile
* @param {{ confidence?: number }} [options]
* @returns {{ value: number, lower: number, upper: number }}
*/
percentileCI(percentile, options = kEmptyObject) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateNumber(percentile, 'percentile');
if (NumberIsNaN(percentile) || percentile <= 0 || percentile > 100)
throw new ERR_OUT_OF_RANGE('percentile', '> 0 && <= 100', percentile);
validateObject(options, 'options');
const { confidence = 0.95 } = options;
validateNumber(confidence, 'options.confidence');
if (NumberIsNaN(confidence) || confidence <= 0 || confidence >= 1)
throw new ERR_OUT_OF_RANGE('options.confidence',
'> 0 && < 1', confidence);
const result = this[kHandle]?.percentileCI(percentile, confidence);
if (result === undefined) return undefined;
return {
__proto__: null,
value: result[0],
lower: result[1],
upper: result[2],
};
}
/**
* @readonly
* @type {Map<number,number>}
*/
get percentiles() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
const map = new Map();
this[kHandle]?.percentiles(map);
return map;
}
/**
* @readonly
* @type {Map<number,bigint>}
*/
get percentilesBigInt() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
const map = new Map();
this[kHandle]?.percentilesBigInt(map);
return map;
}
/**
* Returns a {Map} of values at the specified percentiles, computed
* in a single efficient pass over the histogram.
* @param {number[]} percentiles Array of percentile values (0, 100].
* @returns {Map<number,number>}
*/
percentilesAt(percentiles) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateArray(percentiles, 'percentiles');
for (let i = 0; i < percentiles.length; i++) {
validateNumber(percentiles[i], `percentiles[${i}]`);
if (NumberIsNaN(percentiles[i]) ||
percentiles[i] <= 0 || percentiles[i] > 100)
throw new ERR_OUT_OF_RANGE(
`percentiles[${i}]`, '> 0 && <= 100', percentiles[i]);
}
const sorted = [...percentiles].sort((a, b) => a - b);
const input = new Float64Array(sorted);
const map = new Map();
this[kHandle]?.percentilesAt(map, input);
return map;
}
/**
* Builds a quantile-respectful density estimate using Harrell-Davis
* quantiles. Values can be spread deterministically within their HDR
* equivalent-value ranges to reduce quantization artifacts.
* @param {{ bins?: number, probabilities?: number[],
* dequantize?: 'none'|'hdr'|'all', cache?: boolean }} [options]
* @returns {Promise<{
* probabilities: Float64Array,
* quantiles: Float64Array,
* densities: Float64Array,
* count: bigint,
* bucketCount: number,
* corrections: number,
* dequantize: 'none'|'hdr'|'all',
* }>}
*/
qrde(options = kEmptyObject) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
validateObject(options, 'options');
const { bins, probabilities, dequantize = 'hdr', cache = false } = options;
if (bins !== undefined && probabilities !== undefined) {
throw new ERR_INVALID_ARG_VALUE(
'options', options, '"bins" and "probabilities" are mutually exclusive');
}
let boundaries;
if (probabilities === undefined) {
const binCount = bins ?? 100;
validateInteger(binCount, 'options.bins', 1, 1000);
boundaries = new Float64Array(binCount + 1);
for (let i = 0; i <= binCount; i++) boundaries[i] = i / binCount;
} else {
validateArray(probabilities, 'options.probabilities', 2);
const length = probabilities.length;
if (length > 1001) {
throw new ERR_OUT_OF_RANGE(
'options.probabilities.length', '>= 2 && <= 1001', length);
}
boundaries = new Float64Array(length);
let previous = -1;
for (let i = 0; i < length; i++) {
const probability = probabilities[i];
validateNumber(probability, `options.probabilities[${i}]`, 0, 1);
if (probability <= previous) {
throw new ERR_INVALID_ARG_VALUE(
'options.probabilities', probabilities, 'must be strictly increasing');
}
boundaries[i] = probability;
previous = probability;
}
if (boundaries[0] !== 0 || boundaries[length - 1] !== 1) {
throw new ERR_INVALID_ARG_VALUE(
'options.probabilities', probabilities, 'must start with 0 and end with 1');
}
}
validateOneOf(dequantize,
'options.dequantize', kQrdeDequantizationModes);
validateBoolean(cache, 'options.cache');
let mode = 0;
if (dequantize === 'hdr') mode = 1;
else if (dequantize === 'all') mode = 2;
return this[kHandle]?.qrde(boundaries, mode, cache);
}
/**
* @returns {void}
*/
reset() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
this[kHandle]?.reset();
}
/**
* The number of times values have been removed from the histogram by
* `reset()` or `subtract()`.
* @readonly
* @type {number}
*/
get resetCount() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return this[kHandle]?.resetCount();
}
/**
* Returns a new, independent histogram containing a copy of this
* histogram's current state. Values cannot be recorded into the returned
* histogram.
* @returns {Histogram}
*/
snapshot() {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
return new ClonedHistogram(this[kHandle].snapshot());
}
/**
* Returns a new histogram containing the values recorded in this histogram
* after `other`, an earlier snapshot of it, was taken.
* @param {Histogram} other
* @returns {Histogram}
*/
diff(other) {
if (!isHistogram(this))
throw new ERR_INVALID_THIS('Histogram');
if (!isHistogram(other))
throw new ERR_INVALID_ARG_TYPE('other', 'Histogram', other);
return new ClonedHistogram(this[kHandle].diff(other[kHandle]));
}
[kClone]() {
const handle = this[kHandle];
return {
data: { handle },
deserializeInfo: 'internal/histogram:ClonedHistogram',
};
}
[kDeserialize]({ handle }) {
this[kHandle] = handle;
}
toJSON() {
const json = {
count: this.count,
min: this.min,
max: this.max,
mean: this.mean,
exceeds: this.exceeds,
stddev: this.stddev,
skewness: this.skewness,
kurtosis: this.kurtosis,
ewmaMean: this.ewmaMean,
ewmaStddev: this.ewmaStddev,
ewmaErrorRate: this.ewmaErrorRate,
percentiles: ObjectFromEntries(MapPrototypeEntries(this.percentiles)),
};
return json;
}
}
class RecordableHistogram extends Histogram {
constructor(skipThrowSymbol = undefined) {
if (skipThrowSymbol !== kSkipThrow) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
super(skipThrowSymbol);
}
/**
* @param {number|bigint} val
* @returns {void}
*/
record(val) {
if (this[kRecordable] === undefined)
throw new ERR_INVALID_THIS('RecordableHistogram');
if (typeof val === 'bigint') {
this[kHandle]?.record(val);
return;
}
validateInteger(val, 'val', 0);
this[kHandle]?.record(val);
}
/**
* @returns {void}
*/
recordDelta() {
if (this[kRecordable] === undefined)
throw new ERR_INVALID_THIS('RecordableHistogram');
this[kHandle]?.recordDelta();
}
/**
* Records a value with coordinated omission correction, backfilling
* intermediate values at `expectedInterval` steps between the last
* recorded value and `val`. This compensates for measurement gaps
* caused by the system being stalled.
* @param {number|bigint} val The amount to record.
* @param {number|bigint} expectedInterval The expected recording interval.
* @returns {void}
*/
recordCorrected(val, expectedInterval) {
if (this[kRecordable] === undefined)
throw new ERR_INVALID_THIS('RecordableHistogram');
if (typeof val === 'bigint') {
if (typeof expectedInterval !== 'bigint')
throw new ERR_INVALID_ARG_TYPE(
'expectedInterval', 'bigint', expectedInterval);
this[kHandle]?.recordCorrected(val, expectedInterval);
return;
}
validateInteger(val, 'val', 0);
validateInteger(expectedInterval, 'expectedInterval', 1);
this[kHandle]?.recordCorrected(val, expectedInterval);
}
/**
* Subtracts the values of `other` from this histogram. Both
* histograms must have compatible configurations. Counts that would
* become negative are clamped to zero.
* @param {RecordableHistogram} other
*/
subtract(other) {
if (this[kRecordable] === undefined)
throw new ERR_INVALID_THIS('RecordableHistogram');
if (other[kRecordable] === undefined)
throw new ERR_INVALID_ARG_TYPE('other', 'RecordableHistogram', other);
this[kHandle]?.subtract(other[kHandle]);
}
/**
* @param {RecordableHistogram} other
*/
add(other) {
if (this[kRecordable] === undefined)
throw new ERR_INVALID_THIS('RecordableHistogram');
if (other[kRecordable] === undefined)
throw new ERR_INVALID_ARG_TYPE('other', 'RecordableHistogram', other);
this[kHandle]?.add(other[kHandle]);
}
[kClone]() {
const handle = this[kHandle];
return {
data: { handle },
deserializeInfo: 'internal/histogram:ClonedRecordableHistogram',
};
}
[kDeserialize]({ handle }) {
this[kHandle] = handle;
}
}
class SlidingWindowHistogram {
constructor(skipThrowSymbol = undefined) {
if (skipThrowSymbol !== kSkipThrow) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
}
/**
* @param {number|bigint} val
* @returns {void}
*/
record(val) {
if (this[kSlidingWindowHandle] === undefined)
throw new ERR_INVALID_THIS('SlidingWindowHistogram');
if (typeof val === 'bigint') {
this[kSlidingWindowHandle].record(val);
return;
}
validateInteger(val, 'val', 0);
this[kSlidingWindowHandle].record(val);
}
/**
* @returns {Histogram}
*/
snapshot() {
if (this[kSlidingWindowHandle] === undefined)
throw new ERR_INVALID_THIS('SlidingWindowHistogram');
return new ClonedHistogram(this[kSlidingWindowHandle].snapshot());
}
/**
* @returns {void}
*/
reset() {
if (this[kSlidingWindowHandle] === undefined)
throw new ERR_INVALID_THIS('SlidingWindowHistogram');
this[kSlidingWindowHandle].reset();
}
}
function ClonedHistogram(handle) {
const histogram = new Histogram(kSkipThrow);
markTransferMode(histogram, true, false);
histogram[kHandle] = handle;
return histogram;
}
ClonedHistogram.prototype[kDeserialize] = () => { };
function ClonedRecordableHistogram(handle) {
const histogram = new RecordableHistogram(kSkipThrow);
markTransferMode(histogram, true, false);
histogram[kRecordable] = true;
histogram[kHandle] = handle;
histogram.constructor = RecordableHistogram;
return histogram;
}
ClonedRecordableHistogram.prototype[kDeserialize] = () => { };
function createRecordableHistogram(handle) {
return new ClonedRecordableHistogram(handle);
}
function validateHistogramOptions(lowest, highest, figures) {
if (typeof lowest !== 'bigint') {
validateInteger(lowest, 'options.lowest', 1, NumberMAX_SAFE_INTEGER);
} else if (lowest < 1n || lowest > kMaxInt64) {
throw new ERR_OUT_OF_RANGE(
'options.lowest', `>= 1n && <= ${kMaxInt64}n`, lowest);
}
if (typeof highest !== 'bigint') {
validateInteger(highest, 'options.highest', 1, NumberMAX_SAFE_INTEGER);
} else if (highest < 1n || highest > kMaxInt64) {
throw new ERR_OUT_OF_RANGE(
'options.highest', `>= 1n && <= ${kMaxInt64}n`, highest);
}
const minimumHighest = 2n *
(typeof lowest === 'bigint' ? lowest : BigInt(lowest));
const highestBigInt = typeof highest === 'bigint' ?
highest : BigInt(highest);
if (highestBigInt < minimumHighest) {
throw new ERR_OUT_OF_RANGE(
'options.highest', `>= 2 * options.lowest (${minimumHighest}n)`, highest);
}
validateInteger(figures, 'options.figures', 1, 5);
}
/**
* @param {{
* lowest? : number,
* highest? : number,
* figures? : number,
* halfLife? : number,
* threshold? : number
* }} [options]
* @returns {RecordableHistogram}
*/
function createHistogram(options = kEmptyObject) {
validateObject(options, 'options');
const {
lowest = 1,
highest = NumberMAX_SAFE_INTEGER,
figures = 3,
halfLife = 0,
threshold = 0,
} = options;
validateHistogramOptions(lowest, highest, figures);
validateNumber(halfLife, 'options.halfLife');
if (halfLife < 0)
throw new ERR_OUT_OF_RANGE('options.halfLife', '>= 0', halfLife);
validateNumber(threshold, 'options.threshold');
if (threshold < 0)
throw new ERR_OUT_OF_RANGE('options.threshold', '>= 0', threshold);
return createRecordableHistogram(
new _Histogram(lowest, highest, figures, halfLife, threshold));
}
/**
* @param {{
* chunks: number,
* chunkDuration? : number,
* recordsPerChunk? : number,
* lowest? : number|bigint,
* highest? : number|bigint,
* figures? : number,
* }} options
* @returns {SlidingWindowHistogram}
*/
function createSlidingWindowHistogram(options) {
validateObject(options, 'options');
const {
chunks,
chunkDuration,
recordsPerChunk,
lowest = 1,
highest = NumberMAX_SAFE_INTEGER,
figures = 3,
} = options;
validateInteger(
chunks, 'options.chunks', 1, kMaxSlidingWindowHistogramChunks);
validateHistogramOptions(lowest, highest, figures);
const timeBased = chunkDuration !== undefined;
if (timeBased === (recordsPerChunk !== undefined)) {
throw new ERR_INVALID_ARG_VALUE(
'options', options,
'must specify exactly one of "chunkDuration" or "recordsPerChunk"');
}
let rotateAt;
if (timeBased) {
validateInteger(
chunkDuration, 'options.chunkDuration', 1, kMaxChunkDuration);
rotateAt = BigInt(chunkDuration) * 1_000_000n;
} else {
validateInteger(
recordsPerChunk, 'options.recordsPerChunk', 1, NumberMAX_SAFE_INTEGER);
rotateAt = BigInt(recordsPerChunk);
}
const histogram = new SlidingWindowHistogram(kSkipThrow);
markTransferMode(histogram, false, false);
histogram[kSlidingWindowHandle] = new _SlidingWindowHistogram(
lowest, highest, figures, chunks, timeBased, rotateAt);
return histogram;
}
/**
* Reconstructs a histogram from a CBOR-encoded Uint8Array previously
* produced by `histogram.export()`.
* @param {Uint8Array} data
* @returns {RecordableHistogram}
*/
function importHistogram(data) {
if (!isUint8Array(data))
throw new ERR_INVALID_ARG_TYPE('data', 'Uint8Array', data);
return createRecordableHistogram(_Histogram.import(data));
}
module.exports = {
Histogram,
RecordableHistogram,
SlidingWindowHistogram,
ClonedHistogram,
ClonedRecordableHistogram,
isHistogram,
kDestroy,
kHandle,
kMaxInt64,
kSkipThrow,
createHistogram,
createSlidingWindowHistogram,
importHistogram,
validateHistogramOptions,
};