Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions benchmark/perf_hooks/histogram-sliding-window-record.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const assert = require('assert');
const common = require('../common.js');
const { createSlidingWindowHistogram } = require('perf_hooks');

const bench = common.createBenchmark(main, {
n: [1e6],
mode: ['count', 'time'],
chunks: [6],
});

function main({ n, mode, chunks }) {
const options = mode === 'count' ?
{ chunks, recordsPerChunk: 1000 } :
{ chunks, chunkDuration: 1 };
const histogram = createSlidingWindowHistogram(options);

bench.start();
for (let i = 0; i < n; i++) histogram.record((i % 1000) + 1);
bench.end(n);

assert.ok(histogram.snapshot().count > 0);
}
29 changes: 29 additions & 0 deletions benchmark/perf_hooks/histogram-sliding-window-snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const assert = require('assert');
const common = require('../common.js');
const { createSlidingWindowHistogram } = require('perf_hooks');

const bench = common.createBenchmark(main, {
n: [100],
chunks: [2, 8],
recordsPerChunk: [1000],
});

let snapshot;

function main({ n, chunks, recordsPerChunk }) {
const histogram = createSlidingWindowHistogram({
chunks,
recordsPerChunk,
});
for (let i = 0; i < chunks * recordsPerChunk; i++) {
histogram.record((i % 1000) + 1);
}

bench.start();
for (let i = 0; i < n; i++) snapshot = histogram.snapshot();
bench.end(n);

assert.strictEqual(snapshot.count, chunks * recordsPerChunk);
}
104 changes: 104 additions & 0 deletions doc/api/perf_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,61 @@ added:

Returns a {RecordableHistogram}.

## `perf_hooks.createSlidingWindowHistogram(options)`

<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `chunks` {number} The number of histogram chunks retained. Must be an
integer between `1` and `1024`.
* `chunkDuration` {number} The duration of each chunk in milliseconds. Must
be an integer between `1` and `18_446_744_073_709`. Exactly one of
`chunkDuration` and `recordsPerChunk` must be specified.
* `recordsPerChunk` {number} The number of calls to `record()` assigned to
each chunk. Must be an integer between `1` and `Number.MAX_SAFE_INTEGER`.
Exactly one of `chunkDuration` and `recordsPerChunk` must be specified.
* `lowest` {number|bigint} The lowest discernible value. Must be an integer
value greater than `0`. **Default:** `1`.
* `highest` {number|bigint} The highest recordable value. Must be an integer
value that is equal to or greater than two times `lowest`.
**Default:** `Number.MAX_SAFE_INTEGER`.
* `figures` {number} The number of accuracy digits. Must be an integer between
`1` and `5`. **Default:** `3`.
* Returns: {SlidingWindowHistogram}

Creates a {SlidingWindowHistogram} that retains the latest `chunks` histogram
chunks. Rotation is lazy and does not create a timer. Time-based rotation is
evaluated when `record()` or `snapshot()` is called. Count-based rotation is
evaluated when `record()` is called.

One histogram chunk is allocated during construction. Additional chunks are
allocated lazily. The maximum native memory used by the window scales with
`chunks` and with the `lowest`, `highest`, and `figures` histogram options.

The window boundary has chunk-level precision. With `N` chunks of duration
`D`, a recorded value is retained for between `(N - 1) * D` and `N * D`
milliseconds. Once a count-based window is populated, it retains between
`(N - 1) * C + 1` and `N * C` recording attempts, where `C` is
`recordsPerChunk`. Recording attempts which exceed `highest` are included when
determining count-based rotation.

```js
const { createSlidingWindowHistogram } = require('node:perf_hooks');

const window = createSlidingWindowHistogram({
chunks: 6,
chunkDuration: 10_000,
});

window.record(20_000_000);

// Materialize the current window as an independent Histogram.
const snapshot = window.snapshot();
console.log(snapshot.percentile(99));
```

## `perf_hooks.importHistogram(data)`

<!-- YAML
Expand Down Expand Up @@ -2725,6 +2780,54 @@ Subtracts the values of `other` from this histogram. Both histograms should
have compatible configurations. Bucket counts that would become negative
are clamped to zero.

## Class: `SlidingWindowHistogram`

<!-- YAML
added: REPLACEME
-->

Records values into a lazily rotated ring of histogram chunks. Instances are
created using [`perf_hooks.createSlidingWindowHistogram()`][] and cannot be
constructed directly. A `SlidingWindowHistogram` does not extend {Histogram};
call `snapshot()` to materialize the current window as a {Histogram}.

