mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Getting the values recorded during an interval requires calling `reset()`, which removes them for every other user of the histogram, or copying and subtracting histograms, which silently produces a wrong result when the source was reset in between. Add `histogram.diff(other)`, which returns a new read-only `Histogram` containing the values recorded after `other`, an earlier snapshot of the histogram, was taken. Neither histogram is changed. Unlike `subtract()`, it verifies that both histograms have the same layout, and it throws instead of clamping when `other` contains values that the histogram does not. Add `histogram.resetCount`, the number of calls to `reset()` and `subtract()`, which `snapshot()` copies. `diff()` throws when the counts differ, so a reset between two snapshots is detected even when every bucket has since grown past its previous count. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: OpenCode PR-URL: https://github.com/nodejs/node/pull/66099 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
170 lines
5.6 KiB
JavaScript
170 lines
5.6 KiB
JavaScript
'use strict';
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const { setTimeout: delay } = require('timers/promises');
|
|
const {
|
|
createHistogram,
|
|
createSlidingWindowHistogram,
|
|
importHistogram,
|
|
monitorEventLoopDelay,
|
|
} = require('perf_hooks');
|
|
|
|
{
|
|
const histogram = createHistogram({ highest: 1000, halfLife: 4, threshold: 50 });
|
|
for (let i = 1; i <= 10; i++) histogram.record(i);
|
|
histogram.record(2000);
|
|
const previous = histogram.snapshot();
|
|
for (let i = 101; i <= 120; i++) histogram.record(i);
|
|
histogram.record(3000);
|
|
histogram.record(4000);
|
|
const current = histogram.snapshot();
|
|
const exportedPrevious = previous.export();
|
|
const exportedCurrent = current.export();
|
|
|
|
const delta = current.diff(previous);
|
|
assert.strictEqual(delta.constructor.name, 'Histogram');
|
|
assert.strictEqual(delta.record, undefined);
|
|
assert.strictEqual(delta.count, 20);
|
|
assert.strictEqual(delta.exceeds, 2);
|
|
assert.strictEqual(delta.min, 101);
|
|
assert.strictEqual(delta.max, 120);
|
|
assert.strictEqual(delta.resetCount, 0);
|
|
|
|
// The difference has the same distribution as a histogram of the values
|
|
// recorded between the snapshots, and no EWMA state.
|
|
const expected = createHistogram({ highest: 1000 });
|
|
for (let i = 101; i <= 120; i++) expected.record(i);
|
|
assert.deepStrictEqual(delta.percentiles, expected.percentiles);
|
|
assert.strictEqual(delta.ewmaMean, 0);
|
|
assert.strictEqual(delta.ewmaStddev, 0);
|
|
assert.strictEqual(delta.ewmaErrorRate, 0);
|
|
|
|
// Neither histogram is changed.
|
|
assert.deepStrictEqual(previous.export(), exportedPrevious);
|
|
assert.deepStrictEqual(current.export(), exportedCurrent);
|
|
|
|
assert.strictEqual(histogram.diff(previous).count, 20);
|
|
assert.strictEqual(histogram.diff(histogram).count, 0);
|
|
assert.strictEqual(current.diff(current).count, 0);
|
|
|
|
// Reversed arguments.
|
|
assert.throws(() => previous.diff(current), {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
});
|
|
}
|
|
|
|
{
|
|
// Consumers with different intervals each keep their own previous snapshot.
|
|
const histogram = createHistogram();
|
|
const consumers = [3, 10].map((interval) => ({
|
|
interval,
|
|
previous: histogram.snapshot(),
|
|
pending: 0,
|
|
total: 0,
|
|
}));
|
|
for (let i = 1; i <= 100; i++) {
|
|
histogram.record(i);
|
|
for (const consumer of consumers) {
|
|
consumer.pending++;
|
|
if (i % consumer.interval !== 0) continue;
|
|
const current = histogram.snapshot();
|
|
const delta = current.diff(consumer.previous);
|
|
assert.strictEqual(delta.count, consumer.pending);
|
|
assert.strictEqual(delta.min, i - consumer.pending + 1);
|
|
assert.strictEqual(delta.max, i);
|
|
consumer.total += delta.count;
|
|
consumer.previous = current;
|
|
consumer.pending = 0;
|
|
}
|
|
}
|
|
assert.strictEqual(consumers[0].total, 99);
|
|
assert.strictEqual(consumers[1].total, 100);
|
|
}
|
|
|
|
{
|
|
const histogram = createHistogram();
|
|
assert.strictEqual(histogram.resetCount, 0);
|
|
histogram.record(1);
|
|
histogram.recordCorrected(100, 10);
|
|
histogram.add(createHistogram());
|
|
assert.strictEqual(histogram.resetCount, 0);
|
|
|
|
histogram.reset();
|
|
assert.strictEqual(histogram.resetCount, 1);
|
|
histogram.record(1);
|
|
const previous = histogram.snapshot();
|
|
histogram.reset();
|
|
assert.strictEqual(histogram.resetCount, 2);
|
|
assert.strictEqual(previous.resetCount, 1);
|
|
|
|
// A reset is detected even when every count has grown past its previous
|
|
// value since.
|
|
for (let i = 0; i < 10; i++) histogram.record(1);
|
|
assert.throws(() => histogram.diff(previous), {
|
|
code: 'ERR_INVALID_STATE',
|
|
});
|
|
|
|
// subtract() also removes values. Subtracting an empty histogram leaves
|
|
// every count unchanged.
|
|
const snapshot = histogram.snapshot();
|
|
assert.strictEqual(snapshot.resetCount, 2);
|
|
histogram.subtract(createHistogram());
|
|
assert.strictEqual(histogram.resetCount, 3);
|
|
assert.throws(() => histogram.diff(snapshot), {
|
|
code: 'ERR_INVALID_STATE',
|
|
});
|
|
}
|
|
|
|
{
|
|
const histogram = createHistogram();
|
|
for (const options of [{ lowest: 2 }, { highest: 1000 }, { figures: 2 }]) {
|
|
assert.throws(() => histogram.diff(createHistogram(options)), {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
});
|
|
}
|
|
|
|
// Histograms with a different normalizing index offset map values to
|
|
// different indexes.
|
|
const data = createHistogram().export();
|
|
const offset = Buffer.from(data).indexOf(Buffer.from([0x06, 0x00, 0x07, 0x00]));
|
|
assert.notStrictEqual(offset, -1);
|
|
data[offset + 3] = 1;
|
|
assert.throws(() => histogram.diff(importHistogram(data)), {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
});
|
|
|
|
assert.throws(() => histogram.diff.call({}, histogram), {
|
|
code: 'ERR_INVALID_THIS',
|
|
});
|
|
const { get } = Object.getOwnPropertyDescriptor(
|
|
Object.getPrototypeOf(histogram.snapshot()), 'resetCount');
|
|
assert.throws(() => get.call({}), { code: 'ERR_INVALID_THIS' });
|
|
const window = createSlidingWindowHistogram({ chunks: 1, recordsPerChunk: 1 });
|
|
for (const other of [undefined, null, {}, 1, window]) {
|
|
assert.throws(() => histogram.diff(other), {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
});
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
const histogram = monitorEventLoopDelay({ samplePerIteration: true });
|
|
histogram.enable();
|
|
while (histogram.count < 2) await delay(1);
|
|
const previous = histogram.snapshot();
|
|
while (histogram.count < previous.count + 3) await delay(1);
|
|
|
|
// Samples are only recorded while the event loop is running.
|
|
const current = histogram.snapshot();
|
|
assert.strictEqual(current.diff(previous).count,
|
|
current.count - previous.count);
|
|
|
|
histogram.disable();
|
|
histogram.reset();
|
|
assert.strictEqual(histogram.resetCount, 1);
|
|
assert.throws(() => histogram.diff(previous), {
|
|
code: 'ERR_INVALID_STATE',
|
|
});
|
|
})().then(common.mustCall());
|