diff --git a/java/google/registry/monitoring/metrics/BUILD b/java/google/registry/monitoring/metrics/BUILD
index 7d1739c5f..2b58e2426 100644
--- a/java/google/registry/monitoring/metrics/BUILD
+++ b/java/google/registry/monitoring/metrics/BUILD
@@ -18,6 +18,7 @@ java_library(
"//java/com/google/common/collect",
"//java/com/google/common/html",
"//java/com/google/common/math",
+ "//java/com/google/common/primitives",
"//java/com/google/common/util/concurrent",
"//third_party/java/appengine:appengine-api",
"//third_party/java/auto:auto_value",
diff --git a/java/google/registry/monitoring/metrics/CustomFitter.java b/java/google/registry/monitoring/metrics/CustomFitter.java
new file mode 100644
index 000000000..c4c556f09
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/CustomFitter.java
@@ -0,0 +1,53 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static google.registry.monitoring.metrics.MetricsUtils.checkDouble;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Ordering;
+
+/**
+ * Models a {@link DistributionFitter} with arbitrary sized intervals.
+ *
+ *
If only only one boundary is provided, then the fitter will consist of an overflow and
+ * underflow interval separated by that boundary.
+ */
+@AutoValue
+public abstract class CustomFitter implements DistributionFitter {
+
+ /**
+ * Create a new {@link CustomFitter} with the given interval boundaries.
+ *
+ * @param boundaries is a sorted list of interval boundaries
+ * @throws IllegalArgumentException if {@code boundaries} is empty or not sorted in ascending
+ * order, or if a value in the set is infinite, {@code NaN}, or {@code -0.0}.
+ */
+ public static CustomFitter create(ImmutableSet boundaries) {
+ checkArgument(boundaries.size() > 0, "boundaries must not be empty");
+ checkArgument(Ordering.natural().isOrdered(boundaries), "boundaries must be sorted");
+ for (Double d : boundaries) {
+ checkDouble(d);
+ }
+
+ return new AutoValue_CustomFitter(ImmutableSortedSet.copyOf(boundaries));
+ }
+
+ @Override
+ public abstract ImmutableSortedSet boundaries();
+}
diff --git a/java/google/registry/monitoring/metrics/Distribution.java b/java/google/registry/monitoring/metrics/Distribution.java
new file mode 100644
index 000000000..81c9d83de
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/Distribution.java
@@ -0,0 +1,46 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import com.google.common.collect.ImmutableRangeMap;
+
+/**
+ * Models a distribution of double-precision floating point sample data, and provides summary
+ * statistics of the distribution. This class also models the probability density function (PDF) of
+ * the distribution with a histogram.
+ *
+ * The summary statistics provided are the mean and sumOfSquaredDeviation of the distribution.
+ *
+ *
The histogram fitting function is provided via a {@link DistributionFitter} implementation.
+ *
+ * @see DistributionFitter
+ */
+public interface Distribution {
+
+ /** Returns the mean of this distribution. */
+ double mean();
+
+ /** Returns the sum of squared deviations from the mean of this distribution. */
+ double sumOfSquaredDeviation();
+
+ /** Returns the count of samples in this distribution. */
+ long count();
+
+ /** Returns a histogram of the distribution's values. */
+ ImmutableRangeMap intervalCounts();
+
+ /** Returns the {@link DistributionFitter} of this distribution. */
+ DistributionFitter distributionFitter();
+}
diff --git a/java/google/registry/monitoring/metrics/DistributionFitter.java b/java/google/registry/monitoring/metrics/DistributionFitter.java
new file mode 100644
index 000000000..bea7c9c7c
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/DistributionFitter.java
@@ -0,0 +1,33 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import com.google.common.collect.ImmutableSortedSet;
+
+/**
+ * A companion interface to {@link Distribution} which fits samples to a histogram in order to
+ * estimate the probability density function (PDF) of the {@link Distribution}.
+ *
+ * The fitter models the histogram with a set of finite boundaries. The closed-open interval
+ * [a,b) between two consecutive boundaries represents the domain of permissible values in that
+ * interval. The values less than the first boundary are in the underflow interval of (-inf, a) and
+ * values greater or equal to the last boundary in the array are in the overflow interval of [a,
+ * inf).
+ */
+public interface DistributionFitter {
+
+ /** Returns a sorted set of the boundaries modeled by this {@link DistributionFitter}. */
+ ImmutableSortedSet boundaries();
+}
diff --git a/java/google/registry/monitoring/metrics/ExponentialFitter.java b/java/google/registry/monitoring/metrics/ExponentialFitter.java
new file mode 100644
index 000000000..f16939e7f
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/ExponentialFitter.java
@@ -0,0 +1,68 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static google.registry.monitoring.metrics.MetricsUtils.checkDouble;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSortedSet;
+
+/**
+ * Models a {@link DistributionFitter} with intervals of exponentially increasing size.
+ *
+ * The interval boundaries are defined by {@code scale * Math.pow(base, i)} for {@code i} in
+ * {@code [0, numFiniteIntervals]}.
+ *
+ *
For example, an {@link ExponentialFitter} with {@code numFiniteIntervals=3, base=4.0,
+ * scale=1.5} represents a histogram with intervals {@code (-inf, 1.5), [1.5, 6), [6, 24), [24, 96),
+ * [96, +inf)}.
+ */
+@AutoValue
+public abstract class ExponentialFitter implements DistributionFitter {
+
+ /**
+ * Create a new {@link ExponentialFitter}.
+ *
+ * @param numFiniteIntervals the number of intervals, excluding the underflow and overflow
+ * intervals
+ * @param base the base of the exponent
+ * @param scale a multiplicative factor for the exponential function
+ * @throws IllegalArgumentException if {@code numFiniteIntervals <= 0}, {@code width <= 0} or
+ * {@code base <= 1}
+ */
+ public static ExponentialFitter create(int numFiniteIntervals, double base, double scale) {
+ checkArgument(numFiniteIntervals > 0, "numFiniteIntervals must be greater than 0");
+ checkArgument(scale != 0, "scale must not be 0");
+ checkArgument(base > 1, "base must be greater than 1");
+ checkDouble(base);
+ checkDouble(scale);
+
+ ImmutableSortedSet.Builder boundaries = ImmutableSortedSet.naturalOrder();
+
+ for (int i = 0; i < numFiniteIntervals + 1; i++) {
+ boundaries.add(scale * Math.pow(base, i));
+ }
+
+ return new AutoValue_ExponentialFitter(base, scale, boundaries.build());
+ }
+
+ public abstract double base();
+
+ public abstract double scale();
+
+ @Override
+ public abstract ImmutableSortedSet boundaries();
+}
diff --git a/java/google/registry/monitoring/metrics/ImmutableDistribution.java b/java/google/registry/monitoring/metrics/ImmutableDistribution.java
new file mode 100644
index 000000000..2134ccf2a
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/ImmutableDistribution.java
@@ -0,0 +1,54 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableRangeMap;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An immutable {@link Distribution}. Instances of this class can used to create {@link MetricPoint}
+ * instances, and should be used when exporting values to a {@link MetricWriter}.
+ *
+ * @see MutableDistribution
+ */
+@ThreadSafe
+@AutoValue
+public abstract class ImmutableDistribution implements Distribution {
+
+ public static ImmutableDistribution copyOf(Distribution distribution) {
+ return new AutoValue_ImmutableDistribution(
+ distribution.mean(),
+ distribution.sumOfSquaredDeviation(),
+ distribution.count(),
+ distribution.intervalCounts(),
+ distribution.distributionFitter());
+ }
+
+ @Override
+ public abstract double mean();
+
+ @Override
+ public abstract double sumOfSquaredDeviation();
+
+ @Override
+ public abstract long count();
+
+ @Override
+ public abstract ImmutableRangeMap intervalCounts();
+
+ @Override
+ public abstract DistributionFitter distributionFitter();
+}
diff --git a/java/google/registry/monitoring/metrics/LinearFitter.java b/java/google/registry/monitoring/metrics/LinearFitter.java
new file mode 100644
index 000000000..1e4c5773b
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/LinearFitter.java
@@ -0,0 +1,64 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static google.registry.monitoring.metrics.MetricsUtils.checkDouble;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSortedSet;
+
+/**
+ * Models a {@link DistributionFitter} with equally sized intervals.
+ *
+ * The interval boundaries are defined by {@code width * i + offset} for {@code i} in {@code [0,
+ * numFiniteIntervals}.
+ *
+ *
For example, a {@link LinearFitter} with {@code numFiniteIntervals=2, width=10, offset=5}
+ * represents a histogram with intervals {@code (-inf, 5), [5, 15), [15, 25), [25, +inf)}.
+ */
+@AutoValue
+public abstract class LinearFitter implements DistributionFitter {
+
+ /**
+ * Create a new {@link LinearFitter}.
+ *
+ * @param numFiniteIntervals the number of intervals, excluding the underflow and overflow
+ * intervals
+ * @param width the width of each interval
+ * @param offset the start value of the first interval
+ * @throws IllegalArgumentException if {@code numFiniteIntervals <= 0} or {@code width <= 0}
+ */
+ public static LinearFitter create(int numFiniteIntervals, double width, double offset) {
+ checkArgument(numFiniteIntervals > 0, "numFiniteIntervals must be greater than 0");
+ checkArgument(width > 0, "width must be greater than 0");
+ checkDouble(offset);
+
+ ImmutableSortedSet.Builder boundaries = ImmutableSortedSet.naturalOrder();
+
+ for (int i = 0; i < numFiniteIntervals + 1; i++) {
+ boundaries.add(width * i + offset);
+ }
+
+ return new AutoValue_LinearFitter(width, offset, boundaries.build());
+ }
+
+ public abstract double width();
+
+ public abstract double offset();
+
+ @Override
+ public abstract ImmutableSortedSet boundaries();
+}
diff --git a/java/google/registry/monitoring/metrics/MetricsUtils.java b/java/google/registry/monitoring/metrics/MetricsUtils.java
index 7e619e688..26e4cec8e 100644
--- a/java/google/registry/monitoring/metrics/MetricsUtils.java
+++ b/java/google/registry/monitoring/metrics/MetricsUtils.java
@@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList;
/** Static helper methods for the Metrics library. */
final class MetricsUtils {
+ private static final Double NEGATIVE_ZERO = -0.0;
private static final String LABEL_SIZE_ERROR =
"The count of labelValues must be equal to the underlying Metric's count of labels.";
@@ -45,4 +46,11 @@ final class MetricsUtils {
static void checkLabelValuesLength(Metric> metric, ImmutableList labelValues) {
checkArgument(labelValues.size() == metric.getMetricSchema().labels().size(), LABEL_SIZE_ERROR);
}
+
+ /** Check that the given double is not infinite, {@code NaN}, or {@code -0.0}. */
+ static void checkDouble(double value) {
+ checkArgument(
+ !Double.isInfinite(value) && !Double.isNaN(value) && !NEGATIVE_ZERO.equals(value),
+ "value must be finite, not NaN, and not -0.0");
+ }
}
diff --git a/java/google/registry/monitoring/metrics/MutableDistribution.java b/java/google/registry/monitoring/metrics/MutableDistribution.java
new file mode 100644
index 000000000..b26e069d8
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/MutableDistribution.java
@@ -0,0 +1,111 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static google.registry.monitoring.metrics.MetricsUtils.checkDouble;
+
+import com.google.common.collect.ImmutableRangeMap;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Range;
+import com.google.common.collect.TreeRangeMap;
+import com.google.common.primitives.Doubles;
+import java.util.Map;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * A mutable {@link Distribution}. Instances of this class should not be used to construct
+ * {@link MetricPoint} instances as {@link MetricPoint} instances are supposed to represent
+ * immutable values.
+ *
+ * @see ImmutableDistribution
+ */
+@NotThreadSafe
+public final class MutableDistribution implements Distribution {
+
+ private final TreeRangeMap intervalCounts;
+ private final DistributionFitter distributionFitter;
+ private double sumOfSquaredDeviation = 0.0;
+ private double mean = 0.0;
+ private int count = 0;
+
+ /** Constructs an empty Distribution with the specified {@link DistributionFitter}. */
+ public MutableDistribution(DistributionFitter distributionFitter) {
+ this.distributionFitter = checkNotNull(distributionFitter);
+ ImmutableSortedSet boundaries = distributionFitter.boundaries();
+
+ checkArgument(boundaries.size() > 0);
+ checkArgument(Ordering.natural().isOrdered(boundaries));
+
+ this.intervalCounts = TreeRangeMap.create();
+
+ double[] boundariesArray = Doubles.toArray(distributionFitter.boundaries());
+
+ // Add underflow and overflow intervals
+ this.intervalCounts.put(Range.lessThan(boundariesArray[0]), 0L);
+ this.intervalCounts.put(Range.atLeast(boundariesArray[boundariesArray.length - 1]), 0L);
+
+ // Add finite intervals
+ for (int i = 1; i < boundariesArray.length; i++) {
+ this.intervalCounts.put(Range.closedOpen(boundariesArray[i - 1], boundariesArray[i]), 0L);
+ }
+ }
+
+ public void add(double value) {
+ add(value, 1L);
+ }
+
+ public void add(double value, long numSamples) {
+ checkArgument(numSamples > 0, "numSamples must be greater than 0");
+ checkDouble(value);
+
+ Map.Entry, Long> entry = intervalCounts.getEntry(value);
+ intervalCounts.put(entry.getKey(), entry.getValue() + numSamples);
+ this.count += numSamples;
+
+ // Update mean and sumOfSquaredDeviation using Welford's method
+ // See Knuth, "The Art of Computer Programming", Vol. 2, page 232, 3rd edition
+ double delta = value - mean;
+ mean += delta * numSamples / count;
+ sumOfSquaredDeviation += delta * (value - mean) * numSamples;
+ }
+
+ @Override
+ public double mean() {
+ return mean;
+ }
+
+ @Override
+ public double sumOfSquaredDeviation() {
+ return sumOfSquaredDeviation;
+ }
+
+ @Override
+ public long count() {
+ return count;
+ }
+
+ @Override
+ public ImmutableRangeMap intervalCounts() {
+ return ImmutableRangeMap.copyOf(intervalCounts);
+ }
+
+ @Override
+ public DistributionFitter distributionFitter() {
+ return distributionFitter;
+ }
+}
diff --git a/javatests/google/registry/monitoring/metrics/CustomFitterTest.java b/javatests/google/registry/monitoring/metrics/CustomFitterTest.java
new file mode 100644
index 000000000..7190475af
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/CustomFitterTest.java
@@ -0,0 +1,55 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link CustomFitter}. */
+@RunWith(JUnit4.class)
+public class CustomFitterTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testCreateCustomFitter_emptyBounds_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("boundaries must not be empty");
+
+ CustomFitter.create(ImmutableSet.of());
+ }
+
+ @Test
+ public void testCreateCustomFitter_outOfOrderBounds_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("boundaries must be sorted");
+
+ CustomFitter.create(ImmutableSet.of(2.0, 0.0));
+ }
+
+ @Test
+ public void testCreateCustomFitter_hasGivenBounds() {
+ CustomFitter fitter = CustomFitter.create(ImmutableSortedSet.of(1.0, 2.0));
+
+ assertThat(fitter.boundaries()).containsExactly(1.0, 2.0).inOrder();
+ }
+}
diff --git a/javatests/google/registry/monitoring/metrics/ExponentialFitterTest.java b/javatests/google/registry/monitoring/metrics/ExponentialFitterTest.java
new file mode 100644
index 000000000..3dbf75627
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/ExponentialFitterTest.java
@@ -0,0 +1,77 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for implementations of {@link DistributionFitter}. */
+@RunWith(JUnit4.class)
+public class ExponentialFitterTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testCreateExponentialFitter_zeroNumIntervals_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("numFiniteIntervals must be greater than 0");
+
+ ExponentialFitter.create(0, 3.0, 1.0);
+ }
+
+ @Test
+ public void testCreateExponentialFitter_negativeNumIntervals_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("numFiniteIntervals must be greater than 0");
+
+ ExponentialFitter.create(-1, 3.0, 1.0);
+ }
+
+ @Test
+ public void testCreateExponentialFitter_invalidBase_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("base must be greater than 1");
+
+ ExponentialFitter.create(3, 0.5, 1.0);
+ }
+
+ @Test
+ public void testCreateExponentialFitter_zeroScale_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("scale must not be 0");
+
+ ExponentialFitter.create(3, 2.0, 0.0);
+ }
+
+ @Test
+ public void testCreateExponentialFitter_NanScale_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("value must be finite, not NaN, and not -0.0");
+
+ ExponentialFitter.create(3, 2.0, Double.NaN);
+ }
+
+ @Test
+ public void testCreateExponentialFitter_hasCorrectBounds() {
+ ExponentialFitter fitter = ExponentialFitter.create(3, 5.0, 2.0);
+
+ assertThat(fitter.boundaries()).containsExactly(2.0, 10.0, 50.0, 250.0).inOrder();
+ }
+}
diff --git a/javatests/google/registry/monitoring/metrics/LinearFitterTest.java b/javatests/google/registry/monitoring/metrics/LinearFitterTest.java
new file mode 100644
index 000000000..27d2b0645
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/LinearFitterTest.java
@@ -0,0 +1,99 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link LinearFitter}. */
+@RunWith(JUnit4.class)
+public class LinearFitterTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testCreateLinearFitter_zeroNumIntervals_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("numFiniteIntervals must be greater than 0");
+
+ LinearFitter.create(0, 3.0, 0.0);
+ }
+
+ @Test
+ public void testCreateLinearFitter_negativeNumIntervals_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("numFiniteIntervals must be greater than 0");
+
+ LinearFitter.create(0, 3.0, 0.0);
+ }
+
+ @Test
+ public void testCreateLinearFitter_zeroWidth_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("width must be greater than 0");
+
+ LinearFitter.create(3, 0.0, 0.0);
+ }
+
+ @Test
+ public void testCreateLinearFitter_negativeWidth_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("width must be greater than 0");
+
+ LinearFitter.create(3, 0.0, 0.0);
+ }
+
+ @Test
+ public void testCreateLinearFitter_NaNWidth_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("width must be greater than 0");
+
+ LinearFitter.create(3, Double.NaN, 0.0);
+ }
+
+ @Test
+ public void testCreateLinearFitter_NaNOffset_throwsException() throws Exception {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("value must be finite, not NaN, and not -0.0");
+
+ LinearFitter.create(3, 1.0, Double.NaN);
+ }
+
+ @Test
+ public void testCreateLinearFitter_hasCorrectBounds() {
+ LinearFitter fitter = LinearFitter.create(1, 10, 0);
+
+ assertThat(fitter.boundaries()).containsExactly(0.0, 10.0).inOrder();
+ }
+
+ @Test
+ public void testCreateLinearFitter_withOffset_hasCorrectBounds() {
+ LinearFitter fitter = LinearFitter.create(1, 10, 5);
+
+ assertThat(fitter.boundaries()).containsExactly(5.0, 15.0).inOrder();
+ }
+
+ @Test
+ public void testCreateLinearFitter_withOffsetAndMultipleIntervals_hasCorrectBounds() {
+ LinearFitter fitter = LinearFitter.create(3, 10, 5);
+
+ assertThat(fitter.boundaries()).containsExactly(5.0, 15.0, 25.0, 35.0).inOrder();
+ }
+}
diff --git a/javatests/google/registry/monitoring/metrics/MutableDistributionTest.java b/javatests/google/registry/monitoring/metrics/MutableDistributionTest.java
new file mode 100644
index 000000000..bd3e1ff67
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/MutableDistributionTest.java
@@ -0,0 +1,292 @@
+// Copyright 2016 The Domain Registry Authors. 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 google.registry.monitoring.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableRangeMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Range;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MutableDistribution} */
+@RunWith(JUnit4.class)
+public class MutableDistributionTest {
+
+ private MutableDistribution distribution;
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void setUp() throws Exception {
+ distribution = new MutableDistribution(CustomFitter.create(ImmutableSet.of(3.0, 5.0)));
+ }
+
+ @Test
+ public void testAdd_oneValue() {
+ distribution.add(5.0);
+
+ assertThat(distribution.count()).isEqualTo(1);
+ assertThat(distribution.mean()).isWithin(0.0).of(5.0);
+ assertThat(distribution.sumOfSquaredDeviation()).isWithin(0.0).of(0);
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(3.0), 0L)
+ .put(Range.closedOpen(3.0, 5.0), 0L)
+ .put(Range.atLeast(5.0), 1L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_zero() {
+ distribution.add(0.0);
+
+ assertThat(distribution.count()).isEqualTo(1);
+ assertThat(distribution.mean()).isWithin(0.0).of(0.0);
+ assertThat(distribution.sumOfSquaredDeviation()).isWithin(0.0).of(0);
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(3.0), 1L)
+ .put(Range.closedOpen(3.0, 5.0), 0L)
+ .put(Range.atLeast(5.0), 0L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_multipleOfOneValue() {
+ distribution.add(4.0, 2);
+
+ assertThat(distribution.count()).isEqualTo(2);
+ assertThat(distribution.mean()).isWithin(0.0).of(4.0);
+ assertThat(distribution.sumOfSquaredDeviation()).isWithin(0.0).of(0);
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(3.0), 0L)
+ .put(Range.closedOpen(3.0, 5.0), 2L)
+ .put(Range.atLeast(5.0), 0L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_positiveThenNegativeValue() {
+ distribution.add(2.0);
+ distribution.add(-2.0);
+
+ assertThat(distribution.count()).isEqualTo(2);
+ assertThat(distribution.mean()).isWithin(0.0).of(0.0);
+ assertThat(distribution.sumOfSquaredDeviation()).isWithin(0.0).of(8.0);
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(3.0), 2L)
+ .put(Range.closedOpen(3.0, 5.0), 0L)
+ .put(Range.atLeast(5.0), 0L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_wideRangeOfValues() {
+ distribution.add(2.0);
+ distribution.add(16.0);
+ distribution.add(128.0, 5);
+
+ assertThat(distribution.count()).isEqualTo(7);
+ assertThat(distribution.mean()).isWithin(0.0).of(94.0);
+ assertThat(distribution.sumOfSquaredDeviation()).isWithin(0.0).of(20328.0);
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(3.0), 1L)
+ .put(Range.closedOpen(3.0, 5.0), 0L)
+ .put(Range.atLeast(5.0), 6L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_negativeZero_throwsException() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("value must be finite, not NaN, and not -0.0");
+ distribution.add(Double.longBitsToDouble(0x80000000));
+ }
+
+ @Test
+ public void testAdd_NaN_throwsException() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("value must be finite, not NaN, and not -0.0");
+ distribution.add(Double.NaN);
+ }
+
+ @Test
+ public void testAdd_positiveInfinity_throwsException() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("value must be finite, not NaN, and not -0.0");
+ distribution.add(Double.POSITIVE_INFINITY);
+ }
+
+ @Test
+ public void testAdd_negativeInfinity_throwsException() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("value must be finite, not NaN, and not -0.0");
+ distribution.add(Double.NEGATIVE_INFINITY);
+ }
+
+ @Test
+ public void testAdd_iteratedFloatingPointValues_hasLowAccumulatedError() {
+ for (int i = 0; i < 500; i++) {
+ distribution.add(1 / 3.0);
+ distribution.add(1 / 7.0);
+ }
+
+ // Test for nine significant figures of accuracy.
+ assertThat(distribution.mean()).isWithin(0.000000001).of(5.0 / 21.0);
+ assertThat(distribution.sumOfSquaredDeviation())
+ .isWithin(0.000000001)
+ .of(1000 * 4.0 / (21.0 * 21.0));
+ }
+
+ @Test
+ public void testAdd_fitterWithNoFiniteIntervals_underflowValue_returnsUnderflowInterval()
+ throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(5.0)));
+
+ distribution.add(3.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(5.0), 1L)
+ .put(Range.atLeast(5.0), 0L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_noFiniteIntervals_overflowValue_returnsOverflowInterval() throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(5.0)));
+
+ distribution.add(10.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(5.0), 0L)
+ .put(Range.atLeast(5.0), 1L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_noFiniteIntervals_edgeValue_returnsOverflowInterval() throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(2.0)));
+
+ distribution.add(2.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(2.0), 0L)
+ .put(Range.atLeast(2.0), 1L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_oneFiniteInterval_underflowValue_returnsUnderflowInterval() throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(1.0, 5.0)));
+
+ distribution.add(0.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(1.0), 1L)
+ .put(Range.closedOpen(1.0, 5.0), 0L)
+ .put(Range.atLeast(5.0), 0L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_oneFiniteInterval_overflowValue_returnsOverflowInterval() throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(1.0, 5.0)));
+
+ distribution.add(10.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(1.0), 0L)
+ .put(Range.closedOpen(1.0, 5.0), 0L)
+ .put(Range.atLeast(5.0), 1L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_oneFiniteInterval_inBoundsValue_returnsInBoundsInterval() throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(1.0, 5.0)));
+
+ distribution.add(3.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(1.0), 0L)
+ .put(Range.closedOpen(1.0, 5.0), 1L)
+ .put(Range.atLeast(5.0), 0L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_oneFiniteInterval_firstEdgeValue_returnsFiniteInterval() throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(1.0, 5.0)));
+
+ distribution.add(1.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(1.0), 0L)
+ .put(Range.closedOpen(1.0, 5.0), 1L)
+ .put(Range.atLeast(5.0), 0L)
+ .build());
+ }
+
+ @Test
+ public void testAdd_oneFiniteInterval_secondEdgeValue_returnsOverflowInterval() throws Exception {
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(1.0, 5.0)));
+
+ distribution.add(5.0);
+
+ assertThat(distribution.intervalCounts())
+ .isEqualTo(
+ ImmutableRangeMap.builder()
+ .put(Range.lessThan(1.0), 0L)
+ .put(Range.closedOpen(1.0, 5.0), 0L)
+ .put(Range.atLeast(5.0), 1L)
+ .build());
+ }
+}