`SlidingWindowHistogram` instances cannot be cloned or transferred through a
{MessagePort}.

### `slidingWindowHistogram.record(val)`

<!-- YAML
added: REPLACEME
-->

* `val` {number|bigint} The amount to record.

Records `val` in the current chunk. For a count-based window, every call that
reaches the native histogram counts toward rotation, including values which
exceed the configured `highest` value.

### `slidingWindowHistogram.reset()`

<!-- YAML
added: REPLACEME
-->

Invalidates all chunks in the current window. Allocated chunks are reset
lazily when reused.

### `slidingWindowHistogram.snapshot()`

<!-- YAML
added: REPLACEME
-->

* Returns: {Histogram}

Materializes the current window as a new, independent {Histogram}. Values
recorded or expired after this method returns do not change the returned
histogram. Materialization allocates one histogram and merges every retained
chunk.

## Histogram analysis examples

The `Histogram` class provides statistical analysis methods useful for
Expand Down Expand Up @@ -3155,6 +3258,7 @@ dns.promises.resolve('localhost');
[`'exit'`]: process.md#event-exit
[`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options
[`histogram.export()`]: #histogramexport
[`perf_hooks.createSlidingWindowHistogram()`]: #perf_hookscreateslidingwindowhistogramoptions
[`perf_hooks.eventLoopUtilization()`]: #perf_hookseventlooputilizationutilization1-utilization2
[`perf_hooks.importHistogram()`]: #perf_hooksimporthistogramdata
[`perf_hooks.monitorEventLoopDelay()`]: #perf_hooksmonitoreventloopdelayoptions
Expand Down
137 changes: 128 additions & 9 deletions lib/internal/histogram.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
BigInt,
Float64Array,
Map,
MapPrototypeEntries,
Expand All @@ -12,6 +13,7 @@ const {

const {
Histogram: _Histogram,
SlidingWindowHistogram: _SlidingWindowHistogram,
} = internalBinding('performance');

const {
Expand Down Expand Up @@ -47,7 +49,11 @@ const {
const kDestroy = Symbol('kDestroy');
const kHandle = Symbol('kHandle');
const kRecordable = Symbol('kRecordable');
const kSlidingWindowHandle = Symbol('kSlidingWindowHandle');
const kQrdeDequantizationModes = ['none', 'hdr', 'all'];
const kMaxSlidingWindowHistogramChunks = 1024;
const kMaxChunkDuration = 18_446_744_073_709;
const kMaxInt64 = 9_223_372_036_854_775_807n;

const {
kClone,
Expand Down Expand Up @@ -801,6 +807,48 @@ class RecordableHistogram extends Histogram {
}
}

class SlidingWindowHistogram {
constructor(skipThrowSymbol = undefined) {
if (skipThrowSymbol !== kSkipThrow) {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
}

/**
* @param {number|bigint} val
* @returns {void}
*/
record(val) {
if (this[kSlidingWindowHandle] === undefined)
throw new ERR_INVALID_THIS('SlidingWindowHistogram');
if (typeof val === 'bigint') {
this[kSlidingWindowHandle].record(val);
return;
}

validateInteger(val, 'val', 1);
this[kSlidingWindowHandle].record(val);
}

/**
* @returns {Histogram}
*/
snapshot() {
if (this[kSlidingWindowHandle] === undefined)
throw new ERR_INVALID_THIS('SlidingWindowHistogram');
return new ClonedHistogram(this[kSlidingWindowHandle].snapshot());
}

/**
* @returns {void}
*/
reset() {
if (this[kSlidingWindowHandle] === undefined)
throw new ERR_INVALID_THIS('SlidingWindowHistogram');
this[kSlidingWindowHandle].reset();
}
}

function ClonedHistogram(handle) {
const histogram = new Histogram(kSkipThrow);
markTransferMode(histogram, true, false);
Expand All @@ -827,6 +875,32 @@ function createRecordableHistogram(handle) {
return new ClonedRecordableHistogram(handle);
}

function validateHistogramOptions(lowest, highest, figures) {
if (typeof lowest !== 'bigint') {
validateInteger(lowest, 'options.lowest', 1, NumberMAX_SAFE_INTEGER);
} else if (lowest < 1n || lowest > kMaxInt64) {
throw new ERR_OUT_OF_RANGE(
'options.lowest', `>= 1n && <= ${kMaxInt64}n`, lowest);
}

if (typeof highest !== 'bigint') {
validateInteger(highest, 'options.highest', 1, NumberMAX_SAFE_INTEGER);
} else if (highest < 1n || highest > kMaxInt64) {
throw new ERR_OUT_OF_RANGE(
'options.highest', `>= 1n && <= ${kMaxInt64}n`, highest);
}

const minimumHighest = 2n *
(typeof lowest === 'bigint' ? lowest : BigInt(lowest));
const highestBigInt = typeof highest === 'bigint' ?
highest : BigInt(highest);
if (highestBigInt < minimumHighest) {
throw new ERR_OUT_OF_RANGE(
'options.highest', `>= 2 * options.lowest (${minimumHighest}n)`, highest);
}
validateInteger(figures, 'options.figures', 1, 5);
}

/**
* @param {{
* lowest? : number,
Expand All @@ -846,15 +920,7 @@ function createHistogram(options = kEmptyObject) {
halfLife = 0,
threshold = 0,
} = options;
if (typeof lowest !== 'bigint')
validateInteger(lowest, 'options.lowest', 1, NumberMAX_SAFE_INTEGER);
if (typeof highest !== 'bigint') {
validateInteger(highest, 'options.highest',
2 * lowest, NumberMAX_SAFE_INTEGER);
} else if (highest < 2n * lowest) {
throw new ERR_INVALID_ARG_VALUE.RangeError('options.highest', highest);
}
validateInteger(figures, 'options.figures', 1, 5);
validateHistogramOptions(lowest, highest, figures);
validateNumber(halfLife, 'options.halfLife');
if (halfLife < 0)
throw new ERR_OUT_OF_RANGE('options.halfLife', '>= 0', halfLife);
Expand All @@ -865,6 +931,57 @@ function createHistogram(options = kEmptyObject) {
new _Histogram(lowest, highest, figures, halfLife, threshold));
}

/**
* @param {{
* chunks: number,
* chunkDuration? : number,
* recordsPerChunk? : number,
* lowest? : number|bigint,
* highest? : number|bigint,
* figures? : number,
* }} options
* @returns {SlidingWindowHistogram}
*/
function createSlidingWindowHistogram(options) {
validateObject(options, 'options');
const {
chunks,
chunkDuration,
recordsPerChunk,
lowest = 1,
highest = NumberMAX_SAFE_INTEGER,
figures = 3,
} = options;

validateInteger(
chunks, 'options.chunks', 1, kMaxSlidingWindowHistogramChunks);
validateHistogramOptions(lowest, highest, figures);

const timeBased = chunkDuration !== undefined;
if (timeBased === (recordsPerChunk !== undefined)) {
throw new ERR_INVALID_ARG_VALUE(
'options', options,
'must specify exactly one of "chunkDuration" or "recordsPerChunk"');
}

let rotateAt;
if (timeBased) {
validateInteger(
chunkDuration, 'options.chunkDuration', 1, kMaxChunkDuration);
rotateAt = BigInt(chunkDuration) * 1_000_000n;
} else {
validateInteger(
recordsPerChunk, 'options.recordsPerChunk', 1, NumberMAX_SAFE_INTEGER);
rotateAt = BigInt(recordsPerChunk);
}

const histogram = new SlidingWindowHistogram(kSkipThrow);
markTransferMode(histogram, false, false);
histogram[kSlidingWindowHandle] = new _SlidingWindowHistogram(
lowest, highest, figures, chunks, timeBased, rotateAt);
return histogram;
}

/**
* Reconstructs a histogram from a CBOR-encoded Uint8Array previously
* produced by `histogram.export()`.
Expand All @@ -880,12 +997,14 @@ function importHistogram(data) {
module.exports = {
Histogram,
RecordableHistogram,
SlidingWindowHistogram,
ClonedHistogram,
ClonedRecordableHistogram,
isHistogram,
kDestroy,
kHandle,
kSkipThrow,
createHistogram,
createSlidingWindowHistogram,
importHistogram,
};
2 changes: 2 additions & 0 deletions lib/perf_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {

const {
createHistogram,
createSlidingWindowHistogram,
importHistogram,
} = require('internal/histogram');

Expand All @@ -44,6 +45,7 @@ module.exports = {
eventLoopUtilization,
timerify,
createHistogram,
createSlidingWindowHistogram,
importHistogram,
performance,
};
Expand Down
Loading
Loading