/* * Copyright 2011 LMAX Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.lmax.disruptor.collections; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Arrays; /** * Histogram for tracking the frequency of observations of values below interval upper bounds. * * This class is useful for recording timings in nanoseconds across a large number of observations * when high performance is required. */ public final class Histogram { private final long[] upperBounds; private final long[] counts; private long minValue = Long.MAX_VALUE; private long maxValue = 0L; /** * Create a new Histogram with a provided list of interval bounds. * * @param upperBounds of the intervals. */ public Histogram(final long[] upperBounds) { validateBounds(upperBounds); this.upperBounds = Arrays.copyOf(upperBounds, upperBounds.length); this.counts = new long[upperBounds.length]; } private void validateBounds(final long[] upperBounds) { long lastBound = -1L; for (final long bound : upperBounds) { if (bound <= 0L) { throw new IllegalArgumentException("Bounds must be positive values"); } if (bound <= lastBound) { throw new IllegalArgumentException("bound " + bound + " is not greater than " + lastBound); } lastBound = bound; } } /** * Size of the list of interval bars. * * @return size of the interval bar list. */ public int getSize() { return upperBounds.length; } /** * Get the upper bound of an interval for an index. * * @param index of the upper bound. * @return the interval upper bound for the index. */ public long getUpperBoundAt(final int index) { return upperBounds[index]; } /** * Get the count of observations at a given index. * * @param index of the observations counter. * @return the count of observations at a given index. */ public long getCountAt(final int index) { return counts[index]; } /** * Add an observation to the histogram and increment the counter for the interval it matches. * * @param value for the observation to be added. * @return return true if in the range of intervals otherwise false. */ public boolean addObservation(final long value) { int low = 0; int high = upperBounds.length - 1; while (low < high) { int mid = low + ((high - low) >> 1); if (upperBounds[mid] < value) { low = mid + 1; } else { high = mid; } } if (value <= upperBounds[high]) { counts[high]++; trackRange(value); return true; } return false; } private void trackRange(final long value) { if (value < minValue) { minValue = value; } if (value > maxValue) { maxValue = value; } } /** * Add observations from another Histogram into this one. * Histograms must have the same intervals. * * @param histogram from which to add the observation counts. */ public void addObservations(final Histogram histogram) { if (upperBounds.length != histogram.upperBounds.length) { throw new IllegalArgumentException("Histograms must have matching intervals"); } for (int i = 0, size = upperBounds.length; i < size; i++) { if (upperBounds[i] != histogram.upperBounds[i]) { throw new IllegalArgumentException("Histograms must have matching intervals"); } } for (int i = 0, size = counts.length; i < size; i++) { counts[i] += histogram.counts[i]; } trackRange(histogram.minValue); trackRange(histogram.maxValue); } /** * Clear the list of interval counters. */ public void clear() { maxValue = 0L; minValue = Long.MAX_VALUE; for (int i = 0, size = counts.length; i < size; i++) { counts[i] = 0L; } } /** * Count total number of recorded observations. * * @return the total number of recorded observations. */ public long getCount() { long count = 0L; for (int i = 0, size = counts.length; i < size; i++) { count += counts[i]; } return count; } /** * Get the minimum observed value. * * @return the minimum value observed. */ public long getMin() { return minValue; } /** * Get the maximum observed value. * * @return the maximum of the observed values; */ public long getMax() { return maxValue; } /** * Calculate the mean of all recorded observations. * * The mean is calculated by the summing the mid points of each interval multiplied by the count * for that interval, then dividing by the total count of observations. The max and min are * considered for adjusting the top and bottom bin when calculating the mid point. * * @return the mean of all recorded observations. */ public BigDecimal getMean() { if (0L == getCount()) { return BigDecimal.ZERO; } long lowerBound = counts[0] > 0L ? minValue : 0L; BigDecimal total = BigDecimal.ZERO; for (int i = 0, size = upperBounds.length; i < size; i++) { if (0L != counts[i]) { long upperBound = Math.min(upperBounds[i], maxValue); long midPoint = lowerBound + ((upperBound - lowerBound) / 2L); BigDecimal intervalTotal = new BigDecimal(midPoint).multiply(new BigDecimal(counts[i])); total = total.add(intervalTotal); } lowerBound = Math.max(upperBounds[i] + 1L, minValue); } return total.divide(new BigDecimal(getCount()), 2, RoundingMode.HALF_UP); } /** * Calculate the upper bound within which 99% of observations fall. * * @return the upper bound for 99% of observations. */ public long getTwoNinesUpperBound() { return getUpperBoundForFactor(0.99d); } /** * Calculate the upper bound within which 99.99% of observations fall. * * @return the upper bound for 99.99% of observations. */ public long getFourNinesUpperBound() { return getUpperBoundForFactor(0.9999d); } /** * Get the interval upper bound for a given factor of the observation population. * * @param factor representing the size of the population. * @return the interval upper bound. */ public long getUpperBoundForFactor(final double factor) { if (0.0d >= factor || factor >= 1.0d) { throw new IllegalArgumentException("factor must be >= 0.0 and <= 1.0"); } final long totalCount = getCount(); final long tailTotal = totalCount - Math.round(totalCount * factor); long tailCount = 0L; for (int i = counts.length - 1; i >= 0; i--) { if (0L != counts[i]) { tailCount += counts[i]; if (tailCount >= tailTotal) { return upperBounds[i]; } } } return 0L; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Histogram{"); sb.append("min=").append(getMin()).append(", "); sb.append("max=").append(getMax()).append(", "); sb.append("mean=").append(getMean()).append(", "); sb.append("99%=").append(getTwoNinesUpperBound()).append(", "); sb.append("99.99%=").append(getFourNinesUpperBound()).append(", "); sb.append('['); for (int i = 0, size = counts.length; i < size; i++) { sb.append(upperBounds[i]).append('=').append(counts[i]).append(", "); } if (counts.length > 0) { sb.setLength(sb.length() - 2); } sb.append(']'); sb.append('}'); return sb.toString(); } }