keymetrics/pm2-io-apm

View on GitHub
src/utils/metrics/histogram.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import EDS from '../EDS'

export default class Histogram {
  private _measurement
  private _callFn

  private _sample = new EDS()
  private _min
  private _max
  private _count: number = 0
  private _sum: number = 0

  // These are for the Welford algorithm for calculating running variance
  // without floating-point doom.
  private _varianceM: number = 0
  private _varianceS: number = 0
  private _ema: number = 0

  private used: boolean = false

  constructor (opts?) {
    opts = opts || {}

    this._measurement = opts.measurement
    this._callFn = null

    const methods = {
      min      : this.getMin,
      max      : this.getMax,
      sum      : this.getSum,
      count    : this.getCount,
      variance : this._calculateVariance,
      mean     : this._calculateMean,
      // stddev   : this._calculateStddev,
      ema      : this.getEma()
    }

    if (methods.hasOwnProperty(this._measurement)) {
      this._callFn = methods[this._measurement]
    } else {
      this._callFn = function () {
        const percentiles = this.percentiles([0.5, 0.75, 0.95, 0.99, 0.999])

        const medians = {
          median   : percentiles[0.5],
          p75      : percentiles[0.75],
          p95      : percentiles[0.95],
          p99      : percentiles[0.99],
          p999     : percentiles[0.999]
        }

        return medians[this._measurement]
      }
    }
  }

  update (value: number) {
    this.used = true
    this._count++
    this._sum += value

    this._sample.update(value)
    this._updateMin(value)
    this._updateMax(value)
    this._updateVariance(value)
    this._updateEma(value)
  }

  percentiles (percentiles) {
    const values = this._sample
      .toArray()
      .sort(function (a, b) {
        return (a === b)
          ? 0
          : a - b
      })

    const results = {}
    for (let i = 0; i < percentiles.length; i++) {
      const percentile = percentiles[i]
      if (!values.length) {
        results[percentile] = null
        continue
      }

      const pos = percentile * (values.length + 1)

      if (pos < 1) {
        results[percentile] = values[0]
      } else if (pos >= values.length) {
        results[percentile] = values[values.length - 1]
      } else {
        const lower = values[Math.floor(pos) - 1]
        const upper = values[Math.ceil(pos) - 1]

        results[percentile] = lower + (pos - Math.floor(pos)) * (upper - lower)
      }
    }

    return results
  }

  val () {
    if (typeof(this._callFn) === 'function') {
      return this._callFn()
    } else {
      return this._callFn
    }
  }

  getMin () {
    return this._min
  }

  getMax () {
    return this._max
  }

  getSum () {
    return this._sum
  }

  getCount () {
    return this._count
  }

  getEma () {
    return this._ema
  }

  fullResults () {
    const percentiles = this.percentiles([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],
      ema      : this._ema
    }
  }

  _updateMin (value) {
    if (this._min === undefined || value < this._min) {
      this._min = value
    }
  }

  _updateMax (value) {
    if (this._max === undefined || value > this._max) {
      this._max = value
    }
  }

  _updateVariance (value) {
    if (this._count === 1) return this._varianceM = value

    const oldM = this._varianceM

    this._varianceM += ((value - oldM) / this._count)
    this._varianceS += ((value - oldM) * (value - this._varianceM))
  }

  _updateEma (value) {
    if (this._count <= 1) return this._ema = this._calculateMean()
    const alpha = 2 / (1 + this._count)
    this._ema = value * alpha + this._ema * (1 - alpha)
  }

  _calculateMean () {
    return (this._count === 0)
      ? 0
      : this._sum / this._count
  }

  _calculateVariance () {
    return (this._count <= 1)
      ? null
      : this._varianceS / (this._count - 1)
  }

  isUsed () {
    return this.used
  }

  // TODO still used ?
  // _calculateStddev () {
  //   return (this._count < 1)
  //     ? null
  //     : Math.sqrt(this._calculateVariance())
  // }
}