// perf_hooks.createHistogram(): Node backs RecordableHistogram with HdrHistogram_c // (deps/histogram/src/hdr_histogram.c), so percentile/mean/stddev are bucketed, not // exact — this ports that C library's bucket math (log-linear buckets picked from // `lowest`/`highest`/`figures`) rather than tracking raw samples, to get Node's exact // numbers. Counts and values use bigint throughout: `highest` defaults to // Number.MAX_SAFE_INTEGER and bucket-index arithmetic shifts past 32 bits, both of // which overflow plain JS bitwise operators. import { invalidArgType } from "./errors"; const INT64_MAX = 9223372036854775807n; interface BucketConfig { readonly unitMagnitude: number; readonly subBucketHalfCountMagnitude: number; readonly subBucketCount: number; readonly subBucketHalfCount: number; readonly bucketCount: number; readonly countsLen: number; } function outOfRange(name: string, range: string, received: unknown): RangeError { return Object.assign(new RangeError(`The value of "${name}" is out of range. It must be ${range}. Received ${received}`), { code: "ERR_OUT_OF_RANGE" }); } function validateInteger(value: unknown, name: string, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER): number { if (typeof value !== "number") throw invalidArgType(name, "of type number", value); if (!Number.isInteger(value)) throw outOfRange(name, "an integer", value); if (value < min || value > max) throw outOfRange(name, `>= ${min} && <= ${max}`, value); return value; } function validateNumber(value: unknown, name: string): number { if (typeof value !== "number") throw invalidArgType(name, "of type number", value); return value; } /** Leading-zero count of a non-negative 64-bit value, for HdrHistogram_c's * `get_bucket_index` (`64 - clz64(value | sub_bucket_mask)`, the smallest power of * two containing the value). bigint's own bit length makes this trivial in JS. */ function clz64(value: bigint): number { if (value <= 0n) return 64; return 64 - value.toString(2).length; } function calculateBucketConfig(lowest: bigint, highest: bigint, figures: number): BucketConfig { const largestValueWithSingleUnitResolution = 2n * 10n ** BigInt(figures); const subBucketCountMagnitude = Math.ceil(Math.log(Number(largestValueWithSingleUnitResolution)) / Math.log(2)); const subBucketHalfCountMagnitude = (subBucketCountMagnitude > 1 ? subBucketCountMagnitude : 1) - 1; const unitMagnitude = Math.floor(Math.log(Number(lowest)) / Math.log(2)); const subBucketCount = 2 ** (subBucketHalfCountMagnitude + 1); const subBucketHalfCount = subBucketCount / 2; let smallestUntrackable = BigInt(subBucketCount) << BigInt(unitMagnitude); let bucketCount = 1; while (smallestUntrackable <= highest) { if (smallestUntrackable > INT64_MAX / 2n) { bucketCount += 1; break; } smallestUntrackable <<= 1n; bucketCount++; } const countsLen = (bucketCount + 1) * (subBucketCount / 2); return { unitMagnitude, subBucketHalfCountMagnitude, subBucketCount, subBucketHalfCount, bucketCount, countsLen }; } /** The HdrHistogram_c core: log-linear buckets over [lowest, highest] holding * `figures` significant decimal digits of resolution, as a flat counts array (no * circular-buffer shifting — Node never shifts a plain RecordableHistogram). */ class HdrCore { readonly cfg: BucketConfig; readonly counts: BigInt64Array; totalCount = 0n; minValue = INT64_MAX; maxValue = 0n; lowest: bigint; highest: bigint; figures: number; constructor(lowest: bigint, highest: bigint, figures: number) { this.lowest = lowest; this.highest = highest; this.figures = figures; this.cfg = calculateBucketConfig(lowest, highest, figures); this.counts = new BigInt64Array(this.cfg.countsLen); } private getBucketIndex(value: bigint): number { const subBucketMask = BigInt(this.cfg.subBucketCount - 1) << BigInt(this.cfg.unitMagnitude); const pow2ceiling = 64 - clz64(value | subBucketMask); return pow2ceiling - this.cfg.unitMagnitude - (this.cfg.subBucketHalfCountMagnitude + 1); } private getSubBucketIndex(value: bigint, bucketIndex: number): number { return Number(value >> BigInt(bucketIndex + this.cfg.unitMagnitude)); } private countsIndex(bucketIndex: number, subBucketIndex: number): number { const bucketBaseIndex = (bucketIndex + 1) << this.cfg.subBucketHalfCountMagnitude; return bucketBaseIndex + (subBucketIndex - this.cfg.subBucketHalfCount); } private countsIndexFor(value: bigint): number { const bucketIndex = this.getBucketIndex(value); return this.countsIndex(bucketIndex, this.getSubBucketIndex(value, bucketIndex)); } /** The bucket's representative value (its floor) — what `hdr_value_at_index` * answers, and what the percentiles iterator exposes (unrounded further). */ valueAtIndex(index: number): bigint { let bucketIndex = (index >> this.cfg.subBucketHalfCountMagnitude) - 1; let subBucketIndex = (index & (this.cfg.subBucketHalfCount - 1)) + this.cfg.subBucketHalfCount; if (bucketIndex < 0) { subBucketIndex -= this.cfg.subBucketHalfCount; bucketIndex = 0; } return BigInt(subBucketIndex) << BigInt(bucketIndex + this.cfg.unitMagnitude); } private sizeOfEquivalentValueRange(value: bigint): bigint { const bucketIndex = this.getBucketIndex(value); const subBucketIndex = this.getSubBucketIndex(value, bucketIndex); const adjustedBucket = subBucketIndex >= this.cfg.subBucketCount ? bucketIndex + 1 : bucketIndex; return 1n << BigInt(this.cfg.unitMagnitude + adjustedBucket); } lowestEquivalentValue(value: bigint): bigint { const bucketIndex = this.getBucketIndex(value); const subBucketIndex = this.getSubBucketIndex(value, bucketIndex); return BigInt(subBucketIndex) << BigInt(bucketIndex + this.cfg.unitMagnitude); } highestEquivalentValue(value: bigint): bigint { return this.lowestEquivalentValue(value) + this.sizeOfEquivalentValueRange(value) - 1n; } medianEquivalentValue(value: bigint): bigint { return this.lowestEquivalentValue(value) + (this.sizeOfEquivalentValueRange(value) >> 1n); } /** `hdr_record_values`: false (never thrown) if value is negative or exceeds * `highest` — the caller counts that as an "exceeds" rather than recording it. */ recordValues(value: bigint, count: bigint): boolean { if (value < 0n || this.highest < value) return false; const index = this.countsIndexFor(value); if (index < 0 || this.cfg.countsLen <= index) return false; this.counts[index]! += count; this.totalCount += count; if (value < this.minValue && value !== 0n) this.minValue = value; if (value > this.maxValue) this.maxValue = value; return true; } reset(): void { this.counts.fill(0n); this.totalCount = 0n; this.minValue = INT64_MAX; this.maxValue = 0n; } min(): bigint { return this.counts[0]! > 0n ? 0n : (this.minValue === INT64_MAX ? INT64_MAX : this.lowestEquivalentValue(this.minValue)); } max(): bigint { return this.maxValue === 0n ? 0n : this.highestEquivalentValue(this.maxValue); } /** `hdr_mean`/`hdr_stddev`: averaged over each bucket's median-equivalent value * (not the raw recorded values, which the histogram never keeps). */ mean(): number { let total = 0, count = 0; for (let i = 0; i < this.cfg.countsLen && count < Number(this.totalCount); i++) { const c = this.counts[i]!; if (c !== 0n) { count += Number(c); total += Number(c) * Number(this.medianEquivalentValue(this.valueAtIndex(i))); } } return total / Number(this.totalCount); } stddev(): number { const mean = this.mean(); let geometricDevTotal = 0; for (let i = 0; i < this.cfg.countsLen; i++) { const c = this.counts[i]!; if (c !== 0n) { const dev = Number(this.medianEquivalentValue(this.valueAtIndex(i))) - mean; geometricDevTotal += dev * dev * Number(c); } } return Math.sqrt(geometricDevTotal / Number(this.totalCount)); } valueAtPercentile(percentile: number): bigint { const requested = percentile < 100 ? percentile : 100; const countAtPercentile = Math.max(1, Math.trunc((requested / 100) * Number(this.totalCount) + 0.5)); let countToIdx = 0n; let valueFromIdx = 0n; for (let i = 0; i < this.cfg.countsLen; i++) { countToIdx += this.counts[i]!; if (countToIdx >= countAtPercentile) { valueFromIdx = this.valueAtIndex(i); break; } } return percentile === 0 ? this.lowestEquivalentValue(valueFromIdx) : this.highestEquivalentValue(valueFromIdx); } /** `hdr_iter_percentile_init` + `percentile_iter_next` (ticks-per-half-distance 1, * as Node's own wrapper always passes): each entry is a percentile point and the * bucket's raw representative value at that point — NOT `highestEquivalentValue` * as `valueAtPercentile` answers for the same point (Node's own `percentiles` map * and `percentile()` genuinely disagree by a bucket-resolution amount; confirmed * under real Node). */ percentileEntries(): Array<[number, bigint]> { const entries: Array<[number, bigint]> = []; let countsIndex = -1; let cumulativeCount = 0n; let count = 0n; let value = 0n; let seenLastValue = false; let percentileToIterateTo = 0; const hasNext = () => cumulativeCount < this.totalCount; const moveNext = (): boolean => { countsIndex++; if (countsIndex >= this.cfg.countsLen) return false; count = this.counts[countsIndex]!; cumulativeCount += count; value = this.valueAtIndex(countsIndex); return true; }; const basicIterNext = (): boolean => hasNext() && countsIndex < this.cfg.countsLen && moveNext(); for (;;) { if (!hasNext()) { if (seenLastValue) break; seenLastValue = true; entries.push([100, value]); continue; } if (countsIndex === -1 && !basicIterNext()) break; let produced = false; do { const currentPercentile = (100 * Number(cumulativeCount)) / Number(this.totalCount); if (count !== 0n && percentileToIterateTo <= currentPercentile) { entries.push([percentileToIterateTo, value]); const temp = Math.floor(Math.log(100 / (100 - percentileToIterateTo)) / Math.log(2)) + 1; const halfDistance = 2 ** temp; percentileToIterateTo += 100 / halfDistance; produced = true; break; } } while (basicIterNext()); if (!produced) break; } return entries; } /** `hdr_add`: each of `other`'s recorded buckets, re-recorded against this * histogram's own bucket config (so two differently-configured histograms still * combine, just losing resolution to whichever is coarser). */ add(other: HdrCore): void { for (let i = 0; i < other.cfg.countsLen; i++) { const c = other.counts[i]!; if (c !== 0n) this.recordValues(other.valueAtIndex(i), c); } } } const kCore = Symbol("kCore"); const kCount = Symbol("kCount"); const kExceeds = Symbol("kExceeds"); const kPrevTime = Symbol("kPrevTime"); const kRecordable = Symbol("kRecordable"); function invalidThis(type: string): TypeError { return Object.assign(new TypeError(`Value of "this" must be of type ${type}`), { code: "ERR_INVALID_THIS" }); } function percentileArg(value: unknown): number { const n = validateNumber(value, "percentile"); if (Number.isNaN(n) || n <= 0 || n > 100) throw outOfRange("percentile", "> 0 && <= 100", n); return n; } /** Node's own `Histogram`/`RecordableHistogram` aren't exported from `node:perf_hooks` * either — only reachable via `createHistogram()`'s result (and its prototype * chain), which is why these aren't exported from this module. */ class Histogram { [kCore]!: HdrCore; get count(): number { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return Number(this[kCore].totalCount); } get countBigInt(): bigint { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return this[kCore].totalCount; } get min(): number { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return Number(this[kCore].min()); } get minBigInt(): bigint { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return this[kCore].min(); } get max(): number { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return Number(this[kCore].max()); } get maxBigInt(): bigint { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return this[kCore].max(); } get mean(): number { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return this[kCore].mean(); } get stddev(): number { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return this[kCore].stddev(); } get exceeds(): number { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return (this as unknown as RecordableHistogram)[kExceeds] ?? 0; } get exceedsBigInt(): bigint { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return BigInt((this as unknown as RecordableHistogram)[kExceeds] ?? 0); } percentile(percentile: unknown): number { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return Number(this[kCore].valueAtPercentile(percentileArg(percentile))); } percentileBigInt(percentile: unknown): bigint { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return this[kCore].valueAtPercentile(percentileArg(percentile)); } get percentiles(): Map { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return new Map(this[kCore].percentileEntries().map(([p, v]) => [p, Number(v)])); } get percentilesBigInt(): Map { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return new Map(this[kCore].percentileEntries()); } reset(): void { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); this[kCore].reset(); (this as unknown as RecordableHistogram)[kCount] = 0; (this as unknown as RecordableHistogram)[kExceeds] = 0; } toJSON(): Record { return { count: this.count, min: this.min, max: this.max, mean: this.mean, exceeds: this.exceeds, stddev: this.stddev, percentiles: Object.fromEntries(this.percentiles), }; } } class RecordableHistogram extends Histogram { [kCount] = 0; [kExceeds] = 0; [kPrevTime] = 0; [kRecordable] = true; record(val: unknown): void { if (!(this instanceof RecordableHistogram)) throw invalidThis("RecordableHistogram"); let value: bigint; if (typeof val === "bigint") { let lossless = true; if (val > 0x7fffffffffffffffn || val < -0x8000000000000000n) lossless = false; if (!lossless || val < 1n) throw Object.assign(new RangeError("value is out of range"), { code: "ERR_OUT_OF_RANGE" }); value = val; } else { value = BigInt(validateInteger(val, "val", 1)); } if (this[kCore].recordValues(value, 1n)) this[kCount]++; else this[kExceeds]++; } recordDelta(): void { if (!(this instanceof RecordableHistogram)) throw invalidThis("RecordableHistogram"); const now = performance.now() * 1e6; // emulate uv_hrtime()'s nanoseconds if (this[kPrevTime] > 0) { const delta = Math.max(0, Math.round(now - this[kPrevTime])); if (this[kCore].recordValues(BigInt(delta), 1n)) this[kCount]++; else this[kExceeds]++; } this[kPrevTime] = now; } add(other: unknown): void { if (!(this instanceof RecordableHistogram)) throw invalidThis("RecordableHistogram"); // Unguarded property read, same as Node: `other[kRecordable]` on null/undefined // throws the engine's own TypeError rather than a coded ERR_*, confirmed under // real Node — only a non-nullish non-RecordableHistogram gets the coded one. if ((other as RecordableHistogram)[kRecordable] === undefined) throw invalidArgType("other", "an instance of RecordableHistogram", other); const o = other as RecordableHistogram; this[kCore].add(o[kCore]); this[kCount] += o[kCount]; this[kExceeds] += o[kExceeds]; } } export interface CreateHistogramOptions { lowest?: number | bigint; highest?: number | bigint; figures?: number; } function validateObjectOption(value: unknown, name: string): Record { if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidArgType(name, "of type object", value); return value as Record; } export function createHistogram(options: CreateHistogramOptions = {}): RecordableHistogram { const opts = validateObjectOption(options, "options"); let lowest: number | bigint = (opts.lowest as number | bigint | undefined) ?? 1; let highest: number | bigint = (opts.highest as number | bigint | undefined) ?? Number.MAX_SAFE_INTEGER; const figures = validateInteger((opts.figures as number | undefined) ?? 3, "options.figures", 1, 5); if (typeof lowest !== "bigint") lowest = validateInteger(lowest, "options.lowest", 1, Number.MAX_SAFE_INTEGER); if (typeof highest !== "bigint") { // Node's own `2 * lowest` here, unguarded: if `lowest` came in as a bigint (and // `highest` didn't), this throws the engine's own "Cannot mix BigInt and other // types" TypeError rather than a coded ERR_* — confirmed under real Node. highest = validateInteger(highest, "options.highest", 2 * (lowest as unknown as number), Number.MAX_SAFE_INTEGER); } else if (highest < 2n * (lowest as unknown as bigint)) { throw Object.assign(new RangeError(`The property 'options.highest' is invalid. Received ${highest}n`), { code: "ERR_INVALID_ARG_VALUE" }); } const histogram = new RecordableHistogram(); histogram[kCore] = new HdrCore(BigInt(lowest), BigInt(highest), figures); return histogram; }