/******************************************************************************* * Copyright (c) 2012-2015 INRIA. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Generoso Pagano - initial API and implementation ******************************************************************************/ /* * Copyright 2011 LMAX Ltd. * * NOTICE: this file has been modified by Generoso Pagano (2013). * * 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. * * Small contributions by Generoso Pagano: * - class name changed from Histogram to HistogramImpl * - class scope is package-private * - methods: * - getIndexForValue() * - getProbabilityAt() * - getUpperBounds() * - fields: * - count */ package fr.inria.soctrace.lib.query.distribution; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Arrays; /** * Implementation of the {@link Histogram} interface. * * @author "Generoso Pagano <generoso.pagano@inria.fr>" */ final class HistogramImpl implements Histogram { // tracks the upper intervals of each of the buckets/bars private final long[] upperBounds; // tracks the count of the corresponding bucket private final long[] counts; // minimum value so far observed private long minValue = Long.MAX_VALUE; // maximum value so far observed private long maxValue = 0L; // tracks the total count (Generoso Pagano) private long count = 0; /** * Create a new Histogram with a provided list of interval bounds. * * @param upperBounds * of the intervals. Bounds must be provided in order least to * greatest, and lowest bound must be greater than or equal to 1. * @throws IllegalArgumentException * if any of the upper bounds are less than or equal to zero * @throws IllegalArgumentException * if the bounds are not in order, least to greatest */ public HistogramImpl(final long[] upperBounds) { validateBounds(upperBounds); this.upperBounds = Arrays.copyOf(upperBounds, upperBounds.length); this.counts = new long[upperBounds.length]; } /** * Validates the input bounds; used by constructor only. */ private void validateBounds(final long[] upperBounds) { long lastBound = -1L; if (upperBounds.length <= 0) { throw new IllegalArgumentException("Must provide at least one interval"); } 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; } } @Override public int getSize() { return upperBounds.length; } @Override public long getUpperBoundAt(final int index) { return upperBounds[index]; } @Override public long getCountAt(final int index) { return counts[index]; } @Override public boolean addObservation(final long value) { int low = 0; int high = upperBounds.length - 1; // do a classic binary search to find the high value while (low < high) { int mid = low + ((high - low) >> 1); if (upperBounds[mid] < value) { low = mid + 1; } else { high = mid; } } // if the binary search found an eligible bucket, increment if (value <= upperBounds[high]) { counts[high]++; count++; // Generoso Pagano trackRange(value); return true; } // otherwise value was not found return false; } @Override public void addObservations(final Histogram histogram) { // validate the intervals if (upperBounds.length != histogram.getSize()) { throw new IllegalArgumentException("Histograms must have matching intervals"); } for (int i = 0, size = upperBounds.length; i < size; i++) { if (upperBounds[i] != histogram.getUpperBoundAt(i)) { throw new IllegalArgumentException("Histograms must have matching intervals"); } } // increment all of the internal counts for (int i = 0, size = counts.length; i < size; i++) { counts[i] += histogram.getCountAt(i); } count = histogram.getCount(); // refresh the minimum and maximum observation ranges trackRange(histogram.getMin()); trackRange(histogram.getMax()); } /** * Keep min and max values updated */ private void trackRange(final long value) { if (value < minValue) { minValue = value; } if (value > maxValue) { maxValue = value; } } @Override public void clear() { maxValue = 0L; minValue = Long.MAX_VALUE; for (int i = 0, size = counts.length; i < size; i++) { counts[i] = 0L; } count = 0; // Generoso Pagano } @Override public long getCount() { return count; // Generoso Pagano } @Override public long getMin() { return minValue; } @Override public long getMax() { return maxValue; } @Override public BigDecimal getMean() { // early exit to avoid divide by zero later if (0L == getCount()) { return BigDecimal.ZERO; } // precalculate the initial lower bound; needed in the loop long lowerBound = counts[0] > 0L ? minValue : 0L; // use BigDecimal to avoid precision errors BigDecimal total = BigDecimal.ZERO; // midpoint is calculated as the average between the lower and upper // bound // (after taking into account the min & max values seen) // then, simply multiply midpoint by the count of values at the interval // (intervalTotal) // and add to running total (total) 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); } // and recalculate the lower bound for the next time around the loop lowerBound = Math.max(upperBounds[i] + 1L, minValue); } return total.divide(new BigDecimal(getCount()), 2, RoundingMode.HALF_UP); } @Override public long getTwoNinesUpperBound() { return getUpperBoundForFactor(0.99d); } @Override public long getFourNinesUpperBound() { return getUpperBoundForFactor(0.9999d); } @Override 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; // reverse search the intervals ('tailCount' from end) 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(); } /* * Added by Generoso Pagano */ @Override public int getIndexForValue(final long value) { if (value < 0 || value > upperBounds[upperBounds.length - 1]) return -1; 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; } return high; } @Override public BigDecimal getProbabilityAt(final int index) { if (count == 0) return BigDecimal.ZERO; return new BigDecimal(counts[index] / (double) count); } @Override public long[] getUpperBounds() { return Arrays.copyOf(upperBounds, upperBounds.length); } }