Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461// 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<number, number> { if (!(this instanceof Histogram)) throw invalidThis("Histogram"); return new Map(this[kCore].percentileEntries().map(([p, v]) => [p, Number(v)])); }
get percentilesBigInt(): Map<number, bigint> { 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<string, unknown> { 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<string, unknown> { if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidArgType(name, "of type object", value); return value as Record<string, unknown>;}
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;}