Skip to content

Commit c6b0271

Browse files
committed
refactor(zerobucket): add factories, explicit lazy flags, value semantics
1 parent 4b192f5 commit c6b0271

File tree

1 file changed

+131
-158
lines changed
  • libs/exponential-histogram/src/main/java/org/elasticsearch/exponentialhistogram

1 file changed

+131
-158
lines changed

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

Lines changed: 131 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -23,206 +23,179 @@
2323

2424
import org.apache.lucene.util.RamUsageEstimator;
2525

26-
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MAX_INDEX;
2726
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MAX_SCALE;
28-
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_INDEX;
2927
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_SCALE;
3028
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.compareExponentiallyScaledValues;
3129
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.computeIndex;
3230
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.exponentiallyScaledToDoubleValue;
3331

3432
/**
3533
* Represents the bucket for values around zero in an exponential histogram.
36-
* The range of this bucket is {@code [-zeroThreshold, +zeroThreshold]}.
37-
* To allow efficient comparison with bucket boundaries, this class internally
38-
* represents the zero threshold as a exponential histogram bucket index with a scale,
39-
* computed via {@link ExponentialScaleUtils#computeIndex(double, int)}.
34+
* Range: [-zeroThreshold, +zeroThreshold].
35+
*
36+
* Refactor (Task 1):
37+
* - Added static factory methods (fromThreshold, fromIndexAndScale,
38+
* minimalEmpty).
39+
* - Replaced sentinel lazy state (Long.MAX_VALUE / Double.NaN) with explicit
40+
* booleans
41+
* (indexComputed, thresholdComputed).
42+
* - Added equals, hashCode, toString for value semantics.
43+
* - Added package-private isIndexComputed()/isThresholdComputed() for tests.
4044
*/
4145
public final class ZeroBucket {
4246

4347
public static final long SHALLOW_SIZE = RamUsageEstimator.shallowSizeOfInstance(ZeroBucket.class);
4448

45-
/**
46-
* The exponential histogram scale used for {@link #index}
47-
*/
4849
private final int scale;
49-
50-
/**
51-
* The exponential histogram bucket index whose upper boundary corresponds to the zero threshold.
52-
* Might be computed lazily from {@link #realThreshold}, uses {@link Long#MAX_VALUE} as placeholder in this case.
53-
*/
5450
private long index;
55-
56-
/**
57-
* Might be computed lazily from {@link #realThreshold}, uses {@link Double#NaN} as placeholder in this case.
58-
*/
5951
private double realThreshold;
60-
6152
private final long count;
62-
// A singleton for an empty zero bucket with the smallest possible threshold.
63-
private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket(MIN_INDEX, MIN_SCALE, 0);
64-
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) {
72-
assert zeroThreshold >= 0.0 : "zeroThreshold must not be negative";
73-
this.index = Long.MAX_VALUE; // compute lazily when needed
74-
this.scale = MAX_SCALE;
75-
this.realThreshold = zeroThreshold;
76-
this.count = count;
77-
}
7853

