Skip to content

Commit 0529772

Browse files
jasnelladuh95
authored andcommitted
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 f15e8c9 commit 0529772

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
@@ -1858,6 +1858,45 @@ added:
18581858

18591859
The number of samples recorded by the histogram.
18601860

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

18631902
<!-- YAML
@@ -1882,6 +1921,59 @@ added:
18821921
The number of times the event loop delay exceeded the maximum 1 hour event
18831922
loop delay threshold.
18841923

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

18871979
<!-- YAML
@@ -1982,6 +2074,20 @@ added:
19822074

19832075
Returns a `Map` object detailing the accumulated percentile distribution.
19842076

2077+
### `histogram.percentilesAt(percentiles)`
2078+
2079+
<!-- YAML
2080+
added: REPLACEME
2081+
-->
2082+
2083+
* `percentiles` {number\[]} An array of percentile values in the range (0, 100].
2084+
* Returns: {Map} A map of percentile values to their corresponding histogram
2085+
values.
2086+
2087+
Returns the values at the specified percentiles, computed in a single
2088+
efficient pass over the histogram data. More efficient than calling
2089+
`histogram.percentile()` multiple times.
2090+
19852091
### `histogram.reset()`
19862092

19872093
<!-- YAML
@@ -1990,6 +2096,19 @@ added: v11.10.0
19902096

19912097
Resets the collected histogram data.
19922098

2099+
### `histogram.skewness`
2100+
2101+
<!-- YAML
2102+
added: REPLACEME
2103+
-->
2104+
2105+
* Type: {number}
2106+
2107+
The skewness of the recorded values. Measures the asymmetry of the
2108+
distribution. A positive value indicates a right-skewed distribution
2109+
(longer right tail, common for latency data); a negative value
2110+
indicates a left-skewed distribution.
2111+
19932112
### `histogram.stddev`
19942113

19952114
<!-- YAML
@@ -2091,6 +2210,127 @@ added:
20912210
Calculates the amount of time (in nanoseconds) that has passed since the
20922211
previous call to `recordDelta()` and records that amount in the histogram.
20932212

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

20962336
### Measuring the duration of async operations

0 commit comments

Comments
 (0)