From 03009ce00611bb6ea3443e2e18485ea13d5515e4 Mon Sep 17 00:00:00 2001 From: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Thu, 6 Oct 2022 15:02:08 -0500 Subject: [PATCH] Lower exclusive exponential histogram bounds (#4700) * Switch exponential histograms to use lower exclusive boundaries * Cache ExponentialHistogramIndexers * Update javadoc to reflect lower exlusive boundaries * Spotless * Fix typo * Add tests and stop rounding subnormal values * spotless * Add explanatory comments --- .../DoubleExponentialHistogramBuckets.java | 44 +--- .../ExponentialHistogramIndexer.java | 109 +++++++++ .../ExponentialHistogramBuckets.java | 4 +- .../ExponentialHistogramPointData.java | 5 +- ...bleExponentialHistogramAggregatorTest.java | 6 +- ...DoubleExponentialHistogramBucketsTest.java | 4 +- .../ExponentialHistogramIndexerTest.java | 223 ++++++++++++++++++ 7 files changed, 348 insertions(+), 47 deletions(-) create mode 100644 sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/ExponentialHistogramIndexer.java create mode 100644 sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/aggregator/ExponentialHistogramIndexerTest.java diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBuckets.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBuckets.java index e34286a27e4..8bedfd20e97 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBuckets.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBuckets.java @@ -23,12 +23,10 @@ */ final class DoubleExponentialHistogramBuckets implements ExponentialHistogramBuckets { - private static final double LOG_BASE2_E = 1D / Math.log(2); - private final ExponentialCounterFactory counterFactory; private ExponentialCounter counts; private int scale; - private double scaleFactor; + private ExponentialHistogramIndexer exponentialHistogramIndexer; private long totalCount; DoubleExponentialHistogramBuckets( @@ -36,7 +34,7 @@ final class DoubleExponentialHistogramBuckets implements ExponentialHistogramBuc this.counterFactory = counterFactory; this.counts = counterFactory.newCounter(maxBuckets); this.scale = startingScale; - this.scaleFactor = computeScaleFactor(startingScale); + this.exponentialHistogramIndexer = ExponentialHistogramIndexer.get(scale); this.totalCount = 0; } @@ -45,7 +43,7 @@ final class DoubleExponentialHistogramBuckets implements ExponentialHistogramBuc this.counterFactory = buckets.counterFactory; this.counts = counterFactory.copy(buckets.counts); this.scale = buckets.scale; - this.scaleFactor = buckets.scaleFactor; + this.exponentialHistogramIndexer = buckets.exponentialHistogramIndexer; this.totalCount = buckets.totalCount; } @@ -65,7 +63,7 @@ boolean record(double value) { // Guarded by caller. If passed 0 it would be a bug in the SDK. throw new IllegalStateException("Illegal attempted recording of zero at bucket level."); } - int index = valueToIndex(value); + int index = exponentialHistogramIndexer.computeIndex(value); boolean recordingSuccessful = this.counts.increment(index, 1); if (recordingSuccessful) { totalCount++; @@ -131,7 +129,7 @@ void downscale(int by) { } this.scale = this.scale - by; - this.scaleFactor = computeScaleFactor(this.scale); + this.exponentialHistogramIndexer = ExponentialHistogramIndexer.get(this.scale); } /** @@ -220,7 +218,7 @@ int getScale() { * @return The required scale reduction in order to fit the value in these buckets. */ int getScaleReduction(double value) { - long index = valueToIndex(value); + long index = exponentialHistogramIndexer.computeIndex(value); long newStart = Math.min(index, counts.getIndexStart()); long newEnd = Math.max(index, counts.getIndexEnd()); return getScaleReduction(newStart, newEnd); @@ -237,36 +235,6 @@ int getScaleReduction(long newStart, long newEnd) { return scaleReduction; } - private int getIndexByLogarithm(double value) { - return (int) Math.floor(Math.log(value) * scaleFactor); - } - - private int getIndexByExponent(double value) { - return Math.getExponent(value) >> -scale; - } - - private static double computeScaleFactor(int scale) { - return Math.scalb(LOG_BASE2_E, scale); - } - - /** - * Maps a recorded double value to a bucket index. - * - *
The strategy to retrieve the index is specified in the OpenTelemetry
- * specification.
- *
- * @param value Measured value (must be non-zero).
- * @return the index of the bucket which the value maps to.
- */
- private int valueToIndex(double value) {
- double absValue = Math.abs(value);
- if (scale > 0) {
- return getIndexByLogarithm(absValue);
- }
- return getIndexByExponent(absValue);
- }
-
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof DoubleExponentialHistogramBuckets)) {
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/ExponentialHistogramIndexer.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/ExponentialHistogramIndexer.java
new file mode 100644
index 00000000000..8bd95f3275b
--- /dev/null
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/ExponentialHistogramIndexer.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.sdk.metrics.internal.aggregator;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+class ExponentialHistogramIndexer {
+
+ private static final Map The algorithm to retrieve the index is specified in the OpenTelemetry
+ * specification.
+ *
+ * @param value Measured value (must be non-zero).
+ * @return the index of the bucket which the value maps to.
+ */
+ int computeIndex(double value) {
+ double absValue = Math.abs(value);
+ // For positive scales, compute the index by logarithm, which is simpler but may be
+ // inaccurate near bucket boundaries
+ if (scale > 0) {
+ return getIndexByLogarithm(absValue);
+ }
+ // For scale zero, compute the exact index by extracting the exponent
+ if (scale == 0) {
+ return mapToIndexScaleZero(absValue);
+ }
+ // For negative scales, compute the exact index by extracting the exponent and shifting it to
+ // the right by -scale
+ return mapToIndexScaleZero(absValue) >> -scale;
+ }
+
+ /**
+ * Compute the bucket index using a logarithm based approach.
+ *
+ * @see All
+ * Scales: Use the Logarithm Function
+ */
+ private int getIndexByLogarithm(double value) {
+ return (int) Math.ceil(Math.log(value) * scaleFactor) - 1;
+ }
+
+ /**
+ * Compute the exact bucket index for scale zero by extracting the exponent.
+ *
+ * @see Scale
+ * Zero: Extract the Exponent
+ */
+ private static int mapToIndexScaleZero(double value) {
+ long rawBits = Double.doubleToLongBits(value);
+ long rawExponent = (rawBits & EXPONENT_BIT_MASK) >> SIGNIFICAND_WIDTH;
+ long rawSignificand = rawBits & SIGNIFICAND_BIT_MASK;
+ if (rawExponent == 0) {
+ rawExponent -= Long.numberOfLeadingZeros(rawSignificand - 1) - EXPONENT_WIDTH - 1;
+ }
+ int ieeeExponent = (int) (rawExponent - EXPONENT_BIAS);
+ if (rawSignificand == 0) {
+ return ieeeExponent - 1;
+ }
+ return ieeeExponent;
+ }
+
+ private static double computeScaleFactor(int scale) {
+ return Math.scalb(LOG_BASE2_E, scale);
+ }
+}
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramBuckets.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramBuckets.java
index be87d2ad82c..edff712e37d 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramBuckets.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramBuckets.java
@@ -12,7 +12,7 @@
* ExponentialHistogramBuckets represents either the positive or negative measurements taken for a
* {@link ExponentialHistogramPointData}.
*
- * The bucket boundaries are lower-bound inclusive, and are calculated using the {@link
+ * The bucket boundaries are lower-bound exclusive, and are calculated using the {@link
* ExponentialHistogramPointData#getScale()} and the {@link #getOffset()}.
*
* For example, assume {@link ExponentialHistogramPointData#getScale()} is 0, the base is 2.0.
@@ -35,7 +35,7 @@ public interface ExponentialHistogramBuckets {
int getOffset();
/**
- * The bucket counts is a of counts representing number of measurements that fall into each
+ * The bucket counts is a list of counts representing number of measurements that fall into each
* bucket.
*
* @return the bucket counts.
diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramPointData.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramPointData.java
index 6d9bdc1b178..808ca97d70c 100644
--- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramPointData.java
+++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramPointData.java
@@ -20,8 +20,9 @@
* The bucket boundaries are calculated using both the scale {@link #getScale()}, and the offset
* {@link ExponentialHistogramBuckets#getOffset()}.
*
- * See:
- * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponentialhistogram
+ * See data
+ * model.
*
* This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramAggregatorTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramAggregatorTest.java
index e4becdaa99c..ec36cf9e9af 100644
--- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramAggregatorTest.java
+++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramAggregatorTest.java
@@ -68,7 +68,7 @@ private static Stream