Skip to content

Commit 03009ce

Browse files
authored
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
1 parent b242ec2 commit 03009ce

File tree

7 files changed

+348
-47
lines changed

7 files changed

+348
-47
lines changed

Diff for: sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBuckets.java

+6-38
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,18 @@
2323
*/
2424
final class DoubleExponentialHistogramBuckets implements ExponentialHistogramBuckets {
2525

26-
private static final double LOG_BASE2_E = 1D / Math.log(2);
27-
2826
private final ExponentialCounterFactory counterFactory;
2927
private ExponentialCounter counts;
3028
private int scale;
31-
private double scaleFactor;
29+
private ExponentialHistogramIndexer exponentialHistogramIndexer;
3230
private long totalCount;
3331

3432
DoubleExponentialHistogramBuckets(
3533
int startingScale, int maxBuckets, ExponentialCounterFactory counterFactory) {
3634
this.counterFactory = counterFactory;
3735
this.counts = counterFactory.newCounter(maxBuckets);
3836
this.scale = startingScale;
39-
this.scaleFactor = computeScaleFactor(startingScale);
37+
this.exponentialHistogramIndexer = ExponentialHistogramIndexer.get(scale);
4038
this.totalCount = 0;
4139
}
4240

@@ -45,7 +43,7 @@ final class DoubleExponentialHistogramBuckets implements ExponentialHistogramBuc
4543
this.counterFactory = buckets.counterFactory;
4644
this.counts = counterFactory.copy(buckets.counts);
4745
this.scale = buckets.scale;
48-
this.scaleFactor = buckets.scaleFactor;
46+
this.exponentialHistogramIndexer = buckets.exponentialHistogramIndexer;
4947
this.totalCount = buckets.totalCount;
5048
}
5149

@@ -65,7 +63,7 @@ boolean record(double value) {
6563
// Guarded by caller. If passed 0 it would be a bug in the SDK.
6664
throw new IllegalStateException("Illegal attempted recording of zero at bucket level.");
6765
}
68-
int index = valueToIndex(value);
66+
int index = exponentialHistogramIndexer.computeIndex(value);
6967
boolean recordingSuccessful = this.counts.increment(index, 1);
7068
if (recordingSuccessful) {
7169
totalCount++;
@@ -131,7 +129,7 @@ void downscale(int by) {
131129
}
132130

133131
this.scale = this.scale - by;
134-
this.scaleFactor = computeScaleFactor(this.scale);
132+
this.exponentialHistogramIndexer = ExponentialHistogramIndexer.get(this.scale);
135133
}
136134

