current commit
260925.5 · perf_hooks: add histogram.diff()
'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());