@@ -1621,6 +1621,16 @@ added:
16211621 **Default:** `Number.MAX_SAFE_INTEGER`.
16221622 * `figures` {number} The number of accuracy digits. Must be a number between
16231623 `1` and `5`. **Default:** `3`.
1624+ * `halfLife` {number} The EWMA half-life in number of samples. When set to
1625+ a value greater than 0, the histogram tracks an exponentially weighted
1626+ moving average and standard deviation, accessible via
1627+ `histogram.ewmaMean` and `histogram.ewmaStddev`. After `halfLife`
1628+ recordings, a value's influence has decayed to 50%. **Default:** `0`
1629+ (disabled).
1630+ * `threshold` {number} An SLO threshold value. When set together with
1631+ `halfLife`, the histogram tracks a smoothed error rate for values
1632+ exceeding this threshold, accessible via `histogram.ewmaErrorRate` and
1633+ `histogram.burnRate()`. **Default:** `0` (disabled).
16241634* Returns: {RecordableHistogram}
16251635
16261636Returns a {RecordableHistogram}.
@@ -1885,6 +1895,36 @@ value, representing the probability that a recorded value will be less
18851895than or equal to `value`. This is the inverse operation of
18861896`histogram.percentile()`.
18871897
1898+ ### `histogram.cliffsD(other)`
1899+
1900+ <!-- YAML
1901+ added: REPLACEME
1902+ -->
1903+
1904+ * `other` {Histogram} The histogram to compare against.
1905+ * Returns: {number} A value between -1.0 and 1.0.
1906+
1907+ Computes [Cliff's delta][], a non-parametric effect size measure. Returns
1908+ the probability that a random value from this histogram exceeds a random
1909+ value from `other`, minus the reverse probability. A value of 1 means every
1910+ value in this histogram exceeds every value in `other`; -1 means the
1911+ opposite; 0 means no tendency in either direction.
1912+
1913+ ### `histogram.cohensD(other)`
1914+
1915+ <!-- YAML
1916+ added: REPLACEME
1917+ -->
1918+
1919+ * `other` {Histogram} The histogram to compare against.
1920+ * Returns: {number} The effect size.
1921+
1922+ Computes [Cohen's d][] effect size, the standardized difference between the
1923+ means of this histogram and `other`, using the pooled standard deviation.
1924+ Positive values indicate this histogram has a higher mean. By convention,
1925+ |d| < 0.2 is a small effect, 0.5 is medium, and 0.8 or greater is large.
1926+ Both histograms must have at least 2 recorded values; otherwise returns 0.
1927+
18881928### `histogram.countAt(value)`
18891929
18901930<!-- YAML
@@ -1921,6 +1961,74 @@ added:
19211961The number of times the event loop delay exceeded the maximum 1 hour event
19221962loop delay threshold.
19231963
1964+ ### `histogram.ewmaMean`
1965+
1966+ <!-- YAML
1967+ added: REPLACEME
1968+ -->
1969+
1970+ * Type: {number}
1971+
1972+ The exponentially weighted moving average of recorded values. Only active
1973+ when the histogram was created with a `halfLife` option greater than 0.
1974+ Returns `0` when EWMA is disabled or no values have been recorded.
1975+
1976+ ### `histogram.ewmaStddev`
1977+
1978+ <!-- YAML
1979+ added: REPLACEME
1980+ -->
1981+
1982+ * Type: {number}
1983+
1984+ The exponentially weighted moving standard deviation. Only active when the
1985+ histogram was created with a `halfLife` option greater than 0. Returns `0`
1986+ when EWMA is disabled or no values have been recorded.
1987+
1988+ ### `histogram.ewmaErrorRate`
1989+
1990+ <!-- YAML
1991+ added: REPLACEME
1992+ -->
1993+
1994+ * Type: {number}
1995+
1996+ The EWMA-smoothed probability of a recorded value exceeding the configured
1997+ `threshold`. Only active when the histogram was created with both `halfLife`
1998+ and `threshold` options. Returns `0` when not enabled or no values have been
1999+ recorded.
2000+
2001+ ### `histogram.burnRate(sloTarget)`
2002+
2003+ <!-- YAML
2004+ added: REPLACEME
2005+ -->
2006+
2007+ * `sloTarget` {number} The SLO target as a fraction between 0 and 1
2008+ (exclusive). For example, `0.999` for a 99.9% SLO.
2009+ * Returns: {number}
2010+
2011+ Returns the SLO burn rate: `ewmaErrorRate / (1 - sloTarget)`. A burn rate
2012+ of 1 means the error budget will be exactly exhausted over the SLO window.
2013+ A burn rate greater than 1 means it is being consumed faster than allowed.
2014+ Requires the histogram to have been created with both `halfLife` and
2015+ `threshold` options.
2016+
2017+ ```js
2018+ const { createHistogram } = require('node:perf_hooks');
2019+
2020+ // Track latency with a 200ms SLO threshold, half-life of 100 samples
2021+ const h = createHistogram({ halfLife: 100, threshold: 200_000_000 });
2022+
2023+ // ... record latency values ...
2024+
2025+ // Check burn rate against a 99.9% SLO
2026+ const rate = h.burnRate(0.999);
2027+ if (rate > 1) {
2028+ console.log(`SLO burn rate: ${rate.toFixed(2)}x — error budget depleting`);
2029+ }
2030+ ```
2031+
19242032### `histogram.ksTest(other)`
19252033
19262034<!-- YAML
@@ -1974,6 +2082,24 @@ Returns the histogram data rebucketed into logarithmically-spaced
19742082intervals, where each bucket's width is multiplied by `base`.
19752083Useful for visualization and export.
19762084
2085+ ### `histogram.mannWhitneyTest(other)`
2086+
2087+ <!-- YAML
2088+ added: REPLACEME
2089+ -->
2090+
2091+ * `other` {Histogram} The histogram to compare against.
2092+ * Returns: {Object}
2093+ * `uStatistic` {number} The Mann-Whitney U statistic.
2094+ * `zScore` {number} The z-score (normal approximation).
2095+ * `pValue` {number} Two-tailed p-value.
2096+
2097+ Performs a [Mann-Whitney U test][] comparing whether this histogram tends to
2098+ produce larger or smaller values than `other`. Unlike `welchTest()`, this is a
2099+ non-parametric test that makes no assumptions about the shape of the
2100+ distributions. Uses the normal approximation with tie correction for the
2101+ p-value.
2102+
19772103### `histogram.max`
19782104
19792105<!-- YAML
@@ -2052,6 +2178,40 @@ added:
20522178
20532179Returns the value at the given percentile.
20542180
2181+ ### `histogram.percentileCI(percentile[, options])`
2182+
2183+ <!-- YAML
2184+ added: REPLACEME
2185+ -->
2186+
2187+ * `percentile` {number} A percentile value in the range (0, 100].
2188+ * `options` {Object}
2189+ * `confidence` {number} The confidence level for the interval, between
2190+ 0 and 1 (exclusive). **Default:** `0.95`.
2191+ * Returns: {Object}
2192+ * `value` {number} The point estimate (same as `histogram.percentile()`).
2193+ * `lower` {number} The lower bound of the confidence interval.
2194+ * `upper` {number} The upper bound of the confidence interval.
2195+
2196+ Returns a confidence interval for the given percentile using the exact
2197+ binomial method. With fewer samples, the interval will be wider, reflecting
2198+ the greater uncertainty in the percentile estimate. Requires at least 2
2199+ recorded values; with fewer than 2, `lower` and `upper` will equal `value`.
2200+
2201+ ```js
2202+ const { createHistogram } = require('node:perf_hooks');
2203+
2204+ const h = createHistogram();
2205+ for (let i = 0; i < 1000; i++) {
2206+ h.record(Math.floor(Math.random() * 100));
2207+ }
2208+
2209+ const ci = h.percentileCI(99);
2210+ console.log(ci.value); // The p99 point estimate
2211+ console.log(ci.lower); // The lower bound (95% confidence)
2212+ console.log(ci.upper); // The upper bound (95% confidence)
2213+ ```
2214+
20552215### `histogram.percentiles`
20562216
20572217<!-- YAML
@@ -2119,6 +2279,31 @@ added: v11.10.0
21192279
21202280The standard deviation of the recorded event loop delays.
21212281
2282+ ### `histogram.welchTest(other[, options])`
2283+
2284+ <!-- YAML
2285+ added: REPLACEME
2286+ -->
2287+
2288+ * `other` {Histogram} The histogram to compare against.
2289+ * `options` {Object}
2290+ * `confidence` {number} Confidence level for the interval, between 0 and 1.
2291+ **Default:** `0.95`.
2292+ * Returns: {Object}
2293+ * `tStatistic` {number} The Welch t-statistic.
2294+ * `degreesOfFreedom` {number} Welch-Satterthwaite degrees of freedom.
2295+ * `pValue` {number} Two-tailed p-value.
2296+ * `confidenceInterval` {Object}
2297+ * `lower` {number} Lower bound of the confidence interval on the
2298+ difference of means.
2299+ * `upper` {number} Upper bound.
2300+
2301+ Performs [Welch's t-test][] comparing the means of this histogram and `other`.
2302+ The p-value indicates the probability of observing a difference at least this
2303+ extreme under the null hypothesis that the two distributions have the same
2304+ mean. Both histograms must have at least 2 recorded values; otherwise the
2305+ result has `pValue` 1 and `tStatistic` 0.
2306+
21222307## Class: `ELDHistogram extends Histogram`
21232308
21242309A `Histogram` that records event loop delay, returned by
@@ -2280,6 +2465,32 @@ const violating = latency.ccdf(500_000_000);
22802465console.log(`${(violating * 100).toFixed(1)}% of requests violating SLO`);
22812466```
22822467
2468+ ### SLO burn rate monitoring
2469+
2470+ ```js
2471+ const { createHistogram } = require('node:perf_hooks');
2472+
2473+ // Track latency with EWMA (half-life 100 samples) and a 200ms SLO threshold
2474+ const latency = createHistogram({
2475+ halfLife: 100,
2476+ threshold: 200_000_000, // 200ms in nanoseconds
2477+ });
2478+
2479+ // Record request latencies...
2480+
2481+ // Smoothed error rate: probability of exceeding the threshold
2482+ console.log(`Error rate: ${(latency.ewmaErrorRate * 100).toFixed(2)}%`);
2483+
2484+ // Burn rate against a 99.9% SLO
2485+ // >1 means the error budget is depleting faster than allowed
2486+ const rate = latency.burnRate(0.999);
2487+ console.log(`Burn rate: ${rate.toFixed(2)}x`);
2488+
2489+ // EWMA mean and stddev track the smoothed latency
2490+ console.log(`EWMA latency: ${latency.ewmaMean.toFixed(0)}ns`);
2491+ console.log(`EWMA stddev: ${latency.ewmaStddev.toFixed(0)}ns`);
2492+ ```
2493+
22832494### Regression detection with KS test
22842495
22852496```js
@@ -2331,6 +2542,46 @@ newSnapshot.subtract(snapshot);
23312542console.log('Recent p99:', newSnapshot.percentile(99));
23322543```
23332544
2545+ ### Benchmark comparison with Welch's t-test
2546+
2547+ ```js
2548+ const { createHistogram } = require('node:perf_hooks');
2549+
2550+ const baseline = createHistogram();
2551+ const candidate = createHistogram();
2552+
2553+ // Record operation rates from the old and new builds...
2554+
2555+ const result = baseline.welchTest(candidate);
2556+ const improvement = ((candidate.mean - baseline.mean) / baseline.mean * 100);
2557+
2558+ console.log(`Improvement: ${improvement.toFixed(2)}%`);
2559+ console.log(`p-value: ${result.pValue.toFixed(6)}`);
2560+ console.log(`95% CI: [${result.confidenceInterval.lower.toFixed(2)}, ` +
2561+ `${result.confidenceInterval.upper.toFixed(2)}]`);
2562+
2563+ if (result.pValue < 0.05) {
2564+ const d = baseline.cohensD(candidate);
2565+ console.log(`Statistically significant (Cohen's d = ${d.toFixed(4)})`);
2566+ }
2567+ ```
2568+
2569+ ### Effect size with Cliff's delta
2570+
2571+ ```js
2572+ const { createHistogram } = require('node:perf_hooks');
2573+
2574+ const before = createHistogram();
2575+ const after = createHistogram();
2576+
2577+ // Record latencies before and after a change...
2578+
2579+ const delta = before.cliffsD(after);
2580+ // A delta > 0: before tends to produce larger values (improvement)
2581+ // A delta < 0: after tends to produce larger values (regression)
2582+ console.log(`Cliff's delta: ${delta.toFixed(4)}`);
2583+ ```
2584+
23342585## Examples
23352586
23362587### Measuring the duration of async operations
@@ -2585,13 +2836,17 @@ dns.promises.resolve('localhost');
25852836```
25862837
25872838[Async Hooks]: async_hooks.md
2839+ [Cliff's delta]: https://en.wikipedia.org/wiki/Effect_size#Cliff's_delta
2840+ [Cohen's d]: https://en.wikipedia.org/wiki/Effect_size#Cohen's_d
25882841[Fetch Response Body Info]: https://fetch.spec.whatwg.org/#response-body-info
25892842[Fetch Timing Info]: https://fetch.spec.whatwg.org/#fetch-timing-info
25902843[High Resolution Time]: https://www.w3.org/TR/hr-time-2
2844+ [Mann-Whitney U test]: https://en.wikipedia.org/wiki/Mann%E2%80%93Whitney_U_test
25912845[Performance Timeline]: https://w3c.github.io/performance-timeline/
25922846[Resource Timing]: https://www.w3.org/TR/resource-timing-2/
25932847[User Timing]: https://www.w3.org/TR/user-timing/
25942848[Web Performance APIs]: https://w3c.github.io/perf-timing-primer/
2849+ [Welch's t-test]: https://en.wikipedia.org/wiki/Welch%27s_t-test
25952850[Worker threads]: worker_threads.md#worker-threads
25962851[`'exit'`]: process.md#event-exit
25972852[`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options
0 commit comments