const { MetricTypes } = require('./Metric');
const binarySearch = require('binary-search');
const EDS = require('../util/ExponentiallyDecayingSample');
/**
* Keeps a reservoir of statistically relevant values biased towards the last 5 minutes to explore their distribution.
* @implements {Metric}
* @example
* var Measured = require('measured')
* var histogram = new Measured.Histogram();
* http.createServer(function(req, res) {
* if (req.headers['content-length']) {
* histogram.update(parseInt(req.headers['content-length'], 10));
* }
* });
*/
class Histogram {
/**
@param {HistogramProperties} [properties] see {@link HistogramProperties}.
*/
constructor(properties) {
this._properties = properties || {};
this._initializeState();
}
_initializeState() {
this._sample = this._properties.sample || new EDS();
this._percentilesMethod = this._properties.percentilesMethod || this._percentiles;
this._min = null;
this._max = null;
this._count = 0;
this._sum = 0;
// These are for the Welford algorithm for calculating running constiance
// without floating-point doom.
this._constianceM = 0;
this._constianceS = 0;
}
/**
* Pushes value into the sample. timestamp defaults to Date.now().
* @param {number} value
*/
update(value) {
this._count++;
this._sum += value;
this._sample.update(value);
this._updateMin(value);
this._updateMax(value);
this._updateVariance(value);
}
_percentiles(percentiles) {
const values = this._sample.toArray().sort((a, b) => {
return a === b ? 0 : a - b;
});
const results = {};
let i, percentile, pos, lower, upper;
for (i = 0; i < percentiles.length; i++) {
percentile = percentiles[i];
if (values.length) {
pos = percentile * (values.length + 1);
if (pos < 1) {
results[percentile] = values[0];
} else if (pos >= values.length) {
results[percentile] = values[values.length - 1];
} else {
lower = values[Math.floor(pos) - 1];
upper = values[Math.ceil(pos) - 1];
results[percentile] = lower + (pos - Math.floor(pos)) * (upper - lower);
}
} else {
results[percentile] = null;
}
}
return results;
}
weightedPercentiles(percentiles) {
const values = this._sample.toArrayWithWeights().sort((a, b) => {
return a.value === b.value ? 0 : a.value - b.value;
});
const sumWeight = values.reduce((sum, sample) => {
return sum + sample.priority;
}, 0);
const normWeights = values.map(value => {
return value.priority / sumWeight;
});
const quantiles = [0];
let i;
for (i = 1; i < values.length; i++) {
quantiles[i] = quantiles[i - 1] + normWeights[i - 1];
}
function gt(a, b) {
return a - b;
}
const results = {};
let percentile, pos;
for (i = 0; i < percentiles.length; i++) {
percentile = percentiles[i];
if (values.length) {
pos = binarySearch(quantiles, percentile, gt);
if (pos < 0) {
results[percentile] = values[-pos - 1 - 1].value;
} else if (pos < 1) {
results[percentile] = values[0].value;
} else if (pos >= values.length) {
results[percentile] = values[values.length - 1].value;
}
} else {
results[percentile] = null;
}
}
return results;
}
/**
* Resets all values. Histograms initialized with custom options will be reset to the default settings (patch welcome).
*/
reset() {
// while this is technically a bug?, copying existing logic to maintain current api,
// TODO reset should reset the sample, not override it with a new EDS()
this._properties.sample = new EDS();
this._initializeState();
}
/**
* Checks whether the histogram contains values.
* @return {boolean} Whether the histogram contains values.
*/
hasValues() {
return this._count > 0;
}
/**
* @return {HistogramData}
*/
toJSON() {
const percentiles = this._percentilesMethod([0.5, 0.75, 0.95, 0.99, 0.999]);
return {
min: this._min,
max: this._max,
sum: this._sum,
variance: this._calculateVariance(),
mean: this._calculateMean(),
stddev: this._calculateStddev(),
count: this._count,
median: percentiles[0.5],
p75: percentiles[0.75],
p95: percentiles[0.95],
p99: percentiles[0.99],
p999: percentiles[0.999]
};
}
_updateMin(value) {
if (this._min === null || value < this._min) {
this._min = value;
}
}
_updateMax(value) {
if (this._max === null || value > this._max) {
this._max = value;
}
}
_updateVariance(value) {
if (this._count === 1) {
this._constianceM = value;
return value;
}
const oldM = this._constianceM;
this._constianceM += (value - oldM) / this._count;
this._constianceS += (value - oldM) * (value - this._constianceM);
// TODO is this right, above it returns in the if statement but does nothing but update internal state for the else case?
return undefined;
}
/**
*
* @return {number|null}
* @private
*/
_calculateMean() {
return this._count === 0 ? 0 : this._sum / this._count;
}
/**
* @return {number|null}
* @private
*/
_calculateVariance() {
return this._count <= 1 ? null : this._constianceS / (this._count - 1);
}
/**
* @return {number|null}
* @private
*/
_calculateStddev() {
return this._count < 1 ? null : Math.sqrt(this._calculateVariance());
}
/**
* The type of the Metric Impl. {@link MetricTypes}.
* @return {string} The type of the Metric Impl.
*/
getType() {
return MetricTypes.HISTOGRAM;
}
}
module.exports = Histogram;
/**
* Properties to create a {@link Histogram} with.
*
* @interface HistogramProperties
* @typedef HistogramProperties
* @type {Object}
* @property {object} sample The sample reservoir to use. Defaults to an ExponentiallyDecayingSample.
*/
/**
* The data returned from Histogram::toJSON()
* @interface HistogramData
* @typedef HistogramData
* @typedef {object}
* @property {number|null} min The lowest observed value.
* @property {number|null} max The highest observed value.
* @property {number|null} sum The sum of all observed values.
* @property {number|null} variance The variance of all observed values.
* @property {number|null} mean The average of all observed values.
* @property {number|null} stddev The stddev of all observed values.
* @property {number} count The number of observed values.
* @property {number} median 50% of all values in the resevoir are at or below this value.
* @property {number} p75 See median, 75% percentile.
* @property {number} p95 See median, 95% percentile.
* @property {number} p99 See median, 99% percentile.
* @property {number} p999 See median, 99.9% percentile.
*/