137135
/**
@@ -220,7 +218,7 @@ int getScale() {
220218
* @return The required scale reduction in order to fit the value in these buckets.
221219
*/
222220
int getScaleReduction(double value) {
223-
long index = valueToIndex(value);
221+
long index = exponentialHistogramIndexer.computeIndex(value);
224222
long newStart = Math.min(index, counts.getIndexStart());
225223
long newEnd = Math.max(index, counts.getIndexEnd());
226224
return getScaleReduction(newStart, newEnd);
@@ -237,36 +235,6 @@ int getScaleReduction(long newStart, long newEnd) {
237235
return scaleReduction;
238236
}
239237

240-
private int getIndexByLogarithm(double value) {
241-
return (int) Math.floor(Math.log(value) * scaleFactor);
242-
}
243-
244-
private int getIndexByExponent(double value) {
245-
return Math.getExponent(value) >> -scale;
246-
}
247-
248-
private static double computeScaleFactor(int scale) {
249-
return Math.scalb(LOG_BASE2_E, scale);
250-
}
251-
252-
/**
253-
* Maps a recorded double value to a bucket index.
254-
*
255-
* <p>The strategy to retrieve the index is specified in the <a
256-
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-buckets">OpenTelemetry
257-
* specification</a>.
258-
*
259-
* @param value Measured value (must be non-zero).
260-
* @return the index of the bucket which the value maps to.
261-
*/
262-
private int valueToIndex(double value) {
263-
double absValue = Math.abs(value);
264-
if (scale > 0) {
265-
return getIndexByLogarithm(absValue);
266-
}
267-
return getIndexByExponent(absValue);
268-
}
269-
270238
@Override
271239
public boolean equals(@Nullable Object obj) {
272240
if (!(obj instanceof DoubleExponentialHistogramBuckets)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.metrics.internal.aggregator;
7+
8+
import java.util.Map;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
11+
class ExponentialHistogramIndexer {
12+
13+
private static final Map<Integer, ExponentialHistogramIndexer> cache = new ConcurrentHashMap<>();
14+
15+
/** Bit mask used to isolate exponent of IEEE 754 double precision number. */
16+
private static final long EXPONENT_BIT_MASK = 0x7FF0000000000000L;
17+
18+
/** Bit mask used to isolate the significand of IEEE 754 double precision number. */
19+
private static final long SIGNIFICAND_BIT_MASK = 0xFFFFFFFFFFFFFL;
20+
21+
/** Bias used in representing the exponent of IEEE 754 double precision number. */
22+
private static final int EXPONENT_BIAS = 1023;
23+
24+
/**
25+
* The number of bits used to represent the significand of IEEE 754 double precision number,
26+
* excluding the implicit bit.
27+
*/
28+
private static final int SIGNIFICAND_WIDTH = 52;
29+
30+
/** The number of bits used to represent the exponent of IEEE 754 double precision number. */
31+
private static final int EXPONENT_WIDTH = 11;
32+
33+
private static final double LOG_BASE2_E = 1D / Math.log(2);
34+
35+
private final int scale;
36+
private final double scaleFactor;
37+
38+
private ExponentialHistogramIndexer(int scale) {
39+
this.scale = scale;
40+
this.scaleFactor = computeScaleFactor(scale);
41+
}
42+
43+
/** Get an indexer for the given scale. Indexers are cached and reused for */
44+
public static ExponentialHistogramIndexer get(int scale) {
45+
return cache.computeIfAbsent(scale, unused -> new ExponentialHistogramIndexer(scale));
46+
}
47+
48+
/**
49+
* Compute the index for the given value.
50+
*
51+
* <p>The algorithm to retrieve the index is specified in the <a
52+
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#exponential-buckets">OpenTelemetry
53+
* specification</a>.
54+
*
55+
* @param value Measured value (must be non-zero).
56+
* @return the index of the bucket which the value maps to.
57+
*/
58+
int computeIndex(double value) {
59+
double absValue = Math.abs(value);
60+
// For positive scales, compute the index by logarithm, which is simpler but may be
61+
// inaccurate near bucket boundaries
62+
if (scale > 0) {
63+
return getIndexByLogarithm(absValue);
64+
}
65+
// For scale zero, compute the exact index by extracting the exponent
66+
if (scale == 0) {
67+
return mapToIndexScaleZero(absValue);
68+
}
69+
// For negative scales, compute the exact index by extracting the exponent and shifting it to
70+
// the right by -scale
71+
return mapToIndexScaleZero(absValue) >> -scale;
72+
}
73+
74+
/**
75+
* Compute the bucket index using a logarithm based approach.
76+
*
77+
* @see <a
78+
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function">All
79+
* Scales: Use the Logarithm Function</a>
80+
*/
81+
private int getIndexByLogarithm(double value) {
82+
return (int) Math.ceil(Math.log(value) * scaleFactor) - 1;
83+
}
84+
85+
/**
86+
* Compute the exact bucket index for scale zero by extracting the exponent.
87+
*
88+
* @see <a
89+
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#scale-zero-extract-the-exponent">Scale
90+
* Zero: Extract the Exponent</a>
91+
*/
92+
private static int mapToIndexScaleZero(double value) {
93+
long rawBits = Double.doubleToLongBits(value);
94+
long rawExponent = (rawBits & EXPONENT_BIT_MASK) >> SIGNIFICAND_WIDTH;
95+
long rawSignificand = rawBits & SIGNIFICAND_BIT_MASK;
96+
if (rawExponent == 0) {
97+
rawExponent -= Long.numberOfLeadingZeros(rawSignificand - 1) - EXPONENT_WIDTH - 1;
98+
}
99+
int ieeeExponent = (int) (rawExponent - EXPONENT_BIAS);
100+
if (rawSignificand == 0) {
101+
return ieeeExponent - 1;
102+
}
103+
return ieeeExponent;
104+
}
105+
106+
private static double computeScaleFactor(int scale) {
107+
return Math.scalb(LOG_BASE2_E, scale);
108+
}
109+
}

Diff for: sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramBuckets.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* ExponentialHistogramBuckets represents either the positive or negative measurements taken for a
1313
* {@link ExponentialHistogramPointData}.
1414
*
15-
* <p>The bucket boundaries are lower-bound inclusive, and are calculated using the {@link
15+
* <p>The bucket boundaries are lower-bound exclusive, and are calculated using the {@link
1616
* ExponentialHistogramPointData#getScale()} and the {@link #getOffset()}.
1717
*
1818
* <p>For example, assume {@link ExponentialHistogramPointData#getScale()} is 0, the base is 2.0.
@@ -35,7 +35,7 @@ public interface ExponentialHistogramBuckets {
3535
int getOffset();
3636

3737
/**
38-
* The bucket counts is a of counts representing number of measurements that fall into each
38+
* The bucket counts is a list of counts representing number of measurements that fall into each
3939
* bucket.
4040
*
4141
* @return the bucket counts.

Diff for: sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/data/exponentialhistogram/ExponentialHistogramPointData.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
* <p>The bucket boundaries are calculated using both the scale {@link #getScale()}, and the offset
2121
* {@link ExponentialHistogramBuckets#getOffset()}.
2222
*
23-
* <p>See:
24-
* https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponentialhistogram
23+
* <p>See <a
24+
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponentialhistogram">data
25+
* model</a>.
2526
*
2627
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
2728
* at any time.

Diff for: sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramAggregatorTest.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private static Stream<DoubleExponentialHistogramAggregator> provideAggregator()
6868

6969
private static int valueToIndex(int scale, double value) {
7070
double scaleFactor = Math.scalb(1D / Math.log(2), scale);
71-
return (int) Math.floor(Math.log(value) * scaleFactor);
71+
return (int) Math.ceil(Math.log(value) * scaleFactor) - 1;
7272
}
7373

7474
private static ExponentialHistogramAccumulation getTestAccumulation(
@@ -362,8 +362,8 @@ void testInsert1M() {
362362
AggregatorHandle<ExponentialHistogramAccumulation, DoubleExemplarData> handle =
363363
aggregator.createHandle();
364364

365-
double min = 1.0 / (1 << 16);
366365
int n = 1024 * 1024 - 1;
366+
double min = 16.0 / n;
367367
double d = min;
368368
for (int i = 0; i < n; i++) {
369369
handle.recordDouble(d);
@@ -393,7 +393,7 @@ void testDownScale() {
393393
assertThat(Objects.requireNonNull(acc).getScale()).isEqualTo(0);
394394
ExponentialHistogramBuckets buckets = acc.getPositiveBuckets();
395395
assertThat(acc.getSum()).isEqualTo(23.5);
396-
assertThat(buckets.getOffset()).isEqualTo(-1);
396+
assertThat(buckets.getOffset()).isEqualTo(-2);
397397
assertThat(buckets.getBucketCounts()).isEqualTo(Arrays.asList(1L, 1L, 1L, 1L, 0L, 1L));
398398
assertThat(buckets.getTotalCount()).isEqualTo(5);
399399
}

Diff for: sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/internal/aggregator/DoubleExponentialHistogramBucketsTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void testDownscale(ExponentialBucketStrategy buckets) {
6060
MetricAssertions.assertThat(b)
6161
.hasTotalCount(3)
6262
.hasCounts(Arrays.asList(1L, 1L, 1L))
63-
.hasOffset(0);
63+
.hasOffset(-1);
6464
}
6565

6666
@ParameterizedTest
@@ -110,6 +110,6 @@ void testToString(ExponentialBucketStrategy buckets) {
110110
DoubleExponentialHistogramBuckets b = buckets.newBuckets();
111111
b.record(1);
112112
assertThat(b.toString())
113-
.isEqualTo("DoubleExponentialHistogramBuckets{scale: 20, offset: 0, counts: {0=1} }");
113+
.isEqualTo("DoubleExponentialHistogramBuckets{scale: 20, offset: -1, counts: {-1=1} }");
114114
}
115115
}

0 commit comments

Comments
 (0)