class ScoutApm::NumericHistogram

Attributes

bins[R]
max_bins[R]
mutex[R]

This class should be threadsafe.

total[RW]

Public Class Methods

new(max_bins) click to toggle source
# File lib/scout_apm/histogram.rb, line 21
def initialize(max_bins)
  @max_bins = max_bins
  @bins = []
  @total = 0
  @mutex = Mutex.new
end

Public Instance Methods

add(new_value) click to toggle source
# File lib/scout_apm/histogram.rb, line 28
def add(new_value)
  mutex.synchronize do
    @total += 1
    create_new_bin(new_value.to_f)
    trim
  end
end
approximate_quantile_of_value(v) click to toggle source

Given a value, where in this histogram does it fall? Returns a float between 0 and 1

# File lib/scout_apm/histogram.rb, line 61
def approximate_quantile_of_value(v)
  mutex.synchronize do
    return 100 if total == 0

    count_examined = 0

    bins.each_with_index do |bin, index|
      if v <= bin.value
        break
      end

      count_examined += bin.count
    end

    count_examined / total.to_f
  end
end
as_json() click to toggle source
# File lib/scout_apm/histogram.rb, line 105
def as_json
  mutex.synchronize do
    bins.map{ |b|
      [
        ScoutApm::Utils::Numbers.round(b.value, 4),
        b.count
      ]
    }
  end
end
combine!(other) click to toggle source
# File lib/scout_apm/histogram.rb, line 90
def combine!(other)
  mutex.synchronize do
    other.mutex.synchronize do
      @bins = (other.bins + @bins).
        group_by {|b| b.value }.
        map {|val, bs| [val, bs.inject(0) {|sum, b| sum + b.count }] }.
        map {|val, sum| HistogramBin.new(val,sum) }.
        sort_by { |b| b.value }
      @total += other.total
      trim
      self
    end
  end
end
marshal_dump() click to toggle source
# File lib/scout_apm/histogram.rb, line 12
def marshal_dump
  [@max_bins, @bins, @total]
end
marshal_load(array) click to toggle source
# File lib/scout_apm/histogram.rb, line 16
def marshal_load(array)
  @max_bins, @bins, @total = array
  @mutex = Mutex.new
end
mean() click to toggle source
# File lib/scout_apm/histogram.rb, line 79
def mean
  mutex.synchronize do
    if total == 0
      return 0
    end

    sum = bins.inject(0) { |s, bin| s + (bin.value * bin.count) }
    return sum.to_f / total.to_f
  end
end
quantile(q) click to toggle source
# File lib/scout_apm/histogram.rb, line 36
def quantile(q)
  mutex.synchronize do
    return 0 if total == 0

    if q > 1
      q = q / 100.0
    end

    count = q.to_f * total.to_f

    bins.each_with_index do |bin, index|
      count -= bin.count

      if count <= 0
        return bin.value
      end
    end

    # If we fell through, we were asking for the last (max) value
    return bins[-1].value
  end
end

Private Instance Methods

create_new_bin(new_value) click to toggle source

If we exactly match an existing bin, add to it, otherwise create a new bin holding a count for the new value.

# File lib/scout_apm/histogram.rb, line 119
def create_new_bin(new_value)
  bins.each_with_index do |bin, index|
    # If it matches exactly, increment the bin's count
    if bin.value == new_value
      bin.count += 1
      return
    end

    # We've gone one bin too far, so insert before the current bin.
    if bin.value > new_value
      # Insert at this index
      new_bin = HistogramBin.new(new_value, 1)
      bins.insert(index, new_bin)
      return
    end
  end

  # If we get to here, the bin needs to be added to the end.
  bins << HistogramBin.new(new_value, 1)
end
trim() click to toggle source
# File lib/scout_apm/histogram.rb, line 140
def trim
  while bins.length > max_bins
    trim_one
  end
end
trim_one() click to toggle source
# File lib/scout_apm/histogram.rb, line 146
def trim_one
  minDelta = Float::MAX
  minDeltaIndex = 0

  # Which two bins should we merge?
  bins.each_with_index do |_, index|
    next if index == 0

    delta = bins[index].value - bins[index - 1].value
    if delta < minDelta
      minDelta = delta
      minDeltaIndex = index
    end
  end

  # Create the merged bin with summed count, and weighted value
  mergedCount = bins[minDeltaIndex - 1].count + bins[minDeltaIndex].count
  mergedValue = (
    bins[minDeltaIndex - 1].value * bins[minDeltaIndex - 1].count +
    bins[minDeltaIndex].value     * bins[minDeltaIndex].count
    ) / mergedCount

  mergedBin = HistogramBin.new(mergedValue, mergedCount)

  # Remove the two bins we just merged together, then add the merged one
  bins.slice!(minDeltaIndex - 1, 2)
  bins.insert(minDeltaIndex - 1, mergedBin)
rescue => e
  ScoutApm::Agent.instance.context.logger.info("Error in NumericHistogram#trim_one. #{e.message}, #{e.backtrace}, #{self.inspect}")
  raise
end