Skip to content

Commit 23e8084

Browse files
committed
Implement exponential histogram equality
1 parent d8257b4 commit 23e8084

File tree

7 files changed

+164
-28
lines changed

7 files changed

+164
-28
lines changed

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,58 @@ static ExponentialHistogram empty() {
120120
return EmptyExponentialHistogram.INSTANCE;
121121
}
122122

123+
static boolean equals(ExponentialHistogram a, ExponentialHistogram b) {
124+
if (a == b) {
125+
return true;
126+
}
127+
if (a == null || b == null) {
128+
return false;
129+
}
130+
if (a.scale() != b.scale()) {
131+
return false;
132+
}
133+
if (a.zeroBucket().count() != b.zeroBucket().count()) {
134+
return false;
135+
}
136+
if (Double.compare(a.zeroBucket().zeroThreshold(), b.zeroBucket().zeroThreshold()) != 0) {
137+
return false;
138+
}
139+
if (a.positiveBuckets().valueCount() != b.positiveBuckets().valueCount()) {
140+
return false;
141+
}
142+
if (a.negativeBuckets().valueCount() != b.negativeBuckets().valueCount()) {
143+
return false;
144+
}
145+
BucketIterator itA = a.positiveBuckets().iterator();
146+
BucketIterator itB = b.positiveBuckets().iterator();
147+
while (itA.hasNext() && itB.hasNext()) {
148+
if (itA.peekIndex() != itB.peekIndex()) {
149+
return false;
150+
}
151+
if (itA.peekCount() != itB.peekCount()) {
152+
return false;
153+
}
154+
itA.advance();
155+
itB.advance();
156+
}
157+
if (itA.hasNext() || itB.hasNext()) {
158+
return false;
159+
}
160+
itA = a.negativeBuckets().iterator();
161+
itB = b.negativeBuckets().iterator();
162+
while (itA.hasNext() && itB.hasNext()) {
163+
if (itA.peekIndex() != itB.peekIndex()) {
164+
return false;
165+
}
166+
if (itA.peekCount() != itB.peekCount()) {
167+
return false;
168+
}
169+
itA.advance();
170+
itB.advance();
171+
}
172+
return itA.hasNext() == false && itB.hasNext() == false;
173+
}
174+
123175
/**
124176
* Creates a histogram representing the distribution of the given values with at most the given number of buckets.
125177
* If the given {@code maxBucketCount} is greater than or equal to the number of values, the resulting histogram will have a

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ public static int getMaximumScaleIncrease(long index) {
185185
return Long.numberOfLeadingZeros(index) - (64 - MAX_INDEX_BITS);
186186
}
187187

188+
189+
/**
190+
* Returns a scale at to which the given index can be scaled down without changing the exponentially scaled number it represents.
191+
* @param index the index of the number
192+
* @param scale the current scale of the number
193+
* @return the new scale
194+
*/
195+
static int normalizeScale(long index, int scale) {
196+
return Math.max(MIN_SCALE, scale - Long.numberOfTrailingZeros(index));
197+
}
198+
188199
/**
189200
* Returns the upper boundary of the bucket with the given index and scale.
190201
*

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

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MAX_SCALE;
2828
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_INDEX;
2929
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_SCALE;
30+
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.adjustScale;
3031
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.compareExponentiallyScaledValues;
3132
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.computeIndex;
3233
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.exponentiallyScaledToDoubleValue;
34+
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.normalizeScale;
3335

3436
/**
3537
* Represents the bucket for values around zero in an exponential histogram.
@@ -62,13 +64,7 @@ public final class ZeroBucket {
6264
// A singleton for an empty zero bucket with the smallest possible threshold.
6365
private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket(MIN_INDEX, MIN_SCALE, 0);
6466

65-
/**
66-
* Creates a new zero bucket with a specific threshold and count.
67-
*
68-
* @param zeroThreshold The threshold defining the bucket's range [-zeroThreshold, +zeroThreshold].
69-
* @param count The number of values in the bucket.
70-
*/
71-
public ZeroBucket(double zeroThreshold, long count) {
67+
private ZeroBucket(double zeroThreshold, long count) {
7268
assert zeroThreshold >= 0.0 : "zeroThreshold must not be negative";
7369
this.index = Long.MAX_VALUE; // compute lazily when needed
7470
this.scale = MAX_SCALE;
@@ -85,11 +81,11 @@ private ZeroBucket(long index, int scale, long count) {
8581
this.count = count;
8682
}
8783

88-
private ZeroBucket(double realThreshold, long index, int scale, long count) {
89-
this.realThreshold = realThreshold;
90-
this.index = index;
91-
this.scale = scale;
92-
this.count = count;
84+
private ZeroBucket(ZeroBucket toCopy, long newCount) {
85+
this.realThreshold = toCopy.realThreshold;
86+
this.index = toCopy.index;
87+
this.scale = toCopy.scale;
88+
this.count = newCount;
9389
}
9490

9591
/**
@@ -109,8 +105,37 @@ public static ZeroBucket minimalWithCount(long count) {
109105
if (count == 0) {
110106
return MINIMAL_EMPTY;
111107
} else {
112-
return new ZeroBucket(MINIMAL_EMPTY.zeroThreshold(), MINIMAL_EMPTY.index(), MINIMAL_EMPTY.scale(), count);
108+
return new ZeroBucket(MINIMAL_EMPTY, count);
109+
}
110+
}
111+
112+
/**
113+
* Creates a zero bucket from the given threshold represented as double.
114+
*
115+
* @param zeroThreshold the zero threshold defining the bucket range [-zeroThreshold, +zeroThreshold], must be non-negative
116+
* @param count the number of values in the bucket
117+
* @return the new {@link ZeroBucket}
118+
*/
119+
public static ZeroBucket create(double zeroThreshold, long count) {
120+
if (zeroThreshold == 0) {
121+
return minimalWithCount(count);
113122
}
123+
return new ZeroBucket(zeroThreshold, count);
124+
}
125+
126+
/**
127+
* Creates a zero bucket from the given threshold represented as exponentially scaled number.
128+
*
129+
* @param index the index of the exponentially scaled number defining the zero threshold
130+
* @param scale the corresponding scale for the index
131+
* @param count the number of values in the bucket
132+
* @return the new {@link ZeroBucket}
133+
*/
134+
public static ZeroBucket create(long index, int scale, long count) {
135+
if (index == MINIMAL_EMPTY.index && scale == MINIMAL_EMPTY.scale) {
136+
return minimalWithCount(count);
137+
}
138+
return new ZeroBucket(index, scale, count);
114139
}
115140

116141
/**
@@ -158,9 +183,9 @@ public ZeroBucket merge(ZeroBucket other) {
158183
long totalCount = count + other.count;
159184
// Both are populated, so we need to use the higher zero-threshold.
160185
if (this.compareZeroThreshold(other) >= 0) {
161-
return new ZeroBucket(realThreshold, index, scale, totalCount);
186+
return new ZeroBucket(this, totalCount);
162187
} else {
163-
return new ZeroBucket(other.realThreshold, other.index, other.scale, totalCount);
188+
return new ZeroBucket(other, totalCount);
164189
}
165190
}
166191
}
@@ -219,10 +244,33 @@ public ZeroBucket collapseOverlappingBuckets(BucketIterator buckets) {
219244
long collapsedUpperBoundIndex = highestCollapsedIndex + 1;
220245
if (compareExponentiallyScaledValues(index(), scale(), collapsedUpperBoundIndex, buckets.scale()) >= 0) {
221246
// Our current zero-threshold is larger than the upper boundary of the largest collapsed bucket, so we keep it.
222-
return new ZeroBucket(realThreshold, index, scale, newZeroCount);
247+
return new ZeroBucket(this, newZeroCount);
223248
} else {
224249
return new ZeroBucket(collapsedUpperBoundIndex, buckets.scale(), newZeroCount);
225250
}
226251
}
227252
}
253+
254+
@Override
255+
public boolean equals(Object o) {
256+
if (o == null || getClass() != o.getClass()) return false;
257+
ZeroBucket that = (ZeroBucket) o;
258+
if (count() != that.count()) return false;
259+
if (Double.compare(zeroThreshold(), that.zeroThreshold()) != 0) return false;
260+
if (compareExponentiallyScaledValues(index(), scale(), that.index(), that.scale()) != 0) return false;
261+
return true;
262+
}
263+
264+
@Override
265+
public int hashCode() {
266+
int normalizedScale = normalizeScale(index(), scale);
267+
int scaleAdjustment = normalizedScale - scale;
268+
long normalizedIndex = adjustScale(index(), scale, scaleAdjustment);
269+
270+
int result = normalizedScale;
271+
result = 31 * result + Long.hashCode(normalizedIndex);
272+
result = 31 * result + Double.hashCode(zeroThreshold());
273+
result = 31 * result + Long.hashCode(count);
274+
return result;
275+
}
228276
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class ExponentialHistogramMergerTests extends ExponentialHistogramTestCas
4343
public void testZeroThresholdCollapsesOverlappingBuckets() {
4444

4545
FixedCapacityExponentialHistogram first = createAutoReleasedHistogram(100);
46-
first.setZeroBucket(new ZeroBucket(2.0001, 10));
46+
first.setZeroBucket(ZeroBucket.create(2.0001, 10));
4747

4848
FixedCapacityExponentialHistogram second = createAutoReleasedHistogram(100);
4949
first.resetBuckets(0); // scale 0 means base 2
@@ -76,7 +76,7 @@ public void testZeroThresholdCollapsesOverlappingBuckets() {
7676

7777
// ensure buckets of the accumulated histogram are collapsed too if needed
7878
FixedCapacityExponentialHistogram third = createAutoReleasedHistogram(100);
79-
third.setZeroBucket(new ZeroBucket(45.0, 1));
79+
third.setZeroBucket(ZeroBucket.create(45.0, 1));
8080

8181
mergeResult = mergeWithMinimumScale(100, 0, mergeResult, third);
8282
assertThat(mergeResult.zeroBucket().zeroThreshold(), closeTo(45.0, 0.000001));
@@ -87,12 +87,12 @@ public void testZeroThresholdCollapsesOverlappingBuckets() {
8787

8888
public void testEmptyZeroBucketIgnored() {
8989
FixedCapacityExponentialHistogram first = createAutoReleasedHistogram(100);
90-
first.setZeroBucket(new ZeroBucket(2.0, 10));
90+
first.setZeroBucket(ZeroBucket.create(2.0, 10));
9191
first.resetBuckets(0); // scale 0 means base 2
9292
first.tryAddBucket(2, 42L, true); // bucket (4, 8]
9393

9494
FixedCapacityExponentialHistogram second = createAutoReleasedHistogram(100);
95-
second.setZeroBucket(new ZeroBucket(100.0, 0));
95+
second.setZeroBucket(ZeroBucket.create(100.0, 0));
9696

9797
ExponentialHistogram mergeResult = mergeWithMinimumScale(100, 0, first, second);
9898

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void testEmptyHistogram() {
3838

3939
public void testFullHistogram() {
4040
FixedCapacityExponentialHistogram histo = createAutoReleasedHistogram(100);
41-
histo.setZeroBucket(new ZeroBucket(0.1234, 42));
41+
histo.setZeroBucket(ZeroBucket.create(0.1234, 42));
4242
histo.resetBuckets(7);
4343
histo.tryAddBucket(-10, 15, false);
4444
histo.tryAddBucket(10, 5, false);
@@ -59,14 +59,14 @@ public void testFullHistogram() {
5959

6060
public void testOnlyZeroThreshold() {
6161
FixedCapacityExponentialHistogram histo = createAutoReleasedHistogram(10);
62-
histo.setZeroBucket(new ZeroBucket(5.0, 0));
62+
histo.setZeroBucket(ZeroBucket.create(5.0, 0));
6363
histo.resetBuckets(3);
6464
assertThat(toJson(histo), equalTo("{\"scale\":3,\"zero\":{\"threshold\":5.0}}"));
6565
}
6666

6767
public void testOnlyZeroCount() {
6868
FixedCapacityExponentialHistogram histo = createAutoReleasedHistogram(10);
69-
histo.setZeroBucket(new ZeroBucket(0.0, 7));
69+
histo.setZeroBucket(ZeroBucket.create(0.0, 7));
7070
histo.resetBuckets(2);
7171
assertThat(toJson(histo), equalTo("{\"scale\":2,\"zero\":{\"count\":7}}"));
7272
}

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
package org.elasticsearch.exponentialhistogram;
2323

2424
import static org.hamcrest.Matchers.equalTo;
25+
import static org.hamcrest.Matchers.not;
2526

2627
public class ZeroBucketTests extends ExponentialHistogramTestCase {
2728

@@ -30,13 +31,13 @@ public void testMinimalBucketHasZeroThreshold() {
3031
}
3132

3233
public void testExactThresholdPreserved() {
33-
ZeroBucket bucket = new ZeroBucket(3.0, 10);
34+
ZeroBucket bucket = ZeroBucket.create(3.0, 10);
3435
assertThat(bucket.zeroThreshold(), equalTo(3.0));
3536
}
3637

3738
public void testMergingPreservesExactThreshold() {
38-
ZeroBucket bucketA = new ZeroBucket(3.0, 10);
39-
ZeroBucket bucketB = new ZeroBucket(3.5, 20);
39+
ZeroBucket bucketA = ZeroBucket.create(3.0, 10);
40+
ZeroBucket bucketB = ZeroBucket.create(3.5, 20);
4041
ZeroBucket merged = bucketA.merge(bucketB);
4142
assertThat(merged.zeroThreshold(), equalTo(3.5));
4243
assertThat(merged.count(), equalTo(30L));
@@ -47,7 +48,7 @@ public void testBucketCollapsingPreservesExactThreshold() {
4748
histo.resetBuckets(0);
4849
histo.tryAddBucket(0, 42, true); // bucket [1,2]
4950

50-
ZeroBucket bucketA = new ZeroBucket(3.0, 10);
51+
ZeroBucket bucketA = ZeroBucket.create(3.0, 10);
5152

5253
CopyableBucketIterator iterator = histo.positiveBuckets().iterator();
5354
ZeroBucket merged = bucketA.collapseOverlappingBuckets(iterator);
@@ -57,4 +58,28 @@ public void testBucketCollapsingPreservesExactThreshold() {
5758
assertThat(merged.count(), equalTo(52L));
5859
}
5960

61+
public void testHashCodeEquality() {
62+
assertEqualsContract(ZeroBucket.minimalEmpty(), ZeroBucket.create(0.0, 0));
63+
assertThat(ZeroBucket.minimalEmpty(), not(equalTo(ZeroBucket.create(0.0, 1))));
64+
assertThat(ZeroBucket.minimalEmpty(), not(equalTo(ZeroBucket.create(0.1, 0))));
65+
66+
assertEqualsContract(ZeroBucket.minimalWithCount(42), ZeroBucket.create(0.0, 42));
67+
assertThat(ZeroBucket.minimalWithCount(42), not(equalTo(ZeroBucket.create(0.0, 12))));
68+
assertThat(ZeroBucket.minimalWithCount(42), not(equalTo(ZeroBucket.create(0.1, 42))));
69+
70+
ZeroBucket minimalEmpty = ZeroBucket.minimalEmpty();
71+
assertEqualsContract(ZeroBucket.minimalWithCount(42), ZeroBucket.create(minimalEmpty.index(), minimalEmpty.scale(), 42));
72+
assertThat(ZeroBucket.minimalWithCount(42), not(equalTo(ZeroBucket.create(minimalEmpty.index(), minimalEmpty.scale(), 41))));
73+
assertThat(ZeroBucket.minimalWithCount(42), not(equalTo(ZeroBucket.create(minimalEmpty.index() + 1, minimalEmpty.scale(), 42))));
74+
75+
assertEqualsContract(ZeroBucket.create(123.56, 123), ZeroBucket.create(123.56, 123));
76+
assertThat(ZeroBucket.create(123.56, 123), not(equalTo(ZeroBucket.create(123.57, 123))));
77+
assertThat(ZeroBucket.create(123.56, 123), not(equalTo(ZeroBucket.create(123.56, 12))));
78+
}
79+
80+
void assertEqualsContract(ZeroBucket a, ZeroBucket b) {
81+
assertThat(a, equalTo(b));
82+
assertThat(a.hashCode(), equalTo(b.hashCode()));
83+
}
84+
6085
}

x-pack/plugin/mapper-exponential-histogram/src/main/java/org/elasticsearch/xpack/exponentialhistogram/CompressedExponentialHistogram.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public int scale() {
4848
public ZeroBucket zeroBucket() {
4949
if (lazyZeroBucket == null) {
5050
long zeroCount = valueCount - negativeBuckets.valueCount() - positiveBuckets.valueCount();
51-
lazyZeroBucket = new ZeroBucket(zeroThreshold, zeroCount);
51+
lazyZeroBucket = ZeroBucket.create(zeroThreshold, zeroCount);
5252
}
5353
return lazyZeroBucket;
5454
}

0 commit comments

Comments
 (0)