Skip to content

Commit a88b46b

Browse files
JonasKunzJeremyDahlgren
authored andcommitted
Implement hashcode/equals for exponential histograms (elastic#133648)
1 parent 525417a commit a88b46b

File tree

14 files changed

+418
-35
lines changed

14 files changed

+418
-35
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
/**
25+
* Basic implementation for {@link ExponentialHistogram} with common functionality.
26+
*/
27+
public abstract class AbstractExponentialHistogram implements ExponentialHistogram {
28+
29+
@Override
30+
public long valueCount() {
31+
return zeroBucket().count() + negativeBuckets().valueCount() + positiveBuckets().valueCount();
32+
}
33+
34+
@Override
35+
public int hashCode() {
36+
return ExponentialHistogram.hashCode(this);
37+
}
38+
39+
@Override
40+
public boolean equals(Object obj) {
41+
if (this == obj) {
42+
return true;
43+
}
44+
if (obj instanceof ExponentialHistogram) {
45+
return ExponentialHistogram.equals(this, (ExponentialHistogram) obj);
46+
}
47+
return false;
48+
}
49+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import java.util.OptionalLong;
2525

26-
class EmptyExponentialHistogram implements ReleasableExponentialHistogram {
26+
class EmptyExponentialHistogram extends AbstractExponentialHistogram implements ReleasableExponentialHistogram {
2727

2828
static final EmptyExponentialHistogram INSTANCE = new EmptyExponentialHistogram();
2929

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ public interface ExponentialHistogram extends Accountable {
102102
*/
103103
double sum();
104104

105+
/**
106+
* Returns the number of values represented by this histogram.
107+
* In other words, this is the sum of the counts of all buckets including the zero bucket.
108+
*
109+
* @return the value count, guaranteed to be zero for empty histograms
110+
*/
111+
long valueCount();
112+
105113
/**
106114
* Returns minimum of all values represented by this histogram.
107115
*
@@ -132,6 +140,60 @@ interface Buckets {
132140

133141
}
134142

143+
/**
144+
* Value-based equality for exponential histograms.
145+
* @param a the first histogram (can be null)
146+
* @param b the second histogram (can be null)
147+
* @return true, if both histograms are equal
148+
*/
149+
static boolean equals(ExponentialHistogram a, ExponentialHistogram b) {
150+
if (a == b) return true;
151+
if (a == null) return false;
152+
if (b == null) return false;
153+
154+
return a.scale() == b.scale()
155+
&& a.sum() == b.sum()
156+
&& equalsIncludingNaN(a.min(), b.min())
157+
&& a.zeroBucket().equals(b.zeroBucket())
158+
&& bucketIteratorsEqual(a.negativeBuckets().iterator(), b.negativeBuckets().iterator())
159+
&& bucketIteratorsEqual(a.positiveBuckets().iterator(), b.positiveBuckets().iterator());
160+
}
161+
162+
private static boolean equalsIncludingNaN(double a, double b) {
163+
return (a == b) || (Double.isNaN(a) && Double.isNaN(b));
164+
}
165+
166+
private static boolean bucketIteratorsEqual(BucketIterator a, BucketIterator b) {
167+
if (a.scale() != b.scale()) {
168+
return false;
169+
}
170+
while (a.hasNext() && b.hasNext()) {
171+
if (a.peekIndex() != b.peekIndex() || a.peekCount() != b.peekCount()) {
172+
return false;
173+
}
174+
a.advance();
175+
b.advance();
176+
}
177+
return a.hasNext() == b.hasNext();
178+
}
179+
180+
/**
181+
* Default hash code implementation to be used with {@link #equals(ExponentialHistogram, ExponentialHistogram)}.
182+
* @param histogram the histogram to hash
183+
* @return the hash code
184+
*/
185+
static int hashCode(ExponentialHistogram histogram) {
186+
int hash = histogram.scale();
187+
hash = 31 * hash + Double.hashCode(histogram.sum());
188+
hash = 31 * hash + Long.hashCode(histogram.valueCount());
189+
hash = 31 * hash + Double.hashCode(histogram.min());
190+
hash = 31 * hash + histogram.zeroBucket().hashCode();
191+
// we intentionally don't include the hash of the buckets here, because that is likely expensive to compute
192+
// instead, we assume that the value count and sum are a good enough approximation in most cases to minimize collisions
193+
// the value count is typically available as a cached value and doesn't involve iterating over all buckets
194+
return hash;
195+
}
196+
135197
static ExponentialHistogram empty() {
136198
return EmptyExponentialHistogram.INSTANCE;
137199
}

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

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

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
* Consumers must ensure that if the histogram is mutated, all previously acquired {@link BucketIterator}
3232
* instances are no longer used.
3333
*/
34-
final class FixedCapacityExponentialHistogram implements ReleasableExponentialHistogram {
34+
final class FixedCapacityExponentialHistogram extends AbstractExponentialHistogram implements ReleasableExponentialHistogram {
3535

3636
static final long BASE_SIZE = RamUsageEstimator.shallowSizeOfInstance(FixedCapacityExponentialHistogram.class) + ZeroBucket.SHALLOW_SIZE
3737
+ 2 * Buckets.SHALLOW_SIZE;

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
}

0 commit comments

Comments
 (0)