Skip to content

Commit 22af544

Browse files
authored
Add builder for exponential histograms (elastic#133967)
1 parent 16fb8b8 commit 22af544

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

libs/exponential-histogram/src/main/java/org/elasticsearch/exponentialhistogram/ExponentialHistogram.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@ static ExponentialHistogram empty() {
206206
return EmptyExponentialHistogram.INSTANCE;
207207
}
208208

209+
/**
210+
* Create a builder for an exponential histogram with the given scale.
211+
* @param scale the scale of the histogram to build
212+
* @param breaker the circuit breaker to use
213+
* @return a new builder
214+
*/
215+
static ExponentialHistogramBuilder builder(int scale, ExponentialHistogramCircuitBreaker breaker) {
216+
return new ExponentialHistogramBuilder(scale, breaker);
217+
}
218+
209219
/**
210220
* Creates a histogram representing the distribution of the given values with at most the given number of buckets.
211221
* If the given {@code maxBucketCount} is greater than or equal to the number of values, the resulting histogram will have a
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright Elasticsearch B.V., and/or licensed to Elasticsearch B.V.
3+
* under one or more license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*
19+
* This file is based on a modification of https://github.com/open-telemetry/opentelemetry-java which is licensed under the Apache 2.0 License.
20+
*/
21+
22+
package org.elasticsearch.exponentialhistogram;
23+
24+
import org.elasticsearch.core.Releasables;
25+
26+
import java.util.TreeMap;
27+
28+
/**
29+
* A builder for building a {@link ReleasableExponentialHistogram} directly from buckets.
30+
* Note that this class is not optimized regarding memory allocations, so it is not intended for high-throughput usage.
31+
*/
32+
public class ExponentialHistogramBuilder {
33+
34+
private final ExponentialHistogramCircuitBreaker breaker;
35+
36+
private final int scale;
37+
private ZeroBucket zeroBucket = ZeroBucket.minimalEmpty();
38+
private Double sum;
39+
private Double min;
40+
private Double max;
41+
42+
private final TreeMap<Long, Long> negativeBuckets = new TreeMap<>();
43+
private final TreeMap<Long, Long> positiveBuckets = new TreeMap<>();
44+
45+
ExponentialHistogramBuilder(int scale, ExponentialHistogramCircuitBreaker breaker) {
46+
this.breaker = breaker;
47+
this.scale = scale;
48+
}
49+
50+
public ExponentialHistogramBuilder zeroBucket(ZeroBucket zeroBucket) {
51+
this.zeroBucket = zeroBucket;
52+
return this;
53+
}
54+
55+
/**
56+
* Sets the sum of the histogram values. If not set, the sum will be estimated from the buckets.
57+
* @param sum the sum value
58+
* @return the builder
59+
*/
60+
public ExponentialHistogramBuilder sum(double sum) {
61+
this.sum = sum;
62+
return this;
63+
}
64+
65+
/**
66+
* Sets the min value of the histogram values. If not set, the min will be estimated from the buckets.
67+
* @param min the min value
68+
* @return the builder
69+
*/
70+
public ExponentialHistogramBuilder min(double min) {
71+
this.min = min;
72+
return this;
73+
}
74+
75+
/**
76+
* Sets the max value of the histogram values. If not set, the max will be estimated from the buckets.
77+
* @param max the max value
78+
* @return the builder
79+
*/
80+
public ExponentialHistogramBuilder max(double max) {
81+
this.max = max;
82+
return this;
83+
}
84+
85+
/**
86+
* Adds the given bucket to the positive buckets.
87+
* Buckets may be added in arbitrary order, but each bucket can only be added once.
88+
*
89+
* @param index the index of the bucket
90+
* @param count the count of the bucket, must be at least 1
91+
* @return the builder
92+
*/
93+
public ExponentialHistogramBuilder addPositiveBucket(long index, long count) {
94+
if (count < 1) {
95+
throw new IllegalArgumentException("Bucket count must be at least 1");
96+
}
97+
if (positiveBuckets.containsKey(index)) {
98+
throw new IllegalArgumentException("Positive bucket already exists: " + index);
99+
}
100+
positiveBuckets.put(index, count);
101+
return this;
102+
}
103+
104+
/**
105+
* Adds the given bucket to the negative buckets.
106+
* Buckets may be added in arbitrary order, but each bucket can only be added once.
107+
*
108+
* @param index the index of the bucket
109+
* @param count the count of the bucket, must be at least 1
110+
* @return the builder
111+
*/
112+
public ExponentialHistogramBuilder addNegativeBucket(long index, long count) {
113+
if (count < 1) {
114+
throw new IllegalArgumentException("Bucket count must be at least 1");
115+
}
116+
if (negativeBuckets.containsKey(index)) {
117+
throw new IllegalArgumentException("Negative bucket already exists: " + index);
118+
}
119+
negativeBuckets.put(index, count);
120+
return this;
121+
}
122+
123+
public ReleasableExponentialHistogram build() {
124+
FixedCapacityExponentialHistogram result = FixedCapacityExponentialHistogram.create(
125+
negativeBuckets.size() + positiveBuckets.size(),
126+
breaker
127+
);
128+
boolean success = false;
129+
try {
130+
result.resetBuckets(scale);
131+
result.setZeroBucket(zeroBucket);
132+
negativeBuckets.forEach((index, count) -> result.tryAddBucket(index, count, false));
133+
positiveBuckets.forEach((index, count) -> result.tryAddBucket(index, count, true));
134+
135+
double sumVal = (sum != null)
136+
? sum
137+
: ExponentialHistogramUtils.estimateSum(result.negativeBuckets().iterator(), result.positiveBuckets().iterator());
138+
double minVal = (min != null)
139+
? min
140+
: ExponentialHistogramUtils.estimateMin(zeroBucket, result.negativeBuckets(), result.positiveBuckets()).orElse(Double.NaN);
141+
double maxVal = (max != null)
142+
? max
143+
: ExponentialHistogramUtils.estimateMax(zeroBucket, result.negativeBuckets(), result.positiveBuckets()).orElse(Double.NaN);
144+
145+
result.setMin(minVal);
146+
result.setMax(maxVal);
147+
result.setSum(sumVal);
148+
149+
success = true;
150+
} finally {
151+
if (success == false) {
152+
Releasables.close(result);
153+
}
154+
}
155+
156+
// Create histogram
157+
return result;
158+
}
159+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Elasticsearch B.V., and/or licensed to Elasticsearch B.V.
3+
* under one or more license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*
19+
* This file is based on a modification of https://github.com/open-telemetry/opentelemetry-java which is licensed under the Apache 2.0 License.
20+
*/
21+
22+
package org.elasticsearch.exponentialhistogram;
23+
24+
import java.util.List;
25+
26+
import static org.hamcrest.Matchers.equalTo;
27+
28+
public class ExponentialHistogramBuilderTests extends ExponentialHistogramTestCase {
29+
30+
public void testBuildWithAllFieldsSet() {
31+
ZeroBucket zeroBucket = ZeroBucket.create(1, 2);
32+
ExponentialHistogramBuilder builder = ExponentialHistogram.builder(3, breaker())
33+
.zeroBucket(zeroBucket)
34+
.sum(100.0)
35+
.min(1.0)
36+
.max(50.0)
37+
.addPositiveBucket(2, 10)
38+
.addPositiveBucket(0, 1)
39+
.addPositiveBucket(5, 2)
40+
.addNegativeBucket(-2, 5)
41+
.addNegativeBucket(1, 2);
42+
43+
try (ReleasableExponentialHistogram histogram = builder.build()) {
44+
assertThat(histogram.scale(), equalTo(3));
45+
assertThat(histogram.zeroBucket(), equalTo(zeroBucket));
46+
assertThat(histogram.sum(), equalTo(100.0));
47+
assertThat(histogram.min(), equalTo(1.0));
48+
assertThat(histogram.max(), equalTo(50.0));
49+
assertBuckets(histogram.positiveBuckets(), List.of(0L, 2L, 5L), List.of(1L, 10L, 2L));
50+
assertBuckets(histogram.negativeBuckets(), List.of(-2L, 1L), List.of(5L, 2L));
51+
}
52+
}
53+
54+
public void testBuildWithEstimation() {
55+
ExponentialHistogramBuilder builder = ExponentialHistogram.builder(0, breaker())
56+
.addPositiveBucket(0, 1)
57+
.addPositiveBucket(1, 1)
58+
.addNegativeBucket(0, 4);
59+
60+
try (ReleasableExponentialHistogram histogram = builder.build()) {
61+
assertThat(histogram.scale(), equalTo(0));
62+
assertThat(histogram.zeroBucket(), equalTo(ZeroBucket.minimalEmpty()));
63+
assertThat(histogram.sum(), equalTo(-1.3333333333333335));
64+
assertThat(histogram.min(), equalTo(-2.0));
65+
assertThat(histogram.max(), equalTo(4.0));
66+
assertBuckets(histogram.positiveBuckets(), List.of(0L, 1L), List.of(1L, 1L));
67+
assertBuckets(histogram.negativeBuckets(), List.of(0L), List.of(4L));
68+
}
69+
}
70+
71+
public void testAddDuplicatePositiveBucketThrows() {
72+
ExponentialHistogramBuilder builder = ExponentialHistogram.builder(0, breaker());
73+
builder.addPositiveBucket(1, 10);
74+
expectThrows(IllegalArgumentException.class, () -> builder.addPositiveBucket(1, 5));
75+
}
76+
77+
public void testAddDuplicateNegativeBucketThrows() {
78+
ExponentialHistogramBuilder builder = ExponentialHistogram.builder(0, breaker());
79+
builder.addNegativeBucket(-1, 10);
80+
expectThrows(IllegalArgumentException.class, () -> builder.addNegativeBucket(-1, 5));
81+
}
82+
83+
private static void assertBuckets(ExponentialHistogram.Buckets buckets, List<Long> indices, List<Long> counts) {
84+
List<Long> actualIndices = new java.util.ArrayList<>();
85+
List<Long> actualCounts = new java.util.ArrayList<>();
86+
BucketIterator it = buckets.iterator();
87+
while (it.hasNext()) {
88+
actualIndices.add(it.peekIndex());
89+
actualCounts.add(it.peekCount());
90+
it.advance();
91+
}
92+
assertThat("Expected bucket indices to match", actualIndices, equalTo(indices));
93+
assertThat("Expected bucket counts to match", actualCounts, equalTo(counts));
94+
}
95+
}

0 commit comments

Comments
 (0)