Java tutorial
/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.google.api.control.model; import com.google.api.servicecontrol.v1.Distribution; import com.google.api.servicecontrol.v1.Distribution.BucketOptionCase; import com.google.api.servicecontrol.v1.Distribution.Builder; import com.google.api.servicecontrol.v1.Distribution.ExplicitBuckets; import com.google.api.servicecontrol.v1.Distribution.ExponentialBuckets; import com.google.api.servicecontrol.v1.Distribution.LinearBuckets; import com.google.common.collect.Sets; import com.google.common.math.DoubleMath; import com.google.common.primitives.Doubles; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Utility methods for working with {@link Distribution} instances. */ public final class Distributions { private static final String MSG_BUCKET_COUNTS_MISMATCH = "Bucket counts do not match"; private static final String MSG_BUCKET_OPTIONS_MISMATCH = "Bucket options do not match"; private static final String MSG_UNKNOWN_BUCKET_OPTION_TYPE = "Unknown bucket option type"; private static final String MSG_SOME_BOUNDS_ARE_THE_SAME = "Illegal bounds, at least two bounds are the same!"; private static final String MSG_BAD_DIST_LOW_BUCKET_COUNT = "cannot update a distribution with a low bucket count"; private static final String MSG_DOUBLE_TOO_LOW = "%s should be > %f"; private static final String MSG_BAD_NUM_FINITE_BUCKETS = "number of finite buckets should be > 0"; private static final double TOLERANCE = 1e-5; private static final Logger log = Logger.getLogger(Distributions.class.getName()); private Distributions() { } /** * Creates an {@code Distribution} with {@code ExponentialBuckets}. * * @param numFiniteBuckets initializes the number of finite buckets * @param growthFactor initializes the growth factor * @param scale initializes the scale * @return a {@code Distribution} with {@code ExponentialBuckets} * @throws IllegalArgumentException if a bad input prevents creation. */ public static Distribution createExponential(int numFiniteBuckets, double growthFactor, double scale) { if (numFiniteBuckets <= 0) { throw new IllegalArgumentException(MSG_BAD_NUM_FINITE_BUCKETS); } if (growthFactor <= 1.0) { throw new IllegalArgumentException(String.format(MSG_DOUBLE_TOO_LOW, "growth factor", 1.0)); } if (scale <= 0.0) { throw new IllegalArgumentException(String.format(MSG_DOUBLE_TOO_LOW, "scale", 0.0)); } ExponentialBuckets buckets = ExponentialBuckets.newBuilder().setGrowthFactor(growthFactor) .setNumFiniteBuckets(numFiniteBuckets).setScale(scale).build(); Builder builder = Distribution.newBuilder().setExponentialBuckets(buckets); for (int i = 0; i < numFiniteBuckets + 2; i++) { builder.addBucketCounts(0L); } return builder.build(); } /** * Creates a {@code Distribution} with {@code LinearBuckets}. * * @param numFiniteBuckets initializes the number of finite buckets * @param width initializes the width of each bucket * @param offset initializes the offset of the start bucket * @return a {@code Distribution} with {@code LinearBuckets} * @throws IllegalArgumentException if a bad input prevents creation. */ public static Distribution createLinear(int numFiniteBuckets, double width, double offset) { if (numFiniteBuckets <= 0) { throw new IllegalArgumentException(MSG_BAD_NUM_FINITE_BUCKETS); } if (width <= 0.0) { throw new IllegalArgumentException(String.format(MSG_DOUBLE_TOO_LOW, "width", 0.0)); } LinearBuckets buckets = LinearBuckets.newBuilder().setOffset(offset).setWidth(width) .setNumFiniteBuckets(numFiniteBuckets).build(); Builder builder = Distribution.newBuilder().setLinearBuckets(buckets); for (int i = 0; i < numFiniteBuckets + 2; i++) { builder.addBucketCounts(0L); } return builder.build(); } /** * Creates a {@code Distribution} with {@code ExplicitBuckets}. * * @param bounds initializes the bounds used to define the explicit buckets * * @return a {@code Distribution} with {@code ExplicitBuckets} * @throws IllegalArgumentException if a bad input prevents creation. */ public static Distribution createExplicit(double[] bounds) { List<Double> allBounds = Doubles.asList(bounds); Set<Double> uniqueBounds = Sets.newHashSet(allBounds); if (allBounds.size() != uniqueBounds.size()) { throw new IllegalArgumentException(MSG_SOME_BOUNDS_ARE_THE_SAME); } Collections.sort(allBounds); ExplicitBuckets buckets = ExplicitBuckets.newBuilder().addAllBounds(allBounds).build(); Builder builder = Distribution.newBuilder().setExplicitBuckets(buckets); for (int i = 0; i < allBounds.size() + 1; i++) { builder.addBucketCounts(0L); } return builder.build(); } /** * Updates as new distribution that contains value added to an existing one. * * @param value the sample value * @param distribution a {@code Distribution} * @return the updated distribution */ public static Distribution addSample(double value, Distribution distribution) { Builder builder = distribution.toBuilder(); switch (distribution.getBucketOptionCase()) { case EXPLICIT_BUCKETS: updateStatistics(value, builder); updateExplicitBuckets(value, builder); return builder.build(); case EXPONENTIAL_BUCKETS: updateStatistics(value, builder); updateExponentialBuckets(value, builder); return builder.build(); case LINEAR_BUCKETS: updateStatistics(value, builder); updateLinearBuckets(value, builder); return builder.build(); default: throw new IllegalArgumentException(MSG_UNKNOWN_BUCKET_OPTION_TYPE); } } /** * Merge {@code prior} with {@code latest}. * * @param prior a {@code Distribution} instance * @param latest a {@code Distribution}, expected to be a later version of {@code prior} * * @return a new {@code Distribution} that combines the statistics and buckets of the earlier two * @throws IllegalArgumentException if the bucket options of {@code prior} and {@code latest} * don't match * @throws IllegalArgumentException if the bucket counts of {@code prior} and {@code latest} dont' * match */ public static Distribution merge(Distribution prior, Distribution latest) { if (!bucketsNearlyEquals(prior, latest)) { throw new IllegalArgumentException(MSG_BUCKET_OPTIONS_MISMATCH); } if (prior.getBucketCountsCount() != latest.getBucketCountsCount()) { throw new IllegalArgumentException(MSG_BUCKET_COUNTS_MISMATCH); } if (prior.getCount() == 0) { return latest; } // Merge the distribution statistics Builder builder = latest.toBuilder(); long oldCount = latest.getCount(); double oldMean = latest.getMean(); builder.setCount(prior.getCount() + oldCount); builder.setMaximum(Math.max(prior.getMaximum(), latest.getMaximum())); builder.setMinimum(Math.min(prior.getMinimum(), latest.getMinimum())); double newMean = (oldCount * oldMean + prior.getCount() * prior.getMean()) / builder.getCount(); builder.setMean(newMean); double oldSumOfSquaredDeviation = latest.getSumOfSquaredDeviation(); double newSumOfSquaredDeviation = oldSumOfSquaredDeviation + prior.getSumOfSquaredDeviation() + (oldCount * Math.pow((builder.getMean() - oldMean), 2)) + (prior.getCount() * Math.pow((builder.getMean() - prior.getMean()), 2)); builder.setSumOfSquaredDeviation(newSumOfSquaredDeviation); // Merge the bucket counts for (int i = 0; i < latest.getBucketCountsCount(); i++) { builder.setBucketCounts(i, prior.getBucketCounts(i) + latest.getBucketCounts(i)); } return builder.build(); } private static boolean bucketsNearlyEquals(Distribution a, Distribution b) { BucketOptionCase caseA = a.getBucketOptionCase(); BucketOptionCase caseB = b.getBucketOptionCase(); if (caseA != caseB) { return false; } switch (caseA) { case EXPLICIT_BUCKETS: return bucketsNearlyEquals(a.getExplicitBuckets(), b.getExplicitBuckets()); case EXPONENTIAL_BUCKETS: return bucketsNearlyEquals(a.getExponentialBuckets(), b.getExponentialBuckets()); case LINEAR_BUCKETS: return bucketsNearlyEquals(a.getLinearBuckets(), b.getLinearBuckets()); default: return false; } } private static boolean bucketsNearlyEquals(ExplicitBuckets a, ExplicitBuckets b) { if (a.getBoundsCount() != b.getBoundsCount()) { return false; } for (int i = 0; i < b.getBoundsCount(); i++) { if (!DoubleMath.fuzzyEquals(a.getBounds(i), b.getBounds(i), TOLERANCE)) { return false; } } return true; } private static boolean bucketsNearlyEquals(ExponentialBuckets a, ExponentialBuckets b) { return ((a.getNumFiniteBuckets() == b.getNumFiniteBuckets()) && (DoubleMath.fuzzyEquals(a.getGrowthFactor(), b.getGrowthFactor(), TOLERANCE) && (DoubleMath.fuzzyEquals(a.getScale(), b.getScale(), TOLERANCE)))); } private static boolean bucketsNearlyEquals(LinearBuckets a, LinearBuckets b) { return ((a.getNumFiniteBuckets() == b.getNumFiniteBuckets()) && (DoubleMath.fuzzyEquals(a.getWidth(), b.getWidth(), TOLERANCE) && (DoubleMath.fuzzyEquals(a.getOffset(), b.getOffset(), TOLERANCE)))); } private static void updateStatistics(double value, Builder distribution) { long oldCount = distribution.getCount(); if (oldCount == 0) { distribution.setCount(1); distribution.setMaximum(value); distribution.setMinimum(value); distribution.setMean(value); distribution.setSumOfSquaredDeviation(0); } else { double oldMean = distribution.getMean(); double newMean = ((oldCount * oldMean) + value) / (oldCount + 1); double deltaSumOfSquares = (value - oldMean) * (value - newMean); distribution.setCount(oldCount + 1); distribution.setMean(newMean); distribution.setMaximum(Math.max(value, distribution.getMaximum())); distribution.setMinimum(Math.min(value, distribution.getMinimum())); distribution.setSumOfSquaredDeviation(deltaSumOfSquares + distribution.getSumOfSquaredDeviation()); } } private static void updateLinearBuckets(double value, Builder distribution) { LinearBuckets buckets = distribution.getLinearBuckets(); if (distribution.getBucketCountsCount() != buckets.getNumFiniteBuckets() + 2) { throw new IllegalArgumentException(MSG_BAD_DIST_LOW_BUCKET_COUNT); } // Determine the offset the value fits into double upper = buckets.getWidth() * buckets.getNumFiniteBuckets() + buckets.getOffset(); int index = 0; if (value >= upper) { index = buckets.getNumFiniteBuckets() + 1; } else if (value > buckets.getOffset()) { index = 1 + ((int) Math.round((value - buckets.getOffset()) / buckets.getWidth())); } long newCount = distribution.getBucketCounts(index) + 1; log.log(Level.FINE, "Updating explicit bucket {0} to {1} for {2}", new Object[] { index, newCount, value }); distribution.setBucketCounts(index, newCount); } private static void updateExponentialBuckets(double value, Builder distribution) { ExponentialBuckets buckets = distribution.getExponentialBuckets(); if (distribution.getBucketCountsCount() != buckets.getNumFiniteBuckets() + 2) { throw new IllegalArgumentException(MSG_BAD_DIST_LOW_BUCKET_COUNT); } // Determine the offset the value fits into int index = 0; if (value > buckets.getScale()) { index = 1 + (int) (Math.log(value / buckets.getScale()) / Math.log(buckets.getGrowthFactor())); index = Math.min(buckets.getNumFiniteBuckets() + 1, index); } long newCount = distribution.getBucketCounts(index) + 1; if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Updating explicit bucket {0} to {1} for {2}", new Object[] { index, newCount, value }); } distribution.setBucketCounts(index, newCount); } private static void updateExplicitBuckets(double value, Builder distribution) { ExplicitBuckets buckets = distribution.getExplicitBuckets(); if (distribution.getBucketCountsCount() != buckets.getBoundsCount() + 1) { throw new IllegalArgumentException(MSG_BAD_DIST_LOW_BUCKET_COUNT); } // Determine the offset for the value using Collections.binarySearch. // // Note that when the value is not in the list the result of // Collections.binarySearch is -ve: - (insertion point) - 1. int index = Collections.binarySearch(buckets.getBoundsList(), value); if (index < 0) { index = -index - 1; } else { index += 1; } long newCount = distribution.getBucketCounts(index) + 1; if (log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Updating explicit bucket {0} to {1} for {2}", new Object[] { index, newCount, value }); } distribution.setBucketCounts(index, newCount); } }