Skip to content

Commit bfa3e98

Browse files
committed
lib,src: improve histogram implementation
Several improvements: 1. In histogram-inl, Add previous locked only this->mutex while reading the other's fields unsafely. 2. In histogram.cc, PrepareCB now uses ContainerOf 3. In histogram.cc, BigInt value range is checked 4. In histogram.js, simplified impl and reduced duplication 5. In event_loop_delay.js, use a more consistent constructor Adds new analytical APIs to Histogram * histogram.ccdf(value) * histogram.cdf(value) * histogram.countAt(value) * histogram.ksTest(other) * histogram.kurtosis * histogram.linearBuckets(stepSize) * histogram.logBuckets(first, base) * histogram.percentilesAt(percentiles) * histogram.shewness On RecordableHistogram * histogram.recordCorrected(val, expectedInterval) * histogram.subtract(other) Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode/Opus PR-URL: #65024 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3ad9427 commit bfa3e98

7 files changed

Lines changed: 1530 additions & 156 deletions

File tree

doc/api/perf_hooks.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,6 +1868,45 @@ added:
18681868

18691869
The number of samples recorded by the histogram.
18701870

1871+
### `histogram.ccdf(value)`
1872+
1873+
<!-- YAML
1874+
added: REPLACEME
1875+
-->
1876+
1877+
* `value` {number} The value to query.
1878+
* Returns: {number} A probability between 0.0 and 1.0.
1879+
1880+
Returns the complementary cumulative distribution function (CCDF) value
1881+
for the given value, representing the probability that a recorded value
1882+
will exceed `value`. Equivalent to `1 - histogram.cdf(value)`.
1883+
1884+
### `histogram.cdf(value)`
1885+
1886+
<!-- YAML
1887+
added: REPLACEME
1888+
-->
1889+
1890+
* `value` {number} The value to query.
1891+
* Returns: {number} A probability between 0.0 and 1.0.
1892+
1893+
Returns the cumulative distribution function (CDF) value for the given
1894+
value, representing the probability that a recorded value will be less
1895+
than or equal to `value`. This is the inverse operation of
1896+
`histogram.percentile()`.
1897+
1898+
### `histogram.countAt(value)`
1899+
1900+
<!-- YAML
1901+
added: REPLACEME
1902+
-->
1903+
1904+
* `value` {number} The value to query.
1905+
* Returns: {number}
1906+
1907+
Returns the number of recorded values that fall within the equivalent
1908+
value range of the given value.
1909+
18711910
### `histogram.exceeds`
18721911

18731912
<!-- YAML
@@ -1892,6 +1931,59 @@ added:
18921931
The number of times the event loop delay exceeded the maximum 1 hour event
18931932
loop delay threshold.
18941933

1934+
### `histogram.ksTest(other)`
1935+
1936+
<!-- YAML
1937+
added: REPLACEME
1938+
-->
1939+
1940+
* `other` {Histogram} The histogram to compare against.
1941+
* Returns: {number} The KS D-statistic, between 0.0 and 1.0.
1942+
1943+
Computes the Kolmogorov-Smirnov test statistic comparing this histogram's
1944+
distribution to `other`. A value of 0 indicates identical distributions;
1945+
values close to 1 indicate completely disjoint distributions. Useful for
1946+
detecting performance regressions by comparing before/after histograms.
1947+
1948+
### `histogram.kurtosis`
1949+
1950+
<!-- YAML
1951+
added: REPLACEME
1952+
-->
1953+
1954+
* Type: {number}
1955+
1956+
The excess kurtosis of the recorded values. Measures the heaviness of the
1957+
distribution's tails relative to a normal distribution. Positive values
1958+
indicate heavier tails (more extreme outliers); negative values indicate
1959+
lighter tails.
1960+
1961+
### `histogram.linearBuckets(stepSize)`
1962+
1963+
<!-- YAML
1964+
added: REPLACEME
1965+
-->
1966+
1967+
* `stepSize` {number} The width of each linear bucket.
1968+
* Returns: {Map} A map of bucket boundary values to counts.
1969+
1970+
Returns the histogram data rebucketed into linearly-spaced intervals
1971+
of `stepSize`. Useful for visualization and export.
1972+
1973+
### `histogram.logBuckets(firstBucket, base)`
1974+
1975+
<!-- YAML
1976+
added: REPLACEME
1977+
-->
1978+
1979+
* `firstBucket` {number} The value of the first bucket boundary.
1980+
* `base` {number} The logarithmic base for bucket width growth. Must be > 1.
1981+
* Returns: {Map} A map of bucket boundary values to counts.
1982+
1983+
Returns the histogram data rebucketed into logarithmically-spaced
1984+
intervals, where each bucket's width is multiplied by `base`.
1985+
Useful for visualization and export.
1986+
18951987
### `histogram.max`
18961988

18971989
<!-- YAML
@@ -1992,6 +2084,20 @@ added:
19922084

19932085
Returns a `Map` object detailing the accumulated percentile distribution.
19942086

2087+
### `histogram.percentilesAt(percentiles)`
2088+
2089+
<!-- YAML
2090+
added: REPLACEME
2091+
-->
2092+
2093+
* `percentiles` {number\[]} An array of percentile values in the range (0, 100].
2094+
* Returns: {Map} A map of percentile values to their corresponding histogram
2095+
values.
2096+
2097+
Returns the values at the specified percentiles, computed in a single
2098+
efficient pass over the histogram data. More efficient than calling
2099+
`histogram.percentile()` multiple times.
2100+
19952101
### `histogram.reset()`
19962102

