current commit
260927.7 · perf_hooks: fix truncation of monitorEventLoopDelay() resolution
'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,
};