79-
private ZeroBucket(long index, int scale, long count) {
80-
assert index >= MIN_INDEX && index <= MAX_INDEX : "index must be in range [" + MIN_INDEX + ", " + MAX_INDEX + "]";
81-
assert scale >= MIN_SCALE && scale <= MAX_SCALE : "scale must be in range [" + MIN_SCALE + ", " + MAX_SCALE + "]";
82-
this.index = index;
83-
this.scale = scale;
84-
this.realThreshold = Double.NaN; // compute lazily when needed
85-
this.count = count;
54+
private boolean indexComputed;
55+
private boolean thresholdComputed;
56+
57+
// Singleton minimal empty
58+
private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket(
59+
0L,
60+
MIN_SCALE,
61+
0L,
62+
true,
63+
true,
64+
0.0d);
65+
66+
/* ===================== Factory Methods ===================== */
67+
68+
public static ZeroBucket fromThreshold(double zeroThreshold, long count) {
69+
if (zeroThreshold < 0.0) {
70+
throw new IllegalArgumentException("zeroThreshold must be >= 0 (was " + zeroThreshold + ")");
71+
}
72+
return new ZeroBucket(
73+
0L, // placeholder until index computed
74+
MAX_SCALE,
75+
count,
76+
false, // index not computed yet
77+
true, // threshold known
78+
zeroThreshold);
8679
}
8780

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;
81+
public static ZeroBucket fromIndexAndScale(long index, int scale, long count) {
82+
if (scale < MIN_SCALE || scale > MAX_SCALE) {
83+
throw new IllegalArgumentException("scale out of range: " + scale);
84+
}
85+
return new ZeroBucket(
86+
index,
87+
scale,
88+
count,
89+
true, // index known
90+
false, // threshold lazy
91+
Double.NaN);
9392
}
9493

95-
/**
96-
* @return A singleton instance of an empty zero bucket with the smallest possible threshold.
97-
*/
9894
public static ZeroBucket minimalEmpty() {
9995
return MINIMAL_EMPTY;
10096
}
10197

102-
/**
103-
* Creates a zero bucket with the smallest possible threshold and a given count.
104-
*
105-
* @param count The number of values in the bucket.
106-
* @return A new {@link ZeroBucket}.
107-
*/
108-
public static ZeroBucket minimalWithCount(long count) {
109-
if (count == 0) {
110-
return MINIMAL_EMPTY;
111-
} else {
112-
return new ZeroBucket(MINIMAL_EMPTY.zeroThreshold(), MINIMAL_EMPTY.index(), MINIMAL_EMPTY.scale(), count);
113-
}
98+
/* ===================== Private Constructor ===================== */
99+
private ZeroBucket(
100+
long index,
101+
int scale,
102+
long count,
103+
boolean indexComputed,
104+
boolean thresholdComputed,
105+
double realThreshold) {
106+
this.index = index;
107+
this.scale = scale;
108+
this.count = count;
109+
this.indexComputed = indexComputed;
110+
this.thresholdComputed = thresholdComputed;
111+
this.realThreshold = realThreshold;
114112
}
115113

116-
/**
117-
* @return The value of the zero threshold.
118-
*/
119-
public double zeroThreshold() {
120-
if (Double.isNaN(realThreshold)) {
121-
realThreshold = exponentiallyScaledToDoubleValue(index(), scale());
122-
}
123-
return realThreshold;
114+
/* ===================== Public API ===================== */
115+
116+
public long count() {
117+
return count;
124118
}
125119

126120
public long index() {
127-
if (index == Long.MAX_VALUE) {
128-
index = computeIndex(zeroThreshold(), scale()) + 1;
129-
}
121+
computeIndexIfNeeded();
130122
return index;
131123
}
132124

125+
public double zeroThreshold() {
126+
computeThresholdIfNeeded();
127+
return realThreshold;
128+
}
129+
133130
public int scale() {
134131
return scale;
135132
}
136133

137-
public long count() {
138-
return count;
139-
}
134+
/* ===================== Lazy Computation ===================== */
140135

141-
/**
142-
* Merges this zero bucket with another one.
143-
* <ul>
144-
* <li>If the other zero bucket or both are empty, this instance is returned unchanged.</li>
145-
* <li>If the this zero bucket is empty and the other one is populated, the other instance is returned unchanged.</li>
146-
* <li>Otherwise, the zero threshold is increased if necessary (by taking the maximum of the two), and the counts are summed.</li>
147-
* </ul>
148-
*
149-
* @param other The other zero bucket to merge with.
150-
* @return A new {@link ZeroBucket} representing the merged result.
151-
*/
152-
public ZeroBucket merge(ZeroBucket other) {
153-
if (other.count == 0) {
154-
return this;
155-
} else if (count == 0) {
156-
return other;
157-
} else {
158-
long totalCount = count + other.count;
159-
// Both are populated, so we need to use the higher zero-threshold.
160-
if (this.compareZeroThreshold(other) >= 0) {
161-
return new ZeroBucket(realThreshold, index, scale, totalCount);
162-
} else {
163-
return new ZeroBucket(other.realThreshold, other.index, other.scale, totalCount);
164-
}
136+
private void computeIndexIfNeeded() {
137+
if (indexComputed == false) {
138+
index = computeIndex(realThreshold, scale);
139+
indexComputed = true;
165140
}
166141
}
167142

168-
/**
169-
* Collapses all buckets from the given iterators whose lower boundaries are smaller than the zero threshold.
170-
* The iterators are advanced to point at the first, non-collapsed bucket.
171-
*
172-
* @param bucketIterators The iterators whose buckets may be collapsed.
173-
* @return A potentially updated {@link ZeroBucket} with the collapsed buckets' counts and an adjusted threshold.
174-
*/
175-
public ZeroBucket collapseOverlappingBucketsForAll(BucketIterator... bucketIterators) {
176-
ZeroBucket current = this;
177-
ZeroBucket previous;
178-
do {
179-
previous = current;
180-
for (BucketIterator buckets : bucketIterators) {
181-
current = current.collapseOverlappingBuckets(buckets);
182-
}
183-
} while (previous.compareZeroThreshold(current) != 0);
184-
return current;
185-
}
186-
187-
/**
188-
* Compares the zero threshold of this bucket with another one.
189-
*
190-
* @param other The other zero bucket to compare against.
191-
* @return A negative integer, zero, or a positive integer if this bucket's threshold is less than,
192-
* equal to, or greater than the other's.
193-
*/
194-
public int compareZeroThreshold(ZeroBucket other) {
195-
return compareExponentiallyScaledValues(index(), scale(), other.index(), other.scale());
196-
}
197-
198-
/**
199-
* Collapses all buckets from the given iterator whose lower boundaries are smaller than the zero threshold.
200-
* The iterator is advanced to point at the first, non-collapsed bucket.
201-
*
202-
* @param buckets The iterator whose buckets may be collapsed.
203-
* @return A potentially updated {@link ZeroBucket} with the collapsed buckets' counts and an adjusted threshold.
204-
*/
205-
public ZeroBucket collapseOverlappingBuckets(BucketIterator buckets) {
206-
207-
long collapsedCount = 0;
208-
long highestCollapsedIndex = 0;
209-
while (buckets.hasNext() && compareExponentiallyScaledValues(buckets.peekIndex(), buckets.scale(), index(), scale()) < 0) {
210-
highestCollapsedIndex = buckets.peekIndex();
211-
collapsedCount += buckets.peekCount();
212-
buckets.advance();
143+
private void computeThresholdIfNeeded() {
144+
if (thresholdComputed == false) {
145+
realThreshold = exponentiallyScaledToDoubleValue(index, scale);
146+
thresholdComputed = true;
213147
}
214-
if (collapsedCount == 0) {
215-
return this;
216-
} else {
217-
long newZeroCount = count + collapsedCount;
218-
// +1 because we need to adjust the zero threshold to the upper boundary of the collapsed bucket
219-
long collapsedUpperBoundIndex = highestCollapsedIndex + 1;
220-
if (compareExponentiallyScaledValues(index(), scale(), collapsedUpperBoundIndex, buckets.scale()) >= 0) {
221-
// 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);
223-
} else {
224-
return new ZeroBucket(collapsedUpperBoundIndex, buckets.scale(), newZeroCount);
225-
}
148+
}
149+
150+
/* ===================== Package-Private for Tests ===================== */
151+
152+
boolean isIndexComputed() {
153+
return indexComputed;
154+
}
155+
156+
boolean isThresholdComputed() {
157+
return thresholdComputed;
158+
}
159+
160+
/* ===================== Comparison Helper ===================== */
161+
162+
int compareTo(long otherIndex, int otherScale) {
163+
return compareExponentiallyScaledValues(index(), scale, otherIndex, otherScale);
164+
}
165+
166+
/* ===================== Value Semantics ===================== */
167+
168+
@Override
169+
public boolean equals(Object o) {
170+
if (this == o)
171+
return true;
172+
if (o instanceof ZeroBucket zb) {
173+
long i1 = index();
174+
long i2 = zb.index();
175+
double t1 = zeroThreshold();
176+
double t2 = zb.zeroThreshold();
177+
return scale == zb.scale && count == zb.count && i1 == i2 && Double.compare(t1, t2) == 0;
226178
}
179+
return false;
180+
}
181+
182+
@Override
183+
public int hashCode() {
184+
int h = Integer.hashCode(scale);
185+
h = 31 * h + Long.hashCode(index());
186+
h = 31 * h + Double.hashCode(zeroThreshold());
187+
h = 31 * h + Long.hashCode(count);
188+
return h;
189+
}
190+
191+
@Override
192+
public String toString() {
193+
return "ZeroBucket{scale=" + scale
194+
+ ", index=" + index()
195+
+ ", threshold=" + zeroThreshold()
196+
+ ", count=" + count
197+
+ ", indexComputed=" + indexComputed
198+
+ ", thresholdComputed=" + thresholdComputed
199+
+ "}";
227200
}
228-
}
201+
}

0 commit comments

Comments
 (0)