19972103
<!-- YAML
@@ -2000,6 +2106,19 @@ added: v11.10.0
20002106

20012107
Resets the collected histogram data.
20022108

2109+
### `histogram.skewness`
2110+
2111+
<!-- YAML
2112+
added: REPLACEME
2113+
-->
2114+
2115+
* Type: {number}
2116+
2117+
The skewness of the recorded values. Measures the asymmetry of the
2118+
distribution. A positive value indicates a right-skewed distribution
2119+
(longer right tail, common for latency data); a negative value
2120+
indicates a left-skewed distribution.
2121+
20032122
### `histogram.stddev`
20042123

20052124
<!-- YAML
@@ -2101,6 +2220,127 @@ added:
21012220
Calculates the amount of time (in nanoseconds) that has passed since the
21022221
previous call to `recordDelta()` and records that amount in the histogram.
21032222

2223+
### `histogram.recordCorrected(val, expectedInterval)`
2224+
2225+
<!-- YAML
2226+
added: REPLACEME
2227+
-->
2228+
2229+
* `val` {number|bigint} The value to record.
2230+
* `expectedInterval` {number|bigint} The expected recording interval.
2231+
2232+
Records a value with coordinated omission correction. When a system stall
2233+
prevents timely recording, this method backfills intermediate values at
2234+
`expectedInterval` steps between the previously recorded value and `val`.
2235+
This compensates for measurement gaps that would otherwise underrepresent
2236+
latency.
2237+
2238+
### `histogram.subtract(other)`
2239+
2240+
<!-- YAML
2241+
added: REPLACEME
2242+
-->
2243+
2244+
* `other` {RecordableHistogram}
2245+
2246+
Subtracts the values of `other` from this histogram. Both histograms should
2247+
have compatible configurations. Bucket counts that would become negative
2248+
are clamped to zero.
2249+
2250+
## Histogram analysis examples
2251+
2252+
The `Histogram` class provides statistical analysis methods useful for
2253+
performance monitoring, SLO enforcement, and regression detection.
2254+
2255+
### Distribution shape analysis
2256+
2257+
```js
2258+
const { createHistogram } = require('node:perf_hooks');
2259+
2260+
const h = createHistogram();
2261+
2262+
// Simulate a right-skewed latency distribution
2263+
for (let i = 0; i < 1000; i++) {
2264+
h.record(Math.ceil(Math.random() * 100));
2265+
}
2266+
// Add some outliers
2267+
for (let i = 0; i < 10; i++) {
2268+
h.record(500 + Math.ceil(Math.random() * 500));
2269+
}
2270+
2271+
console.log('Skewness:', h.skewness.toFixed(4)); // Positive = right-skewed
2272+
console.log('Kurtosis:', h.kurtosis.toFixed(4)); // Positive = heavy tails
2273+
```
2274+
2275+
### SLO monitoring with CDF
2276+
2277+
```js
2278+
const { createHistogram } = require('node:perf_hooks');
2279+
2280+
const latency = createHistogram();
2281+
2282+
// Record request latencies (in nanoseconds)...
2283+
2284+
// "What fraction of requests complete within 100ms?"
2285+
const withinSLO = latency.cdf(100_000_000);
2286+
console.log(`${(withinSLO * 100).toFixed(1)}% of requests within SLO`);
2287+
2288+
// "What fraction of requests exceed 500ms?"
2289+
const violating = latency.ccdf(500_000_000);
2290+
console.log(`${(violating * 100).toFixed(1)}% of requests violating SLO`);
2291+
```
2292+
2293+
### Regression detection with KS test
2294+
2295+
```js
2296+
const { createHistogram } = require('node:perf_hooks');
2297+
2298+
const baseline = createHistogram();
2299+
const current = createHistogram();
2300+
2301+
// Record baseline and current latencies...
2302+
2303+
// D-statistic: 0 = identical, 1 = completely different
2304+
const d = baseline.ksTest(current);
2305+
if (d > 0.1) {
2306+
console.log(`Possible regression detected (D=${d.toFixed(4)})`);
2307+
}
2308+
```
2309+
2310+
### Batch percentile queries
2311+
2312+
```js
2313+
const { createHistogram } = require('node:perf_hooks');
2314+
2315+
const h = createHistogram();
2316+
// Record values...
2317+
2318+
// Efficiently query common monitoring percentiles in one pass
2319+
const p = h.percentilesAt([50, 75, 90, 95, 99, 99.9]);
2320+
console.log('p50:', p.get(50));
2321+
console.log('p99:', p.get(99));
2322+
```
2323+
2324+
### Snapshot diffing with subtract
2325+
2326+
```js
2327+
const { createHistogram } = require('node:perf_hooks');
2328+
2329+
const total = createHistogram();
2330+
const snapshot = createHistogram();
2331+
2332+
// Record values into total...
2333+
// Periodically snapshot for "last interval" analysis:
2334+
snapshot.add(total);
2335+
2336+
// Later, take a new snapshot and diff:
2337+
const newSnapshot = createHistogram();
2338+
newSnapshot.add(total);
2339+
newSnapshot.subtract(snapshot);
2340+
// newSnapshot now contains only the values recorded since the last snapshot
2341+
console.log('Recent p99:', newSnapshot.percentile(99));
2342+
```
2343+
21042344
## Examples
21052345

21062346
### Measuring the duration of async operations

0 commit comments

Comments
 (0)