Skip to content

Commit 72fbc9c

Browse files
committed
Implement max for exponential histograms
1 parent 0a0e317 commit 72fbc9c

File tree

15 files changed

+302
-69
lines changed

15 files changed

+302
-69
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ public double min() {
8787
return Double.NaN;
8888
}
8989

90+
@Override
91+
public double max() {
92+
return Double.NaN;
93+
}
94+
9095
@Override
9196
public long ramBytesUsed() {
9297
return 0;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
*/
4848
public interface ExponentialHistogram extends Accountable {
4949

50-
// TODO(b/128622): support min/max storage and merging.
5150
// TODO(b/128622): Add special positive and negative infinity buckets
5251
// to allow representation of explicit bucket histograms with open boundaries.
5352

@@ -117,6 +116,13 @@ public interface ExponentialHistogram extends Accountable {
117116
*/
118117
double min();
119118

119+
/**
120+
* Returns maximum of all values represented by this histogram.
121+
*
122+
* @return the minimum, NaN for empty histograms
123+
*/
124+
double max();
125+
120126
/**
121127
* Represents a bucket range of an {@link ExponentialHistogram}, either the positive or the negative range.
122128
*/
@@ -154,6 +160,7 @@ static boolean equals(ExponentialHistogram a, ExponentialHistogram b) {
154160
return a.scale() == b.scale()
155161
&& a.sum() == b.sum()
156162
&& equalsIncludingNaN(a.min(), b.min())
163+
&& equalsIncludingNaN(a.max(), b.max())
157164
&& a.zeroBucket().equals(b.zeroBucket())
158165
&& bucketIteratorsEqual(a.negativeBuckets().iterator(), b.negativeBuckets().iterator())
159166
&& bucketIteratorsEqual(a.positiveBuckets().iterator(), b.positiveBuckets().iterator());
@@ -187,6 +194,7 @@ static int hashCode(ExponentialHistogram histogram) {
187194
hash = 31 * hash + Double.hashCode(histogram.sum());
188195
hash = 31 * hash + Long.hashCode(histogram.valueCount());
189196
hash = 31 * hash + Double.hashCode(histogram.min());
197+
hash = 31 * hash + Double.hashCode(histogram.max());
190198
hash = 31 * hash + histogram.zeroBucket().hashCode();
191199
// we intentionally don't include the hash of the buckets here, because that is likely expensive to compute
192200
// instead, we assume that the value count and sum are a good enough approximation in most cases to minimize collisions

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ private void mergeValuesToHistogram() {
126126
Aggregates aggregates = rawValuesAggregates();
127127
valueBuffer.setSum(aggregates.sum());
128128
valueBuffer.setMin(aggregates.min());
129+
valueBuffer.setMax(aggregates.max());
129130
int scale = valueBuffer.scale();
130131

131132
// Buckets must be provided with their indices in ascending order.
@@ -166,15 +167,17 @@ private void mergeValuesToHistogram() {
166167

167168
private Aggregates rawValuesAggregates() {
168169
if (valueCount == 0) {
169-
return new Aggregates(0, Double.NaN);
170+
return new Aggregates(0, Double.NaN, Double.NaN);
170171
}
171172
double sum = 0;
172173
double min = Double.MAX_VALUE;
174+
double max = -Double.MAX_VALUE;
173175
for (int i = 0; i < valueCount; i++) {
174176
sum += rawValueBuffer[i];
175177
min = Math.min(min, rawValueBuffer[i]);
178+
max = Math.max(max, rawValueBuffer[i]);
176179
}
177-
return new Aggregates(sum, min);
180+
return new Aggregates(sum, min, max);
178181
}
179182

180183
private static long estimateBaseSize(int numBuckets) {
@@ -198,5 +201,5 @@ public void close() {
198201
}
199202
}
200203

201-
private record Aggregates(double sum, double min) {}
204+
private record Aggregates(double sum, double min, double max) {}
202205
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.elasticsearch.core.Releasable;
2828

2929
import java.util.OptionalLong;
30+
import java.util.function.DoubleBinaryOperator;
3031

3132
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.getMaximumScaleIncrease;
3233

@@ -151,7 +152,8 @@ public void add(ExponentialHistogram toAdd) {
151152
}
152153
buffer.setZeroBucket(zeroBucket);
153154
buffer.setSum(a.sum() + b.sum());
154-
buffer.setMin(nanAwareMin(a.min(), b.min()));
155+
buffer.setMin(nanAwareAggregate(a.min(), b.min(), Math::min));
156+
buffer.setMax(nanAwareAggregate(a.max(), b.max(), Math::max));
155157
// We attempt to bring everything to the scale of A.
156158
// This might involve increasing the scale for B, which would increase its indices.
157159
// We need to ensure that we do not exceed MAX_INDEX / MIN_INDEX in this case.
@@ -231,14 +233,14 @@ private static int putBuckets(
231233
return overflowCount;
232234
}
233235

234-
private static double nanAwareMin(double a, double b) {
236+
private static double nanAwareAggregate(double a, double b, DoubleBinaryOperator aggregator) {
235237
if (Double.isNaN(a)) {
236238
return b;
237239
}
238240
if (Double.isNaN(b)) {
239241
return a;
240242
}
241-
return Math.min(a, b);
243+
return aggregator.applyAsDouble(a, b);
242244
}
243245

244246
}

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static double estimateSum(BucketIterator negativeBuckets, BucketIterator
6767
* Estimates the minimum value of the histogram based on the populated buckets.
6868
* The returned value is guaranteed to be less than or equal to the exact minimum value of the histogram values.
6969
* If the histogram is empty, an empty Optional is returned.
70-
*
70+
* <p>
7171
* Note that this method can return +-Infinity if the histogram bucket boundaries are not representable in a double.
7272
*
7373
* @param zeroBucket the zero bucket of the histogram
@@ -102,4 +102,40 @@ public static OptionalDouble estimateMin(
102102
}
103103
return OptionalDouble.empty();
104104
}
105+
106+
/**
107+
* Estimates the maximum value of the histogram based on the populated buckets.
108+
* The returned value is guaranteed to be greater than or equal to the exact maximum value of the histogram values.
109+
* If the histogram is empty, an empty Optional is returned.
110+
* <p>
111+
* Note that this method can return +-Infinity if the histogram bucket boundaries are not representable in a double.
112+
*
113+
* @param zeroBucket the zero bucket of the histogram
114+
* @param negativeBuckets the negative buckets of the histogram
115+
* @param positiveBuckets the positive buckets of the histogram
116+
* @return the estimated minimum
117+
*/
118+
public static OptionalDouble estimateMax(
119+
ZeroBucket zeroBucket,
120+
ExponentialHistogram.Buckets negativeBuckets,
121+
ExponentialHistogram.Buckets positiveBuckets
122+
) {
123+
int scale = negativeBuckets.iterator().scale();
124+
assert scale == positiveBuckets.iterator().scale();
125+
126+
OptionalLong positiveMaxIndex = positiveBuckets.maxBucketIndex();
127+
if (positiveMaxIndex.isPresent()) {
128+
return OptionalDouble.of(ExponentialScaleUtils.getUpperBucketBoundary(positiveMaxIndex.getAsLong(), scale));
129+
}
130+
131+
if (zeroBucket.count() > 0) {
132+
return OptionalDouble.of(zeroBucket.zeroThreshold());
133+
}
134+
135+
BucketIterator negativeBucketsIt = negativeBuckets.iterator();
136+
if (negativeBucketsIt.hasNext()) {
137+
return OptionalDouble.of(-ExponentialScaleUtils.getLowerBucketBoundary(negativeBucketsIt.peekIndex(), scale));
138+
}
139+
return OptionalDouble.empty();
140+
}
105141
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class ExponentialHistogramXContent {
3333
public static final String SCALE_FIELD = "scale";
3434
public static final String SUM_FIELD = "sum";
3535
public static final String MIN_FIELD = "min";
36+
public static final String MAX_FIELD = "max";
3637
public static final String ZERO_FIELD = "zero";
3738
public static final String ZERO_COUNT_FIELD = "count";
3839
public static final String ZERO_THRESHOLD_FIELD = "threshold";
@@ -55,6 +56,9 @@ public static void serialize(XContentBuilder builder, ExponentialHistogram histo
5556
if (Double.isNaN(histogram.min()) == false) {
5657
builder.field(MIN_FIELD, histogram.min());
5758
}
59+
if (Double.isNaN(histogram.max()) == false) {
60+
builder.field(MAX_FIELD, histogram.max());
61+
}
5862
double zeroThreshold = histogram.zeroBucket().zeroThreshold();
5963
long zeroCount = histogram.zeroBucket().count();
6064

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ final class FixedCapacityExponentialHistogram extends AbstractExponentialHistogr
5555

5656
private double sum;
5757
private double min;
58+
private double max;
5859

5960
private final ExponentialHistogramCircuitBreaker circuitBreaker;
6061
private boolean closed = false;
@@ -83,6 +84,7 @@ private FixedCapacityExponentialHistogram(int bucketCapacity, ExponentialHistogr
8384
void reset() {
8485
sum = 0;
8586
min = Double.NaN;
87+
max = Double.NaN;
8688
setZeroBucket(ZeroBucket.minimalEmpty());
8789
resetBuckets(MAX_SCALE);
8890
}
@@ -133,6 +135,15 @@ void setMin(double min) {
133135
this.min = min;
134136
}
135137

138+
@Override
139+
public double max() {
140+
return max;
141+
}
142+
143+
void setMax(double max) {
144+
this.max = max;
145+
}
146+
136147
/**
137148
* Attempts to add a bucket to the positive or negative range of this histogram.
138149
* <br>

libs/exponential-histogram/src/test/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramEqualityTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public enum Modification {
4141
SCALE,
4242
SUM,
4343
MIN,
44+
MAX,
4445
ZERO_THRESHOLD,
4546
ZERO_COUNT,
4647
POSITIVE_BUCKETS,
@@ -102,6 +103,11 @@ private ExponentialHistogram copyWithModification(ExponentialHistogram toCopy, M
102103
} else {
103104
copy.setMin(toCopy.min());
104105
}
106+
if (modification == Modification.MAX) {
107+
copy.setMax(randomDouble());
108+
} else {
109+
copy.setMax(toCopy.max());
110+
}
105111
long zeroCount = toCopy.zeroBucket().count();
106112
double zeroThreshold = toCopy.zeroBucket().zeroThreshold();
107113
if (modification == Modification.ZERO_COUNT) {

libs/exponential-histogram/src/test/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramMergerTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public void testAggregatesCorrectness() {
112112
double[] secondValues = randomDoubles(50).map(val -> val * 2 - 1).toArray();
113113
double correctSum = Arrays.stream(firstValues).sum() + Arrays.stream(secondValues).sum();
114114
double correctMin = DoubleStream.concat(Arrays.stream(firstValues), Arrays.stream(secondValues)).min().getAsDouble();
115+
double correctMax = DoubleStream.concat(Arrays.stream(firstValues), Arrays.stream(secondValues)).max().getAsDouble();
115116
try (
116117
// Merge some empty histograms too to test that code path
117118
ReleasableExponentialHistogram merged = ExponentialHistogram.merge(
@@ -125,6 +126,7 @@ public void testAggregatesCorrectness() {
125126
) {
126127
assertThat(merged.sum(), closeTo(correctSum, 0.000001));
127128
assertThat(merged.min(), equalTo(correctMin));
129+
assertThat(merged.max(), equalTo(correctMax));
128130
}
129131
}
130132

libs/exponential-histogram/src/test/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramUtilsTests.java

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,15 @@ public void testSumInfinityHandling() {
8686
assertThat(sum, equalTo(Double.NEGATIVE_INFINITY));
8787
}
8888

89-
public void testMinimumEstimation() {
89+
public void testMinMaxEstimation() {
9090
for (int i = 0; i < 100; i++) {
9191
int positiveValueCount = randomBoolean() ? 0 : randomIntBetween(10, 10_000);
9292
int negativeValueCount = randomBoolean() ? 0 : randomIntBetween(10, 10_000);
9393
int zeroValueCount = randomBoolean() ? 0 : randomIntBetween(10, 100);
9494
int bucketCount = randomIntBetween(4, 500);
9595

9696
double correctMin = Double.MAX_VALUE;
97+
double correctMax = -Double.MAX_VALUE;
9798
double zeroThreshold = Double.MAX_VALUE;
9899
double[] values = new double[positiveValueCount + negativeValueCount];
99100
for (int j = 0; j < values.length; j++) {
@@ -105,56 +106,84 @@ public void testMinimumEstimation() {
105106
}
106107
zeroThreshold = Math.min(zeroThreshold, absValue / 2);
107108
correctMin = Math.min(correctMin, values[j]);
109+
correctMax = Math.max(correctMax, values[j]);
108110
}
109111
if (zeroValueCount > 0) {
110112
correctMin = Math.min(correctMin, -zeroThreshold);
113+
correctMax = Math.max(correctMax, zeroThreshold);
111114
}
112115

113116
ExponentialHistogram histo = createAutoReleasedHistogram(bucketCount, values);
114117

118+
ZeroBucket zeroBucket = ZeroBucket.create(zeroThreshold, zeroValueCount);
115119
OptionalDouble estimatedMin = ExponentialHistogramUtils.estimateMin(
116-
ZeroBucket.create(zeroThreshold, zeroValueCount),
120+
zeroBucket,
121+
histo.negativeBuckets(),
122+
histo.positiveBuckets()
123+
);
124+
OptionalDouble estimatedMax = ExponentialHistogramUtils.estimateMax(
125+
zeroBucket,
117126
histo.negativeBuckets(),
118127
histo.positiveBuckets()
119128
);
120129
if (correctMin == Double.MAX_VALUE) {
121130
assertThat(estimatedMin.isPresent(), equalTo(false));
131+
assertThat(estimatedMax.isPresent(), equalTo(false));
122132
} else {
123133
assertThat(estimatedMin.isPresent(), equalTo(true));
134+
assertThat(estimatedMax.isPresent(), equalTo(true));
124135
// If the histogram does not contain mixed sign values, we have a guaranteed relative error bound of 2^(2^-scale) - 1
125136
double histogramBase = Math.pow(2, Math.pow(2, -histo.scale()));
126-
double allowedError = Math.abs(correctMin * (histogramBase - 1));
127-
assertThat(estimatedMin.getAsDouble(), closeTo(correctMin, allowedError));
137+
double allowedErrorMin = Math.abs(correctMin * (histogramBase - 1));
138+
assertThat(estimatedMin.getAsDouble(), closeTo(correctMin, allowedErrorMin));
139+
double allowedErrorMax = Math.abs(correctMax * (histogramBase - 1));
140+
assertThat(estimatedMax.getAsDouble(), closeTo(correctMax, allowedErrorMax));
128141
}
129142
}
130143
}
131144

132-
public void testMinimumEstimationPositiveInfinityHandling() {
145+
public void testMinMaxEstimationPositiveInfinityHandling() {
133146
FixedCapacityExponentialHistogram histo = createAutoReleasedHistogram(100);
134147
histo.resetBuckets(0);
135148
histo.tryAddBucket(2000, 1, true);
136149

137-
OptionalDouble estimate = ExponentialHistogramUtils.estimateMin(
150+
OptionalDouble minEstimate = ExponentialHistogramUtils.estimateMin(
138151
ZeroBucket.minimalEmpty(),
139152
histo.negativeBuckets(),
140153
histo.positiveBuckets()
141154
);
142-
assertThat(estimate.isPresent(), equalTo(true));
143-
assertThat(estimate.getAsDouble(), equalTo(Double.POSITIVE_INFINITY));
155+
assertThat(minEstimate.isPresent(), equalTo(true));
156+
assertThat(minEstimate.getAsDouble(), equalTo(Double.POSITIVE_INFINITY));
157+
158+
OptionalDouble maxEstimate = ExponentialHistogramUtils.estimateMax(
159+
ZeroBucket.minimalEmpty(),
160+
histo.negativeBuckets(),
161+
histo.positiveBuckets()
162+
);
163+
assertThat(maxEstimate.isPresent(), equalTo(true));
164+
assertThat(maxEstimate.getAsDouble(), equalTo(Double.POSITIVE_INFINITY));
144165
}
145166

146-
public void testMinimumEstimationNegativeInfinityHandling() {
167+
public void testMinMaxEstimationNegativeInfinityHandling() {
147168
FixedCapacityExponentialHistogram histo = createAutoReleasedHistogram(100);
148169
histo.resetBuckets(0);
149170
histo.tryAddBucket(2000, 1, false);
150171

151-
OptionalDouble estimate = ExponentialHistogramUtils.estimateMin(
172+
OptionalDouble minEstimate = ExponentialHistogramUtils.estimateMin(
152173
ZeroBucket.minimalEmpty(),
153174
histo.negativeBuckets(),
154175
histo.positiveBuckets()
155176
);
156-
assertThat(estimate.isPresent(), equalTo(true));
157-
assertThat(estimate.getAsDouble(), equalTo(Double.NEGATIVE_INFINITY));
177+
assertThat(minEstimate.isPresent(), equalTo(true));
178+
assertThat(minEstimate.getAsDouble(), equalTo(Double.NEGATIVE_INFINITY));
179+
180+
OptionalDouble maxEstimate = ExponentialHistogramUtils.estimateMax(
181+
ZeroBucket.minimalEmpty(),
182+
histo.negativeBuckets(),
183+
histo.positiveBuckets()
184+
);
185+
assertThat(maxEstimate.isPresent(), equalTo(true));
186+
assertThat(maxEstimate.getAsDouble(), equalTo(Double.NEGATIVE_INFINITY));
158187
}
159188

160189
public void testMinimumEstimationSanitizedNegativeZero() {

0 commit comments

Comments
 (0)