🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@nxtedition/histogram

Package Overview
Dependencies
Maintainers
12
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nxtedition/histogram - npm Package Compare versions

Comparing version
1.0.1
to
1.0.2
+6
-6
lib/index.d.ts

@@ -22,12 +22,12 @@ export declare const BUCKETS: readonly number[];

export type LagSampler = Disposable & {
/** Merge pending samples into the decayed histogram and return current percentiles + max. */
/** Return decayed-accumulator percentiles, or null if nothing has been sampled yet. */
sample(): LagHistogram | null;
};
/**
* Event-loop lag sampler. An internal setInterval records overshoot into a
* per-tick histogram at `sampleIntervalMs` cadence; `sample()` folds that into
* an exponentially-decayed accumulator (per-call decay based on wall-clock
* elapsed since the previous `sample()`, so the half-life stays accurate even
* if the caller's tick cadence varies).
* Event-loop lag sampler backed by `perf_hooks.monitorEventLoopDelay`. Each
* `sample()` call reads the ELD's percentile CDF, folds an approximate bucket
* reconstruction into an exponentially-decayed accumulator, resets the ELD,
* and returns decayed percentiles. The 12h half-life semantics are preserved
* even though ELD itself has no decay operation.
*/
export declare function createLagSampler({ sampleIntervalMs, halfLifeMs, }?: LagSamplerOptions): LagSampler;

@@ -0,1 +1,3 @@

import { monitorEventLoopDelay, performance } from 'node:perf_hooks'
// Bucket upper bounds (milliseconds). Values above the highest finite bound

@@ -139,12 +141,59 @@ // land in a final `Infinity` bucket and are reported as `BUCKET_MAX` for

// Credit a single value into the first bucket whose upper bound contains it.
function creditBucket(hist , valueMs , weight ) {
for (let i = 0; i < BUCKETS.length; i++) {
if (valueMs <= BUCKETS[i]) {
hist[i] += weight
return
}
}
}
// Distribute `weight` uniformly across the buckets that overlap the segment
// (lowerMs, upperMs]. Assuming a uniform distribution within a percentile
// segment is the best reconstruction we can do from ELD's CDF, and it's
// unbiased — attributing the entire segment to the upper-bound bucket
// (the previous approach) systematically inflates p95/p99.
function creditSegment(hist , lowerMs , upperMs , weight ) {
if (weight <= 0) {
return
}
const range = upperMs - lowerMs
if (!(range > 0) || !Number.isFinite(range)) {
// Degenerate segment (ELD reported the same percentile value on both
// ends, e.g. all samples equal) — put the whole weight in one bucket.
creditBucket(hist, upperMs, weight)
return
}
let startMs = lowerMs
for (let i = 0; i < BUCKETS.length; i++) {
const bucketTop = BUCKETS[i]
if (bucketTop < startMs) {
continue
}
if (startMs >= upperMs) {
return
}
const segTop = bucketTop === Infinity ? upperMs : Math.min(bucketTop, upperMs)
const fraction = (segTop - startMs) / range
if (fraction > 0) {
hist[i] += fraction * weight
}
startMs = segTop
if (bucketTop === Infinity) {
return
}
}
}
/**
* Event-loop lag sampler. An internal setInterval records overshoot into a
* per-tick histogram at `sampleIntervalMs` cadence; `sample()` folds that into
* an exponentially-decayed accumulator (per-call decay based on wall-clock
* elapsed since the previous `sample()`, so the half-life stays accurate even
* if the caller's tick cadence varies).
* Event-loop lag sampler backed by `perf_hooks.monitorEventLoopDelay`. Each
* `sample()` call reads the ELD's percentile CDF, folds an approximate bucket
* reconstruction into an exponentially-decayed accumulator, resets the ELD,
* and returns decayed percentiles. The 12h half-life semantics are preserved
* even though ELD itself has no decay operation.
*/

@@ -161,15 +210,7 @@ export function createLagSampler({

}
const sampleHist = createHistogram()
const decayedHist = createHistogram()
let lastSampleAt = performance.now()
const timer = setInterval(() => {
const now = performance.now()
const delay = Math.max(0, now - lastSampleAt - sampleIntervalMs)
lastSampleAt = now
recordValue(sampleHist, delay)
}, sampleIntervalMs)
// Node.js only — don't keep the process alive just for sampling.
;(timer ).unref?.()
const eld = monitorEventLoopDelay({ resolution: sampleIntervalMs })
eld.enable()
const decayedHist = createHistogram()
let lastTickAt = performance.now()

@@ -185,5 +226,45 @@

decayHistogram(decayedHist, decayFactor)
mergeHistogram(decayedHist, sampleHist)
sampleHist.fill(0)
// Reconstruct approximate bucket counts from ELD's percentile CDF and
// fold them into the decayed accumulator. For each adjacent percentile
// pair (p_{i-1}, v_{i-1}), (p_i, v_i), `(p_i - p_{i-1}) / 100 * count`
// samples fall in (v_{i-1}, v_i]; we distribute that weight uniformly
// across the buckets overlapping the segment. ELD timings are in
// nanoseconds; we bucket in milliseconds.
//
// NOTE: `eld.percentiles` and `eld.reset()` are not an atomic pair —
// a sample recorded by the native timer between the two calls is
// wiped without being counted. ELD exposes no snapshot-and-clear
// primitive, so this loss is unavoidable; at `sampleIntervalMs=10`
// and typical sample cadences it's negligible.
const count = eld.count
if (count > 0) {
const entries = [...eld.percentiles.entries()].sort((a, b) => a[0] - b[0])
if (entries.length === 0) {
// Defensive: ELD reports count > 0 but published no percentile
// points (would require a racey internal state). Credit the mean
// so we don't lose the samples entirely.
const meanMs = eld.mean / 1e6
if (Number.isFinite(meanMs) && meanMs >= 0) {
creditBucket(decayedHist, meanMs, count)
}
} else {
const minMs = eld.min / 1e6
let prevValueMs = Number.isFinite(minMs) && minMs >= 0 ? minMs : 0
let prevPct = 0
for (const [pct, valueNs] of entries) {
const weight = ((pct - prevPct) / 100) * count
prevPct = pct
if (weight <= 0) {
continue
}
const valueMs = valueNs / 1e6
creditSegment(decayedHist, prevValueMs, valueMs, weight)
prevValueMs = valueMs
}
}
}
eld.reset()
const pcts = computePercentiles(decayedHist, [0.5, 0.95, 0.99])

@@ -197,5 +278,5 @@ const max = computeMax(decayedHist)

[Symbol.dispose]() {
clearInterval(timer)
eld.disable()
},
}
}
{
"name": "@nxtedition/histogram",
"version": "1.0.1",
"version": "1.0.2",
"type": "module",

@@ -37,3 +37,3 @@ "main": "lib/index.js",

},
"gitHead": "45b14f89a7a87553f0da151ba47ca633110d0198"
"gitHead": "adfca68bef0a78441d030ee4d23afcfd497c878a"
}