diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 0eca76ed9842..eb2076eb7921 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1868,6 +1868,45 @@ added: The number of samples recorded by the histogram. +### `histogram.ccdf(value)` + + + +* `value` {number} The value to query. +* Returns: {number} A probability between 0.0 and 1.0. + +Returns the complementary cumulative distribution function (CCDF) value +for the given value, representing the probability that a recorded value +will exceed `value`. Equivalent to `1 - histogram.cdf(value)`. + +### `histogram.cdf(value)` + + + +* `value` {number} The value to query. +* Returns: {number} A probability between 0.0 and 1.0. + +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`. This is the inverse operation of +`histogram.percentile()`. + +### `histogram.countAt(value)` + + + +* `value` {number} The value to query. +* Returns: {number} + +Returns the number of recorded values that fall within the equivalent +value range of the given value. + ### `histogram.exceeds` + +* `other` {Histogram} The histogram to compare against. +* Returns: {number} The KS D-statistic, between 0.0 and 1.0. + +Computes the Kolmogorov-Smirnov test statistic comparing this histogram's +distribution to `other`. A value of 0 indicates identical distributions; +values close to 1 indicate completely disjoint distributions. Useful for +detecting performance regressions by comparing before/after histograms. + +### `histogram.kurtosis` + + + +* Type: {number} + +The excess kurtosis of the recorded values. Measures the heaviness of the +distribution's tails relative to a normal distribution. Positive values +indicate heavier tails (more extreme outliers); negative values indicate +lighter tails. + +### `histogram.linearBuckets(stepSize)` + + + +* `stepSize` {number} The width of each linear bucket. +* Returns: {Map} A map of bucket boundary values to counts. + +Returns the histogram data rebucketed into linearly-spaced intervals +of `stepSize`. Useful for visualization and export. + +### `histogram.logBuckets(firstBucket, base)` + + + +* `firstBucket` {number} The value of the first bucket boundary. +* `base` {number} The logarithmic base for bucket width growth. Must be > 1. +* Returns: {Map} A map of bucket boundary values to counts. + +Returns the histogram data rebucketed into logarithmically-spaced +intervals, where each bucket's width is multiplied by `base`. +Useful for visualization and export. + ### `histogram.max` + +* `percentiles` {number\[]} An array of percentile values in the range (0, 100]. +* Returns: {Map} A map of percentile values to their corresponding histogram + values. + +Returns the values at the specified percentiles, computed in a single +efficient pass over the histogram data. More efficient than calling +`histogram.percentile()` multiple times. + ### `histogram.reset()` + +* Type: {number} + +The skewness of the recorded values. Measures the asymmetry of the +distribution. A positive value indicates a right-skewed distribution +(longer right tail, common for latency data); a negative value +indicates a left-skewed distribution. + ### `histogram.stddev` + +* `val` {number|bigint} The value to record. +* `expectedInterval` {number|bigint} The expected recording interval. + +Records a value with coordinated omission correction. When a system stall +prevents timely recording, this method backfills intermediate values at +`expectedInterval` steps between the previously recorded value and `val`. +This compensates for measurement gaps that would otherwise underrepresent +latency. + +### `histogram.subtract(other)` + + + +* `other` {RecordableHistogram} + +Subtracts the values of `other` from this histogram. Both histograms should +have compatible configurations. Bucket counts that would become negative +are clamped to zero. + +## Histogram analysis examples + +The `Histogram` class provides statistical analysis methods useful for +performance monitoring, SLO enforcement, and regression detection. + +### Distribution shape analysis + +```js +const { createHistogram } = require('node:perf_hooks'); + +const h = createHistogram(); + +// Simulate a right-skewed latency distribution +for (let i = 0; i < 1000; i++) { + h.record(Math.ceil(Math.random() * 100)); +} +// Add some outliers +for (let i = 0; i < 10; i++) { + h.record(500 + Math.ceil(Math.random() * 500)); +} + +console.log('Skewness:', h.skewness.toFixed(4)); // Positive = right-skewed +console.log('Kurtosis:', h.kurtosis.toFixed(4)); // Positive = heavy tails +``` + +### SLO monitoring with CDF + +```js +const { createHistogram } = require('node:perf_hooks'); + +const latency = createHistogram(); + +// Record request latencies (in nanoseconds)... + +// "What fraction of requests complete within 100ms?" +const withinSLO = latency.cdf(100_000_000); +console.log(`${(withinSLO * 100).toFixed(1)}% of requests within SLO`); + +// "What fraction of requests exceed 500ms?" +const violating = latency.ccdf(500_000_000); +console.log(`${(violating * 100).toFixed(1)}% of requests violating SLO`); +``` + +### Regression detection with KS test + +```js +const { createHistogram } = require('node:perf_hooks'); + +const baseline = createHistogram(); +const current = createHistogram(); + +// Record baseline and current latencies... + +// D-statistic: 0 = identical, 1 = completely different +const d = baseline.ksTest(current); +if (d > 0.1) { + console.log(`Possible regression detected (D=${d.toFixed(4)})`); +} +``` + +### Batch percentile queries + +```js +const { createHistogram } = require('node:perf_hooks'); + +const h = createHistogram(); +// Record values... + +// Efficiently query common monitoring percentiles in one pass +const p = h.percentilesAt([50, 75, 90, 95, 99, 99.9]); +console.log('p50:', p.get(50)); +console.log('p99:', p.get(99)); +``` + +### Snapshot diffing with subtract + +```js +const { createHistogram } = require('node:perf_hooks'); + +const total = createHistogram(); +const snapshot = createHistogram(); + +// Record values into total... +// Periodically snapshot for "last interval" analysis: +snapshot.add(total); + +// Later, take a new snapshot and diff: +const newSnapshot = createHistogram(); +newSnapshot.add(total); +newSnapshot.subtract(snapshot); +// newSnapshot now contains only the values recorded since the last snapshot +console.log('Recent p99:', newSnapshot.percentile(99)); +``` + ## Examples ### Measuring the duration of async operations diff --git a/lib/internal/histogram.js b/lib/internal/histogram.js index f2cf3835b9a6..c16c894dd147 100644 --- a/lib/internal/histogram.js +++ b/lib/internal/histogram.js @@ -1,13 +1,13 @@ 'use strict'; const { + ArrayIsArray, + Float64Array, Map, - MapPrototypeClear, MapPrototypeEntries, NumberIsNaN, NumberMAX_SAFE_INTEGER, ObjectFromEntries, - ReflectConstruct, Symbol, } = primordials; @@ -40,7 +40,6 @@ const { const kDestroy = Symbol('kDestroy'); const kHandle = Symbol('kHandle'); -const kMap = Symbol('kMap'); const kRecordable = Symbol('kRecordable'); const { @@ -77,6 +76,8 @@ class Histogram { mean: this.mean, exceeds: this.exceeds, stddev: this.stddev, + skewness: this.skewness, + kurtosis: this.kurtosis, count: this.count, percentiles: this.percentiles, }, opts)}`; @@ -102,6 +103,46 @@ class 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} @@ -172,6 +213,81 @@ class Histogram { return this[kHandle]?.exceedsBigInt(); } + /** + * 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]); + } + + /** + * 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} + */ + 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} + */ + 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} @@ -217,9 +333,9 @@ class Histogram { get percentiles() { if (!isHistogram(this)) throw new ERR_INVALID_THIS('Histogram'); - MapPrototypeClear(this[kMap]); - this[kHandle]?.percentiles(this[kMap]); - return this[kMap]; + const map = new Map(); + this[kHandle]?.percentiles(map); + return map; } /** @@ -229,9 +345,34 @@ class Histogram { get percentilesBigInt() { if (!isHistogram(this)) throw new ERR_INVALID_THIS('Histogram'); - MapPrototypeClear(this[kMap]); - this[kHandle]?.percentilesBigInt(this[kMap]); - return this[kMap]; + 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} + */ + percentilesAt(percentiles) { + if (!isHistogram(this)) + throw new ERR_INVALID_THIS('Histogram'); + if (!ArrayIsArray(percentiles)) + throw new ERR_INVALID_ARG_TYPE('percentiles', 'Array', 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; } /** @@ -263,6 +404,8 @@ class Histogram { mean: this.mean, exceeds: this.exceeds, stddev: this.stddev, + skewness: this.skewness, + kurtosis: this.kurtosis, percentiles: ObjectFromEntries(MapPrototypeEntries(this.percentiles)), }; } @@ -303,6 +446,44 @@ class RecordableHistogram extends Histogram { 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', 1); + 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 */ @@ -328,12 +509,10 @@ class RecordableHistogram extends Histogram { } function ClonedHistogram(handle) { - return ReflectConstruct( - function() { - markTransferMode(this, true, false); - this[kHandle] = handle; - this[kMap] = new Map(); - }, [], Histogram); + const histogram = new Histogram(kSkipThrow); + markTransferMode(histogram, true, false); + histogram[kHandle] = handle; + return histogram; } ClonedHistogram.prototype[kDeserialize] = () => { }; @@ -343,7 +522,6 @@ function ClonedRecordableHistogram(handle) { markTransferMode(histogram, true, false); histogram[kRecordable] = true; - histogram[kMap] = new Map(); histogram[kHandle] = handle; histogram.constructor = RecordableHistogram; @@ -391,6 +569,6 @@ module.exports = { isHistogram, kDestroy, kHandle, - kMap, + kSkipThrow, createHistogram, }; diff --git a/lib/internal/perf/event_loop_delay.js b/lib/internal/perf/event_loop_delay.js index ebf0017b70df..4d14182c83fa 100644 --- a/lib/internal/perf/event_loop_delay.js +++ b/lib/internal/perf/event_loop_delay.js @@ -1,7 +1,5 @@ 'use strict'; const { - ReflectConstruct, - SafeMap, Symbol, SymbolDispose, } = primordials; @@ -26,7 +24,7 @@ const { const { Histogram, kHandle, - kMap, + kSkipThrow, } = require('internal/histogram'); const { @@ -40,8 +38,11 @@ const { const kEnabled = Symbol('kEnabled'); class ELDHistogram extends Histogram { - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(skipThrowSymbol = undefined) { + if (skipThrowSymbol !== kSkipThrow) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } + super(skipThrowSymbol); } /** @@ -87,13 +88,11 @@ function monitorEventLoopDelay(options = kEmptyObject) { validateBoolean(samplePerIteration, 'options.samplePerIteration'); validateInteger(resolution, 'options.resolution', 1); - return ReflectConstruct( - function() { - markTransferMode(this, true, false); - this[kEnabled] = false; - this[kHandle] = createELDHistogram(resolution, samplePerIteration); - this[kMap] = new SafeMap(); - }, [], ELDHistogram); + const histogram = new ELDHistogram(kSkipThrow); + markTransferMode(histogram, true, false); + histogram[kEnabled] = false; + histogram[kHandle] = createELDHistogram(resolution, samplePerIteration); + return histogram; } module.exports = monitorEventLoopDelay; diff --git a/src/histogram-inl.h b/src/histogram-inl.h index 3b8712c87879..7c3545f53aad 100644 --- a/src/histogram-inl.h +++ b/src/histogram-inl.h @@ -10,57 +10,80 @@ namespace node { void Histogram::Reset() { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedWriteLock lock(mutex_); hdr_reset(histogram_.get()); exceeds_ = 0; - count_ = 0; prev_ = 0; } double Histogram::Add(const Histogram& other) { - Mutex::ScopedLock lock(mutex_); - count_ += other.count_; - exceeds_ += other.exceeds_; - if (other.prev_ > prev_) - prev_ = other.prev_; - return static_cast(hdr_add(histogram_.get(), other.histogram_.get())); + auto do_add = [&]() { + exceeds_ += other.exceeds_; + if (other.prev_ > prev_) prev_ = other.prev_; + // hdr_add merges all bucket counts and total_count internally. + return static_cast( + hdr_add(histogram_.get(), other.histogram_.get())); + }; + + // When adding a histogram to itself, a single write lock suffices. + if (this == &other) { + RwLock::ScopedWriteLock lock(mutex_); + return do_add(); + } + + // Write-lock this (modified), read-lock other (only read). + // Lock in pointer order to prevent deadlock. + if (this < &other) { + RwLock::ScopedWriteLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_add(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedWriteLock lock2(mutex_); + return do_add(); } size_t Histogram::Count() const { - Mutex::ScopedLock lock(mutex_); - return count_; + RwLock::ScopedReadLock lock(mutex_); + return static_cast(histogram_->total_count); +} + +size_t Histogram::Exceeds() const { + RwLock::ScopedReadLock lock(mutex_); + return exceeds_; } int64_t Histogram::Min() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_min(histogram_.get()); } int64_t Histogram::Max() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_max(histogram_.get()); } double Histogram::Mean() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_mean(histogram_.get()); } double Histogram::Stddev() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_stddev(histogram_.get()); } int64_t Histogram::Percentile(double percentile) const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); CHECK_GT(percentile, 0); CHECK_LE(percentile, 100); return hdr_value_at_percentile(histogram_.get(), percentile); } template -void Histogram::Percentiles(Iterator&& fn) { - Mutex::ScopedLock lock(mutex_); +void Histogram::Percentiles(Iterator&& fn) const { + RwLock::ScopedReadLock lock(mutex_); hdr_iter iter; hdr_iter_percentile_init(&iter, histogram_.get(), 1); while (hdr_iter_next(&iter)) { @@ -69,37 +92,66 @@ void Histogram::Percentiles(Iterator&& fn) { } } +int64_t Histogram::CountAt(int64_t value) const { + RwLock::ScopedReadLock lock(mutex_); + return hdr_count_at_value(histogram_.get(), value); +} + +bool Histogram::RecordCorrected(int64_t value, int64_t expected_interval) { + RwLock::ScopedWriteLock lock(mutex_); + bool recorded = + hdr_record_corrected_value(histogram_.get(), value, expected_interval); + if (!recorded) exceeds_++; + return recorded; +} + bool Histogram::Record(int64_t value) { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedWriteLock lock(mutex_); bool recorded = hdr_record_value(histogram_.get(), value); - if (!recorded) - exceeds_++; - else - count_++; + if (!recorded) exceeds_++; return recorded; } uint64_t Histogram::RecordDelta() { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedWriteLock lock(mutex_); uint64_t time = uv_hrtime(); int64_t delta = 0; if (prev_ > 0) { CHECK_GE(time, prev_); delta = time - prev_; - if (hdr_record_value(histogram_.get(), delta)) - count_++; - else - exceeds_++; + if (!hdr_record_value(histogram_.get(), delta)) exceeds_++; } prev_ = time; return delta; } size_t Histogram::GetMemorySize() const { - Mutex::ScopedLock lock(mutex_); + RwLock::ScopedReadLock lock(mutex_); return hdr_get_memory_size(histogram_.get()); } +template +void Histogram::LinearBuckets(int64_t step_size, Iterator&& fn) const { + RwLock::ScopedReadLock lock(mutex_); + hdr_iter iter; + hdr_iter_linear_init(&iter, histogram_.get(), step_size); + while (hdr_iter_next(&iter)) { + fn(iter.value, iter.specifics.linear.count_added_in_this_iteration_step); + } +} + +template +void Histogram::LogBuckets(int64_t first_bucket, + double log_base, + Iterator&& fn) const { + RwLock::ScopedReadLock lock(mutex_); + hdr_iter iter; + hdr_iter_log_init(&iter, histogram_.get(), first_bucket, log_base); + while (hdr_iter_next(&iter)) { + fn(iter.value, iter.specifics.log.count_added_in_this_iteration_step); + } +} + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/histogram.cc b/src/histogram.cc index 5dd82c305bf7..3aa451685e86 100644 --- a/src/histogram.cc +++ b/src/histogram.cc @@ -7,6 +7,8 @@ #include "node_external_reference.h" #include "util.h" +#include + namespace node { using v8::BigInt; @@ -52,6 +54,169 @@ void Histogram::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("histogram", GetMemorySize()); } +bool Histogram::IsCompatible(const Histogram& other) const { + return histogram_->counts_len == other.histogram_->counts_len && + histogram_->lowest_discernible_value == + other.histogram_->lowest_discernible_value && + histogram_->highest_trackable_value == + other.histogram_->highest_trackable_value && + histogram_->significant_figures == + other.histogram_->significant_figures; +} + +double Histogram::Cdf(int64_t value) const { + RwLock::ScopedReadLock lock(mutex_); + int64_t total = histogram_->total_count; + if (total == 0) return 0.0; + + hdr_iter iter; + hdr_iter_init(&iter, histogram_.get()); + while (hdr_iter_next(&iter)) { + if (iter.highest_equivalent_value >= value) { + return static_cast(iter.cumulative_count) / + static_cast(total); + } + // All recorded data accounted for; remaining buckets are empty. + if (iter.cumulative_count >= total) break; + } + return 1.0; +} + +double Histogram::Skewness() const { + RwLock::ScopedReadLock lock(mutex_); + int64_t total = histogram_->total_count; + if (total < 3) return 0.0; + + // Compute mean in one pass, then variance and skewness in a second + // pass. This avoids calling hdr_stddev (which internally recomputes + // hdr_mean), reducing the total from 4 iterations to 2. + double mean = hdr_mean(histogram_.get()); + + double m2 = 0.0; + double m3 = 0.0; + hdr_iter iter; + hdr_iter_recorded_init(&iter, histogram_.get()); + while (hdr_iter_next(&iter)) { + double dev = static_cast(hdr_median_equivalent_value( + histogram_.get(), iter.value)) - + mean; + double d2 = dev * dev; + m2 += static_cast(iter.count) * d2; + m3 += static_cast(iter.count) * d2 * dev; + } + + double n = static_cast(total); + double variance = m2 / n; + if (variance == 0.0) return 0.0; + double s3 = variance * std::sqrt(variance); // stddev^3 + return (m3 / n) / s3; +} + +double Histogram::Kurtosis() const { + RwLock::ScopedReadLock lock(mutex_); + int64_t total = histogram_->total_count; + if (total < 4) return 0.0; + + // Same single-pass approach as Skewness: compute mean first, then + // variance and excess kurtosis together in one iteration. + double mean = hdr_mean(histogram_.get()); + + double m2 = 0.0; + double m4 = 0.0; + hdr_iter iter; + hdr_iter_recorded_init(&iter, histogram_.get()); + while (hdr_iter_next(&iter)) { + double dev = static_cast(hdr_median_equivalent_value( + histogram_.get(), iter.value)) - + mean; + double d2 = dev * dev; + m2 += static_cast(iter.count) * d2; + m4 += static_cast(iter.count) * d2 * d2; + } + + double n = static_cast(total); + double variance = m2 / n; + if (variance == 0.0) return 0.0; + double s4 = variance * variance; // stddev^4 + return (m4 / n) / s4 - 3.0; +} + +double Histogram::Subtract(const Histogram& other) { + auto do_subtract = [&]() -> double { + int64_t dropped = 0; + int32_t len = + std::min(histogram_->counts_len, other.histogram_->counts_len); + for (int32_t i = 0; i < len; i++) { + int64_t count = histogram_->counts[i] - other.histogram_->counts[i]; + if (count < 0) { + dropped += -count; + count = 0; + } + histogram_->counts[i] = count; + } + hdr_reset_internal_counters(histogram_.get()); + exceeds_ = (exceeds_ > other.exceeds_) ? exceeds_ - other.exceeds_ : 0; + return static_cast(dropped); + }; + + if (this == &other) { + RwLock::ScopedWriteLock lock(mutex_); + return do_subtract(); + } + + if (this < &other) { + RwLock::ScopedWriteLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_subtract(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedWriteLock lock2(mutex_); + return do_subtract(); +} + +double Histogram::KsTest(const Histogram& other) const { + auto do_ks = [&]() -> double { + int64_t n1 = histogram_->total_count; + int64_t n2 = other.histogram_->total_count; + if (n1 == 0 || n2 == 0) return 0.0; + + double max_d = 0.0; + int64_t cum1 = 0, cum2 = 0; + int32_t len = + std::max(histogram_->counts_len, other.histogram_->counts_len); + + for (int32_t i = 0; i < len; i++) { + if (i < histogram_->counts_len) cum1 += histogram_->counts[i]; + if (i < other.histogram_->counts_len) cum2 += other.histogram_->counts[i]; + double cdf1 = static_cast(cum1) / static_cast(n1); + double cdf2 = static_cast(cum2) / static_cast(n2); + double d = cdf1 > cdf2 ? cdf1 - cdf2 : cdf2 - cdf1; + if (d > max_d) max_d = d; + } + return max_d; + }; + + if (this == &other) return 0.0; + + if (this < &other) { + RwLock::ScopedReadLock lock1(mutex_); + RwLock::ScopedReadLock lock2(other.mutex_); + return do_ks(); + } + + RwLock::ScopedReadLock lock1(other.mutex_); + RwLock::ScopedReadLock lock2(mutex_); + return do_ks(); +} + +void Histogram::PercentilesAt(const double* percentiles, + int64_t* values, + size_t length) const { + RwLock::ScopedReadLock lock(mutex_); + hdr_value_at_percentiles(histogram_.get(), percentiles, values, length); +} + HistogramImpl::HistogramImpl(const Histogram::Options& options) : histogram_(new Histogram(options)) {} @@ -74,6 +239,14 @@ CFunction HistogramImpl::fast_get_stddev_( CFunction::Make(&HistogramImpl::FastGetStddev)); CFunction HistogramImpl::fast_get_percentile_( CFunction::Make(&HistogramImpl::FastGetPercentile)); +CFunction HistogramImpl::fast_get_skewness_( + CFunction::Make(&HistogramImpl::FastGetSkewness)); +CFunction HistogramImpl::fast_get_kurtosis_( + CFunction::Make(&HistogramImpl::FastGetKurtosis)); +CFunction HistogramImpl::fast_get_cdf_( + CFunction::Make(&HistogramImpl::FastGetCdf)); +CFunction HistogramImpl::fast_get_count_at_( + CFunction::Make(&HistogramImpl::FastGetCountAt)); CFunction HistogramBase::fast_record_( CFunction::Make(&HistogramBase::FastRecord)); CFunction HistogramBase::fast_record_delta_( @@ -112,6 +285,17 @@ void HistogramImpl::AddMethods(Isolate* isolate, Local tmpl) { isolate, instance, "stddev", GetStddev, &fast_get_stddev_); SetFastMethodNoSideEffect( isolate, instance, "percentile", GetPercentile, &fast_get_percentile_); + SetFastMethodNoSideEffect( + isolate, instance, "skewness", GetSkewness, &fast_get_skewness_); + SetFastMethodNoSideEffect( + isolate, instance, "kurtosis", GetKurtosis, &fast_get_kurtosis_); + SetFastMethodNoSideEffect(isolate, instance, "cdf", GetCdf, &fast_get_cdf_); + SetFastMethodNoSideEffect( + isolate, instance, "countAt", GetCountAt, &fast_get_count_at_); + SetProtoMethodNoSideEffect(isolate, tmpl, "ksTest", GetKsTest); + SetProtoMethodNoSideEffect(isolate, tmpl, "percentilesAt", GetPercentilesAt); + SetProtoMethodNoSideEffect(isolate, tmpl, "linearBuckets", GetLinearBuckets); + SetProtoMethodNoSideEffect(isolate, tmpl, "logBuckets", GetLogBuckets); SetFastMethod(isolate, instance, "reset", DoReset, &fast_reset_); } @@ -142,6 +326,18 @@ void HistogramImpl::RegisterExternalReferences( registry->Register(fast_get_exceeds_); registry->Register(fast_get_stddev_); registry->Register(fast_get_percentile_); + registry->Register(GetSkewness); + registry->Register(GetKurtosis); + registry->Register(GetCdf); + registry->Register(GetCountAt); + registry->Register(GetKsTest); + registry->Register(GetPercentilesAt); + registry->Register(GetLinearBuckets); + registry->Register(GetLogBuckets); + registry->Register(fast_get_skewness_); + registry->Register(fast_get_kurtosis_); + registry->Register(fast_get_cdf_); + registry->Register(fast_get_count_at_); is_registered = true; } @@ -223,6 +419,39 @@ void HistogramBase::Add(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(count); } +void HistogramBase::Subtract(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); + + CHECK(GetConstructorTemplate(env->isolate_data())->HasInstance(args[0])); + HistogramBase* other; + ASSIGN_OR_RETURN_UNWRAP(&other, args[0]); + + double dropped = (*histogram)->Subtract(*(other->histogram())); + args.GetReturnValue().Set(dropped); +} + +void HistogramBase::RecordCorrected(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_IMPLIES(!args[0]->IsNumber(), args[0]->IsBigInt()); + CHECK_IMPLIES(!args[1]->IsNumber(), args[1]->IsBigInt()); + bool lossless = true; + int64_t value = args[0]->IsBigInt() + ? args[0].As()->Int64Value(&lossless) + : static_cast(args[0].As()->Value()); + if (!lossless || value < 1) + return THROW_ERR_OUT_OF_RANGE(env, "value is out of range"); + int64_t expected_interval = + args[1]->IsBigInt() ? args[1].As()->Int64Value(&lossless) + : static_cast(args[1].As()->Value()); + if (!lossless || expected_interval < 1) + return THROW_ERR_OUT_OF_RANGE(env, "expected_interval is out of range"); + HistogramBase* histogram; + ASSIGN_OR_RETURN_UNWRAP(&histogram, args.This()); + (*histogram)->RecordCorrected(value, expected_interval); +} + BaseObjectPtr HistogramBase::Create( Environment* env, const Histogram::Options& options) { @@ -261,18 +490,22 @@ void HistogramBase::New(const FunctionCallbackInfo& args) { int64_t lowest = 1; int64_t highest = std::numeric_limits::max(); - bool lossless_ignored; + bool lossless = true; if (args[0]->IsNumber()) { lowest = args[0].As()->Value(); } else if (args[0]->IsBigInt()) { - lowest = args[0].As()->Int64Value(&lossless_ignored); + lowest = args[0].As()->Int64Value(&lossless); + if (!lossless) + return THROW_ERR_OUT_OF_RANGE(env, "options.lowest is out of range"); } if (args[1]->IsNumber()) { highest = args[1].As()->Value(); } else if (args[1]->IsBigInt()) { - highest = args[1].As()->Int64Value(&lossless_ignored); + highest = args[1].As()->Int64Value(&lossless); + if (!lossless) + return THROW_ERR_OUT_OF_RANGE(env, "options.highest is out of range"); } int32_t figures = args[2].As()->Value(); @@ -295,6 +528,8 @@ Local HistogramBase::GetConstructorTemplate( SetFastMethod( isolate, instance, "recordDelta", RecordDelta, &fast_record_delta_); SetProtoMethod(isolate, tmpl, "add", Add); + SetProtoMethod(isolate, tmpl, "subtract", Subtract); + SetProtoMethod(isolate, tmpl, "recordCorrected", RecordCorrected); HistogramImpl::AddMethods(isolate, tmpl); isolate_data->set_histogram_ctor_template(tmpl); } @@ -305,8 +540,10 @@ void HistogramBase::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(New); registry->Register(Add); + registry->Register(Subtract); registry->Register(Record); registry->Register(RecordDelta); + registry->Register(RecordCorrected); registry->Register(fast_record_); registry->Register(fast_record_delta_); HistogramImpl::RegisterExternalReferences(registry); @@ -345,11 +582,7 @@ Local IntervalHistogram::GetConstructorTemplate( tmpl = NewFunctionTemplate(isolate, nullptr); tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram")); - auto instance = tmpl->InstanceTemplate(); - instance->SetInternalFieldCount(IntervalHistogram::kInternalFieldCount); - HistogramImpl::AddMethods(isolate, tmpl); - SetFastMethod(isolate, instance, "start", Start, &fast_start_); - SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_); + InitTemplate(isolate, tmpl, IntervalHistogram::kInternalFieldCount); env->set_intervalhistogram_constructor_template(tmpl); } return tmpl; @@ -364,21 +597,16 @@ void IntervalHistogram::RegisterExternalReferences( HistogramImpl::RegisterExternalReferences(registry); } -IntervalHistogram::IntervalHistogram( - Environment* env, - Local wrap, - AsyncWrap::ProviderType type, - int32_t interval, - std::function on_interval, - const Histogram::Options& options) - : HandleWrap( - env, - wrap, - reinterpret_cast(&timer_), - type), +IntervalHistogram::IntervalHistogram(Environment* env, + Local wrap, + AsyncWrap::ProviderType type, + int32_t interval, + OnInterval on_interval, + const Histogram::Options& options) + : HandleWrap(env, wrap, reinterpret_cast(&timer_), type), HistogramImpl(options), interval_(interval), - on_interval_(std::move(on_interval)) { + on_interval_(on_interval) { MakeWeak(); wrap->SetAlignedPointerInInternalField( HistogramImpl::InternalFields::kImplField, @@ -390,8 +618,9 @@ IntervalHistogram::IntervalHistogram( BaseObjectPtr IntervalHistogram::Create( Environment* env, int32_t interval, - std::function on_interval, - const Histogram::Options& options) { + OnInterval on_interval, + const Histogram::Options& options, + AsyncWrap::ProviderType type) { Local obj; if (!GetConstructorTemplate(env) ->InstanceTemplate() @@ -400,12 +629,7 @@ BaseObjectPtr IntervalHistogram::Create( } return MakeBaseObject( - env, - obj, - AsyncWrap::PROVIDER_ELDHISTOGRAM, - interval, - std::move(on_interval), - options); + env, obj, type, interval, on_interval, options); } void IntervalHistogram::TimerCB(uv_timer_t* handle) { @@ -436,19 +660,11 @@ void IntervalHistogram::OnStop() { uv_timer_stop(&timer_); } -void IntervalHistogram::Start(const FunctionCallbackInfo& args) { - StartHandleHistogram(args.This(), args[0]->IsTrue()); -} - void IntervalHistogram::FastStart(Local receiver, bool reset) { TRACK_V8_FAST_API_CALL("histogram.start"); StartHandleHistogram(receiver, reset); } -void IntervalHistogram::Stop(const FunctionCallbackInfo& args) { - StopHandleHistogram(args.This()); -} - void IntervalHistogram::FastStop(Local receiver) { TRACK_V8_FAST_API_CALL("histogram.stop"); StopHandleHistogram(receiver); @@ -462,11 +678,7 @@ Local IterationHistogram::GetConstructorTemplate( tmpl = NewFunctionTemplate(isolate, nullptr); tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Histogram")); - auto instance = tmpl->InstanceTemplate(); - instance->SetInternalFieldCount(IterationHistogram::kInternalFieldCount); - HistogramImpl::AddMethods(isolate, tmpl); - SetFastMethod(isolate, instance, "start", Start, &fast_start_); - SetFastMethod(isolate, instance, "stop", Stop, &fast_stop_); + InitTemplate(isolate, tmpl, IterationHistogram::kInternalFieldCount); env->set_iterationhistogram_constructor_template(tmpl); } return tmpl; @@ -497,11 +709,12 @@ IterationHistogram::IterationHistogram(Environment* env, uv_prepare_init(env->event_loop(), &prepare_handle_); uv_unref(reinterpret_cast(&check_handle_)); uv_unref(reinterpret_cast(&prepare_handle_)); - prepare_handle_.data = this; } BaseObjectPtr IterationHistogram::Create( - Environment* env, const Histogram::Options& options) { + Environment* env, + const Histogram::Options& options, + AsyncWrap::ProviderType type) { Local obj; if (!GetConstructorTemplate(env) ->InstanceTemplate() @@ -510,12 +723,12 @@ BaseObjectPtr IterationHistogram::Create( return nullptr; } - return MakeBaseObject( - env, obj, AsyncWrap::PROVIDER_ELDHISTOGRAM, options); + return MakeBaseObject(env, obj, type, options); } void IterationHistogram::PrepareCB(uv_prepare_t* handle) { - IterationHistogram* self = static_cast(handle->data); + IterationHistogram* self = + ContainerOf(&IterationHistogram::prepare_handle_, handle); if (!self->enabled_) return; self->prepare_time_ = uv_hrtime(); self->timeout_ = uv_backend_timeout(handle->loop); @@ -572,19 +785,11 @@ void IterationHistogram::Close(Local close_callback) { uv_close(reinterpret_cast(&prepare_handle_), nullptr); } -void IterationHistogram::Start(const FunctionCallbackInfo& args) { - StartHandleHistogram(args.This(), args[0]->IsTrue()); -} - void IterationHistogram::FastStart(Local receiver, bool reset) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.start"); StartHandleHistogram(receiver, reset); } -void IterationHistogram::Stop(const FunctionCallbackInfo& args) { - StopHandleHistogram(args.This()); -} - void IterationHistogram::FastStop(Local receiver) { TRACK_V8_FAST_API_CALL("histogram.eventLoopDelay.stop"); StopHandleHistogram(receiver); @@ -670,12 +875,19 @@ void HistogramImpl::GetPercentiles(const FunctionCallbackInfo& args) { HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); CHECK(args[0]->IsMap()); Local map = args[0].As(); - (*histogram)->Percentiles([map, env](double key, int64_t value) { - USE(map->Set( - env->context(), - Number::New(env->isolate(), key), - Number::New(env->isolate(), static_cast(value)))); + + // Collect percentile data under the histogram lock, then populate the + // V8 Map after releasing it to avoid V8 allocations under the lock. + std::vector> entries; + (*histogram)->Percentiles([&entries](double key, int64_t value) { + entries.emplace_back(key, value); }); + for (const auto& entry : entries) { + USE(map->Set( + env->context(), + Number::New(env->isolate(), entry.first), + Number::New(env->isolate(), static_cast(entry.second)))); + } } void HistogramImpl::GetPercentilesBigInt( @@ -684,12 +896,16 @@ void HistogramImpl::GetPercentilesBigInt( HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); CHECK(args[0]->IsMap()); Local map = args[0].As(); - (*histogram)->Percentiles([map, env](double key, int64_t value) { - USE(map->Set( - env->context(), - Number::New(env->isolate(), key), - BigInt::New(env->isolate(), value))); + + std::vector> entries; + (*histogram)->Percentiles([&entries](double key, int64_t value) { + entries.emplace_back(key, value); }); + for (const auto& entry : entries) { + USE(map->Set(env->context(), + Number::New(env->isolate(), entry.first), + BigInt::New(env->isolate(), entry.second))); + } } void HistogramImpl::DoReset(const FunctionCallbackInfo& args) { @@ -746,6 +962,129 @@ double HistogramImpl::FastGetPercentile(Local receiver, return static_cast((*histogram)->Percentile(percentile)); } +void HistogramImpl::GetSkewness(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + args.GetReturnValue().Set((*histogram)->Skewness()); +} + +double HistogramImpl::FastGetSkewness(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.skewness"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->Skewness(); +} + +void HistogramImpl::GetKurtosis(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + args.GetReturnValue().Set((*histogram)->Kurtosis()); +} + +double HistogramImpl::FastGetKurtosis(Local receiver) { + TRACK_V8_FAST_API_CALL("histogram.kurtosis"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->Kurtosis(); +} + +void HistogramImpl::GetCdf(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + int64_t value = static_cast(args[0].As()->Value()); + args.GetReturnValue().Set((*histogram)->Cdf(value)); +} + +double HistogramImpl::FastGetCdf(Local receiver, const int64_t value) { + TRACK_V8_FAST_API_CALL("histogram.cdf"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return (*histogram)->Cdf(value); +} + +void HistogramImpl::GetCountAt(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + int64_t value = static_cast(args[0].As()->Value()); + double count = static_cast((*histogram)->CountAt(value)); + args.GetReturnValue().Set(count); +} + +double HistogramImpl::FastGetCountAt(Local receiver, + const int64_t value) { + TRACK_V8_FAST_API_CALL("histogram.countAt"); + HistogramImpl* histogram = HistogramImpl::FromJSObject(receiver); + return static_cast((*histogram)->CountAt(value)); +} + +void HistogramImpl::GetKsTest(const FunctionCallbackInfo& args) { + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + HistogramImpl* other = HistogramImpl::FromJSObject(args[0]); + args.GetReturnValue().Set((*histogram)->KsTest(*(other->histogram()))); +} + +void HistogramImpl::GetPercentilesAt(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsMap()); + Local map = args[0].As(); + CHECK(args[1]->IsFloat64Array()); + Local input = args[1].As(); + size_t length = input->Length(); + auto backing = input->Buffer()->GetBackingStore(); + double* percentiles = reinterpret_cast( + static_cast(backing->Data()) + input->ByteOffset()); + + std::vector values(length); + (*histogram)->PercentilesAt(percentiles, values.data(), length); + + for (size_t i = 0; i < length; i++) { + USE(map->Set(env->context(), + Number::New(env->isolate(), percentiles[i]), + Number::New(env->isolate(), static_cast(values[i])))); + } +} + +void HistogramImpl::GetLinearBuckets(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsMap()); + int64_t step_size = static_cast(args[0].As()->Value()); + Local map = args[1].As(); + + std::vector> entries; + (*histogram) + ->LinearBuckets(step_size, [&entries](int64_t value, int64_t count) { + entries.emplace_back(value, count); + }); + for (const auto& entry : entries) { + USE(map->Set( + env->context(), + Number::New(env->isolate(), static_cast(entry.first)), + Number::New(env->isolate(), static_cast(entry.second)))); + } +} + +void HistogramImpl::GetLogBuckets(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HistogramImpl* histogram = HistogramImpl::FromJSObject(args.This()); + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsNumber()); + CHECK(args[2]->IsMap()); + int64_t first_bucket = static_cast(args[0].As()->Value()); + double log_base = args[1].As()->Value(); + Local map = args[2].As(); + + std::vector> entries; + (*histogram) + ->LogBuckets( + first_bucket, log_base, [&entries](int64_t value, int64_t count) { + entries.emplace_back(value, count); + }); + for (const auto& entry : entries) { + USE(map->Set( + env->context(), + Number::New(env->isolate(), static_cast(entry.first)), + Number::New(env->isolate(), static_cast(entry.second)))); + } +} + HistogramImpl* HistogramImpl::FromJSObject(Local value) { auto obj = value.As(); DCHECK_GE(obj->InternalFieldCount(), HistogramImpl::kInternalFieldCount); diff --git a/src/histogram.h b/src/histogram.h index b9f968e8347c..5fbffa2a4879 100644 --- a/src/histogram.h +++ b/src/histogram.h @@ -11,10 +11,7 @@ #include "uv.h" #include "v8.h" -#include #include -#include -#include namespace node { @@ -45,7 +42,7 @@ class Histogram : public MemoryRetainer { inline double Mean() const; inline double Stddev() const; inline int64_t Percentile(double percentile) const; - inline size_t Exceeds() const { return exceeds_; } + inline size_t Exceeds() const; inline size_t Count() const; inline uint64_t RecordDelta(); @@ -55,10 +52,31 @@ class Histogram : public MemoryRetainer { // Iterator is a function type that takes two doubles as argument, one for // percentile and one for the value at that percentile. template - inline void Percentiles(Iterator&& fn); + inline void Percentiles(Iterator&& fn) const; inline size_t GetMemorySize() const; + // Analysis methods + inline int64_t CountAt(int64_t value) const; + double Cdf(int64_t value) const; + double Skewness() const; + double Kurtosis() const; + double KsTest(const Histogram& other) const; + double Subtract(const Histogram& other); + void PercentilesAt(const double* percentiles, + int64_t* values, + size_t length) const; + + inline bool RecordCorrected(int64_t value, int64_t expected_interval); + + template + void LinearBuckets(int64_t step_size, Iterator&& fn) const; + + template + void LogBuckets(int64_t first_bucket, double log_base, Iterator&& fn) const; + + bool IsCompatible(const Histogram& other) const; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Histogram) SET_SELF_SIZE(Histogram) @@ -68,8 +86,7 @@ class Histogram : public MemoryRetainer { HistogramPointer histogram_; uint64_t prev_ = 0; size_t exceeds_ = 0; - size_t count_ = 0; - Mutex mutex_; + RwLock mutex_; }; class HistogramImpl { @@ -106,6 +123,15 @@ class HistogramImpl { static void GetPercentilesBigInt( const v8::FunctionCallbackInfo& args); + static void GetSkewness(const v8::FunctionCallbackInfo& args); + static void GetKurtosis(const v8::FunctionCallbackInfo& args); + static void GetCdf(const v8::FunctionCallbackInfo& args); + static void GetCountAt(const v8::FunctionCallbackInfo& args); + static void GetKsTest(const v8::FunctionCallbackInfo& args); + static void GetPercentilesAt(const v8::FunctionCallbackInfo& args); + static void GetLinearBuckets(const v8::FunctionCallbackInfo& args); + static void GetLogBuckets(const v8::FunctionCallbackInfo& args); + static void FastReset(v8::Local receiver); static double FastGetCount(v8::Local receiver); static double FastGetMin(v8::Local receiver); @@ -115,6 +141,11 @@ class HistogramImpl { static double FastGetStddev(v8::Local receiver); static double FastGetPercentile(v8::Local receiver, const double percentile); + static double FastGetSkewness(v8::Local receiver); + static double FastGetKurtosis(v8::Local receiver); + static double FastGetCdf(v8::Local receiver, const int64_t value); + static double FastGetCountAt(v8::Local receiver, + const int64_t value); static void AddMethods(v8::Isolate* isolate, v8::Local tmpl); @@ -134,6 +165,10 @@ class HistogramImpl { static v8::CFunction fast_get_exceeds_; static v8::CFunction fast_get_stddev_; static v8::CFunction fast_get_percentile_; + static v8::CFunction fast_get_skewness_; + static v8::CFunction fast_get_kurtosis_; + static v8::CFunction fast_get_cdf_; + static v8::CFunction fast_get_count_at_; }; class HistogramBase final : public BaseObject, public HistogramImpl { @@ -165,7 +200,9 @@ class HistogramBase final : public BaseObject, public HistogramImpl { static void Record(const v8::FunctionCallbackInfo& args); static void RecordDelta(const v8::FunctionCallbackInfo& args); + static void RecordCorrected(const v8::FunctionCallbackInfo& args); static void Add(const v8::FunctionCallbackInfo& args); + static void Subtract(const v8::FunctionCallbackInfo& args); static void FastRecord(v8::Local receiver, const int64_t value); static void FastRecordDelta(v8::Local receiver); @@ -211,17 +248,48 @@ class HistogramBase final : public BaseObject, public HistogramImpl { static v8::CFunction fast_record_delta_; }; -class IntervalHistogram final : public HandleWrap, public HistogramImpl { +// CRTP mixin for HandleWrap-based histograms with start/stop support. +// Provides: StartFlags enum, Start/Stop slow-path handlers, enabled_ flag, +// and InitTemplate (shared GetConstructorTemplate body). +// Derived must provide: fast_start_, fast_stop_ (static CFunction), +// FastStart, FastStop, OnStart, OnStop. +template +class HandleHistogramMixin { + public: + enum class StartFlags { NONE, RESET }; + + static void Start(const v8::FunctionCallbackInfo& args) { + StartHandleHistogram(args.This(), args[0]->IsTrue()); + } + + static void Stop(const v8::FunctionCallbackInfo& args) { + StopHandleHistogram(args.This()); + } + + protected: + static void InitTemplate(v8::Isolate* isolate, + v8::Local tmpl, + uint32_t internal_field_count) { + auto instance = tmpl->InstanceTemplate(); + instance->SetInternalFieldCount(internal_field_count); + HistogramImpl::AddMethods(isolate, tmpl); + SetFastMethod(isolate, instance, "start", Start, &Derived::fast_start_); + SetFastMethod(isolate, instance, "stop", Stop, &Derived::fast_stop_); + } + + bool enabled_ = false; +}; + +class IntervalHistogram final : public HandleWrap, + public HistogramImpl, + public HandleHistogramMixin { public: enum InternalFields { kInternalFieldCount = std::max( HandleWrap::kInternalFieldCount, HistogramImpl::kInternalFieldCount), }; - enum class StartFlags { - NONE, - RESET - }; + using OnInterval = void (*)(Histogram&); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); @@ -231,19 +299,16 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl { static BaseObjectPtr Create( Environment* env, int32_t interval, - std::function on_interval, - const Histogram::Options& options); - - IntervalHistogram( - Environment* env, - v8::Local wrap, - AsyncWrap::ProviderType type, - int32_t interval, - std::function on_interval, - const Histogram::Options& options = Histogram::Options {}); + OnInterval on_interval, + const Histogram::Options& options, + AsyncWrap::ProviderType type = AsyncWrap::PROVIDER_ELDHISTOGRAM); - static void Start(const v8::FunctionCallbackInfo& args); - static void Stop(const v8::FunctionCallbackInfo& args); + IntervalHistogram(Environment* env, + v8::Local wrap, + AsyncWrap::ProviderType type, + int32_t interval, + OnInterval on_interval, + const Histogram::Options& options = Histogram::Options{}); static void FastStart(v8::Local receiver, bool reset); static void FastStop(v8::Local receiver); @@ -262,45 +327,45 @@ class IntervalHistogram final : public HandleWrap, public HistogramImpl { void OnStart(StartFlags flags = StartFlags::RESET); void OnStop(); + friend class HandleHistogramMixin; template friend void StartHandleHistogram(v8::Local, bool); template friend void StopHandleHistogram(v8::Local); - bool enabled_ = false; int32_t interval_ = 0; - std::function on_interval_; + OnInterval on_interval_ = nullptr; uv_timer_t timer_; static v8::CFunction fast_start_; static v8::CFunction fast_stop_; }; -class IterationHistogram final : public HandleWrap, public HistogramImpl { +class IterationHistogram final + : public HandleWrap, + public HistogramImpl, + public HandleHistogramMixin { public: enum InternalFields { kInternalFieldCount = std::max( HandleWrap::kInternalFieldCount, HistogramImpl::kInternalFieldCount), }; - enum class StartFlags { NONE, RESET }; - static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static v8::Local GetConstructorTemplate( Environment* env); static BaseObjectPtr Create( - Environment* env, const Histogram::Options& options); + Environment* env, + const Histogram::Options& options, + AsyncWrap::ProviderType type = AsyncWrap::PROVIDER_ELDHISTOGRAM); IterationHistogram(Environment* env, v8::Local wrap, AsyncWrap::ProviderType type, const Histogram::Options& options = Histogram::Options{}); - static void Start(const v8::FunctionCallbackInfo& args); - static void Stop(const v8::FunctionCallbackInfo& args); - static void FastStart(v8::Local receiver, bool reset); static void FastStop(v8::Local receiver); @@ -322,12 +387,12 @@ class IterationHistogram final : public HandleWrap, public HistogramImpl { void OnStart(StartFlags flags = StartFlags::RESET); void OnStop(); + friend class HandleHistogramMixin; template friend void StartHandleHistogram(v8::Local, bool); template friend void StopHandleHistogram(v8::Local); - bool enabled_ = false; uv_prepare_t prepare_handle_; uv_check_t check_handle_; uint64_t prepare_time_ = 0; diff --git a/test/parallel/test-perf-hooks-histogram-analysis.js b/test/parallel/test-perf-hooks-histogram-analysis.js new file mode 100644 index 000000000000..acc2b5a7eb2d --- /dev/null +++ b/test/parallel/test-perf-hooks-histogram-analysis.js @@ -0,0 +1,501 @@ +// Flags: --expose-internals --no-warnings --allow-natives-syntax +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { createHistogram } = require('perf_hooks'); +const { internalBinding } = require('internal/test/binding'); +const { inspect } = require('util'); + +// --------------------------------------------------------------------------- +// cdf(value) — cumulative distribution function +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Empty histogram returns 0 + assert.strictEqual(h.cdf(1), 0); + + for (let i = 1; i <= 5; i++) h.record(i); + + // Below min → 0 + assert.strictEqual(h.cdf(0), 0); + + // At or above some values → monotonically increasing + assert.ok(h.cdf(1) > 0); + assert.ok(h.cdf(3) >= h.cdf(1)); + assert.ok(h.cdf(5) >= h.cdf(3)); + + // Well above max → 1.0 + assert.strictEqual(h.cdf(1000000), 1.0); + + // Validation + assert.throws(() => h.cdf('hello'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.cdf(), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.cdf(undefined), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// ccdf(value) — complementary CDF = 1 - cdf +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Empty: cdf=0 so ccdf=1 + assert.strictEqual(h.ccdf(1), 1); + + for (let i = 1; i <= 5; i++) h.record(i); + + // CCDF + CDF === 1 for all values + for (const v of [0, 1, 3, 5, 1000000]) { + const sum = h.ccdf(v) + h.cdf(v); + assert.ok(Math.abs(sum - 1) < 1e-10, `ccdf(${v})+cdf(${v})=${sum}`); + } + + // Well above max → 0 + assert.strictEqual(h.ccdf(1000000), 0); + + // Validation + assert.throws(() => h.ccdf('hello'), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// countAt(value) — count in equivalent bucket +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Empty → 0 + assert.strictEqual(h.countAt(1), 0); + + h.record(1); + h.record(1); + h.record(1); + h.record(100); + + assert.strictEqual(h.countAt(1), 3); + assert.strictEqual(h.countAt(100), 1); + assert.strictEqual(h.countAt(999999), 0); + + // Validation + assert.throws(() => h.countAt('hello'), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.countAt(), { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// skewness getter +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Too few values returns 0 + assert.strictEqual(h.skewness, 0); + h.record(1); + assert.strictEqual(h.skewness, 0); + h.record(2); + assert.strictEqual(h.skewness, 0); + + // With 3+ values, returns a number + h.record(3); + assert.strictEqual(typeof h.skewness, 'number'); + assert.ok(!Number.isNaN(h.skewness)); + + // Right-skewed distribution → positive skewness + const right = createHistogram(); + for (let i = 0; i < 100; i++) right.record(1); + for (let i = 0; i < 10; i++) right.record(10000); + assert.ok(right.skewness > 0); + + // Appears in inspect output + assert.ok(inspect(right, { depth: null }).includes('skewness')); + + // Appears in toJSON + const json = right.toJSON(); + assert.ok('skewness' in json); + assert.strictEqual(typeof json.skewness, 'number'); + + // Uniform distribution: zero stddev → returns 0 + const uniform = createHistogram(); + for (let i = 0; i < 10; i++) uniform.record(1); + assert.strictEqual(uniform.skewness, 0); +} + +// --------------------------------------------------------------------------- +// kurtosis getter +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + // Too few values returns 0 + assert.strictEqual(h.kurtosis, 0); + h.record(1); + h.record(2); + h.record(3); + assert.strictEqual(h.kurtosis, 0); + + // With 4+ values, returns a number + h.record(4); + assert.strictEqual(typeof h.kurtosis, 'number'); + assert.ok(!Number.isNaN(h.kurtosis)); + + // Appears in inspect and toJSON + const h2 = createHistogram(); + for (let i = 1; i <= 100; i++) h2.record(i); + assert.ok(inspect(h2, { depth: null }).includes('kurtosis')); + const json = h2.toJSON(); + assert.ok('kurtosis' in json); + assert.strictEqual(typeof json.kurtosis, 'number'); + + // Uniform distribution: zero stddev → returns 0 + const uniform = createHistogram(); + for (let i = 0; i < 10; i++) uniform.record(1); + assert.strictEqual(uniform.kurtosis, 0); +} + +// --------------------------------------------------------------------------- +// ksTest(other) — Kolmogorov-Smirnov D-statistic +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + // Both empty → 0 + assert.strictEqual(h1.ksTest(h2), 0); + + // Identical distributions → 0 + for (let i = 1; i <= 100; i++) { h1.record(i); h2.record(i); } + assert.strictEqual(h1.ksTest(h2), 0); + + // Same histogram against itself → 0 + assert.strictEqual(h1.ksTest(h1), 0); + + // Different distributions → D > 0 + const h3 = createHistogram(); + for (let i = 1000; i <= 2000; i++) h3.record(i); + const d = h1.ksTest(h3); + assert.ok(d > 0); + assert.ok(d <= 1); + + // Symmetry: D(a,b) === D(b,a) + assert.strictEqual(h1.ksTest(h3), h3.ksTest(h1)); + + // Completely disjoint → D close to 1 + const hLow = createHistogram(); + const hHigh = createHistogram(); + for (let i = 0; i < 100; i++) hLow.record(1); + for (let i = 0; i < 100; i++) hHigh.record(100000); + assert.ok(hLow.ksTest(hHigh) > 0.9); + + // One empty → 0 + const empty = createHistogram(); + assert.strictEqual(h1.ksTest(empty), 0); + + // Validation: non-histogram throws + assert.throws(() => h1.ksTest('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.ksTest(42), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.ksTest({}), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// percentilesAt(percentiles) — batch percentile query +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + for (let i = 1; i <= 100; i++) h.record(i); + + // Returns a Map + const result = h.percentilesAt([50, 90, 99]); + assert.ok(result instanceof Map); + assert.strictEqual(result.size, 3); + + // Keys are the requested percentiles + assert.ok(result.has(50)); + assert.ok(result.has(90)); + assert.ok(result.has(99)); + + // Values match individual percentile() calls + assert.strictEqual(result.get(50), h.percentile(50)); + assert.strictEqual(result.get(90), h.percentile(90)); + assert.strictEqual(result.get(99), h.percentile(99)); + + // Single element + const single = h.percentilesAt([50]); + assert.strictEqual(single.size, 1); + + // Unsorted input still works (internally sorted) + const unsorted = h.percentilesAt([99, 50, 90]); + assert.strictEqual(unsorted.get(50), h.percentile(50)); + + // Validation + assert.throws(() => h.percentilesAt('not array'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.percentilesAt([0]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt([101]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt([NaN]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt([-1]), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.percentilesAt(['hello']), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// linearBuckets(stepSize) — linearly-spaced bucket iteration +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + for (let i = 1; i <= 100; i++) h.record(i); + + const buckets = h.linearBuckets(10); + assert.ok(buckets instanceof Map); + assert.ok(buckets.size > 0); + + // All keys and values are numbers + for (const [key, value] of buckets) { + assert.strictEqual(typeof key, 'number'); + assert.strictEqual(typeof value, 'number'); + assert.ok(value >= 0); + } + + // Total count across buckets equals histogram count + let total = 0; + for (const [, count] of buckets) total += count; + assert.strictEqual(total, h.count); + + // Different step sizes produce different bucket counts + const finer = h.linearBuckets(5); + assert.ok(finer.size >= buckets.size); + + // Validation + assert.throws(() => h.linearBuckets(0), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.linearBuckets(-1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.linearBuckets('hello'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.linearBuckets(1.5), { code: 'ERR_OUT_OF_RANGE' }); +} + +// --------------------------------------------------------------------------- +// logBuckets(firstBucket, base) — logarithmically-spaced bucket iteration +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + for (let i = 1; i <= 1000; i++) h.record(i); + + const buckets = h.logBuckets(1, 2); + assert.ok(buckets instanceof Map); + assert.ok(buckets.size > 0); + + for (const [key, value] of buckets) { + assert.strictEqual(typeof key, 'number'); + assert.strictEqual(typeof value, 'number'); + assert.ok(value >= 0); + } + + // Total count across buckets equals histogram count + let total = 0; + for (const [, count] of buckets) total += count; + assert.strictEqual(total, h.count); + + // Validation + assert.throws(() => h.logBuckets(0, 2), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(-1, 2), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(1, 1), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(1, 0.5), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets(1, -2), { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.logBuckets('hello', 2), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.logBuckets(1, 'hello'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.logBuckets(1.5, 2), { code: 'ERR_OUT_OF_RANGE' }); +} + +// --------------------------------------------------------------------------- +// subtract(other) — subtract histogram counts +// --------------------------------------------------------------------------- +{ + const h1 = createHistogram(); + const h2 = createHistogram(); + + for (let i = 1; i <= 10; i++) h1.record(i); + for (let i = 1; i <= 5; i++) h2.record(i); + + const countBefore = h1.count; + h1.subtract(h2); + + // Count should decrease + assert.ok(h1.count < countBefore); + + // Subtracting from self zeros out + const h3 = createHistogram(); + for (let i = 1; i <= 10; i++) h3.record(i); + h3.subtract(h3); + assert.strictEqual(h3.count, 0); + + // Clamping: subtracting more than present doesn't go negative + const hSmall = createHistogram(); + const hBig = createHistogram(); + hSmall.record(1); + for (let i = 0; i < 100; i++) hBig.record(1); + hSmall.subtract(hBig); + assert.strictEqual(hSmall.count, 0); + + // Validation + assert.throws(() => h1.subtract('not a histogram'), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.subtract(42), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h1.subtract({}), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// --------------------------------------------------------------------------- +// recordCorrected(val, expectedInterval) — coordinated omission correction +// --------------------------------------------------------------------------- +{ + // Basic recording with number args + const h = createHistogram(); + h.recordCorrected(100, 10); + assert.ok(h.count > 0); + + // Should record more values than a plain record (backfilling) + const hPlain = createHistogram(); + hPlain.record(100); + assert.ok(h.count > hPlain.count); + + // BigInt variant + const hBig = createHistogram(); + hBig.recordCorrected(100n, 10n); + assert.ok(hBig.count > 0); + + // Mixed types should throw (bigint val, number interval) + assert.throws(() => h.recordCorrected(100n, 10), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // Validation: non-integer + assert.throws(() => h.recordCorrected('hello', 10), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => h.recordCorrected(100, 'hello'), + { code: 'ERR_INVALID_ARG_TYPE' }); + + // Out of range + assert.throws(() => h.recordCorrected(0, 10), + { code: 'ERR_OUT_OF_RANGE' }); + assert.throws(() => h.recordCorrected(100, 0), + { code: 'ERR_OUT_OF_RANGE' }); +} + +// --------------------------------------------------------------------------- +// ERR_INVALID_THIS for all new methods on wrong receiver +// --------------------------------------------------------------------------- +{ + const { Histogram } = require('internal/histogram'); + const h = createHistogram(); + const wrongThis = {}; + + // Methods + const methods = [ + ['cdf', [1]], + ['ccdf', [1]], + ['countAt', [1]], + ['ksTest', [h]], + ['linearBuckets', [10]], + ['logBuckets', [1, 2]], + ['percentilesAt', [[50]]], + ]; + + for (const [method, args] of methods) { + assert.throws( + () => Histogram.prototype[method].call(wrongThis, ...args), + { code: 'ERR_INVALID_THIS' }, + `${method} should throw ERR_INVALID_THIS` + ); + } + + // Getters + for (const getter of ['skewness', 'kurtosis']) { + const desc = Object.getOwnPropertyDescriptor( + Histogram.prototype, getter); + assert.throws( + () => desc.get.call(wrongThis), + { code: 'ERR_INVALID_THIS' }, + `${getter} getter should throw ERR_INVALID_THIS` + ); + } +} + +// --------------------------------------------------------------------------- +// Empty histogram edge cases +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + + assert.strictEqual(h.cdf(1), 0); + assert.strictEqual(h.ccdf(1), 1); + assert.strictEqual(h.countAt(1), 0); + assert.strictEqual(h.skewness, 0); + assert.strictEqual(h.kurtosis, 0); + + const empty2 = createHistogram(); + assert.strictEqual(h.ksTest(empty2), 0); + + const pctAt = h.percentilesAt([50, 99]); + assert.ok(pctAt instanceof Map); + assert.strictEqual(pctAt.size, 2); + + const linear = h.linearBuckets(10); + assert.ok(linear instanceof Map); + + const log = h.logBuckets(1, 2); + assert.ok(log instanceof Map); +} + +// --------------------------------------------------------------------------- +// Single-value histogram edge cases +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + h.record(42); + + assert.strictEqual(h.skewness, 0); // Needs >= 3 + assert.strictEqual(h.kurtosis, 0); // Needs >= 4 + assert.strictEqual(h.cdf(42), 1); + assert.strictEqual(h.cdf(1), 0); + assert.strictEqual(h.ccdf(42), 0); + assert.strictEqual(h.countAt(42), 1); +} + +// --------------------------------------------------------------------------- +// Fast API call tests for new methods +// --------------------------------------------------------------------------- +{ + const h = createHistogram(); + h.record(1); + h.record(100); + + // Prepare cdf and countAt methods for optimization + eval('%PrepareFunctionForOptimization(h.cdf)'); + eval('%PrepareFunctionForOptimization(h.countAt)'); + + // Warmup call + h.cdf(50); + h.countAt(1); + + // Optimize + eval('%OptimizeFunctionOnNextCall(h.cdf)'); + eval('%OptimizeFunctionOnNextCall(h.countAt)'); + + // Fast-path call + h.cdf(50); + h.countAt(1); + + if (common.isDebug) { + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('histogram.cdf'), 1); + assert.strictEqual(getV8FastApiCallCount('histogram.countAt'), 1); + } +}