Skip to content

Commit 847663e

Browse files
committed
refactor(zerobucket): explicit lazy flags, factories, value semantics, retain original API
1 parent e3a7990 commit 847663e

File tree

2 files changed

+205
-43
lines changed

2 files changed

+205
-43
lines changed

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

Lines changed: 143 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,26 @@
2323

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

26+
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MAX_INDEX;
2627
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MAX_SCALE;
28+
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_INDEX;
2729
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_SCALE;
2830
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.compareExponentiallyScaledValues;
2931
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.computeIndex;
3032
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.exponentiallyScaledToDoubleValue;
3133

3234
/**
3335
* Represents the bucket for values around zero in an exponential histogram.
34-
* Range: [-zeroThreshold, +zeroThreshold].
36+
* The range of this bucket is {@code [-zeroThreshold, +zeroThreshold]}.
3537
*
3638
* 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.
39+
* - Added static factories (fromThreshold, fromIndexAndScale) while keeping
40+
* original public constructor
41+
* - Introduced explicit lazy flags (indexComputed, thresholdComputed) instead
42+
* of sentinels
43+
* - Added value semantics (equals, hashCode, toString)
44+
* - Preserved original API: minimalEmpty, minimalWithCount, merge,
45+
* collapseOverlappingBuckets[ForAll], compareZeroThreshold
4446
*/
4547
public final class ZeroBucket {
4648

@@ -51,51 +53,102 @@ public final class ZeroBucket {
5153
private double realThreshold;
5254
private final long count;
5355

56+
// Explicit lazy flags
5457
private boolean indexComputed;
5558
private boolean thresholdComputed;
5659

57-
// Singleton minimal empty
60+
// Original minimal empty singleton (index known, threshold lazy)
5861
private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket(
59-
0L,
62+
MIN_INDEX,
6063
MIN_SCALE,
6164
0L,
6265
true,
63-
true,
64-
0.0d);
66+
false,
67+
Double.NaN);
68+
69+
/*
70+
* ===================== Original Public Constructor (kept)
71+
* =====================
72+
*/
73+
74+
/**
75+
* Original public constructor (threshold authoritative). Kept for source
76+
* compatibility.
77+
* Deprecated in favor of {@link #fromThreshold(double, long)}.
78+
*/
79+
@Deprecated
80+
public ZeroBucket(double zeroThreshold, long count) {
81+
if (zeroThreshold < 0.0) {
82+
throw new IllegalArgumentException("zeroThreshold must be >= 0 (was " + zeroThreshold + ")");
83+
}
84+
this.scale = MAX_SCALE;
85+
this.count = count;
86+
this.realThreshold = zeroThreshold;
87+
this.index = 0L; // placeholder until computed
88+
this.indexComputed = false;
89+
this.thresholdComputed = true;
90+
}
6591

6692
/* ===================== Factory Methods ===================== */
6793

94+
/**
95+
* Create a ZeroBucket from an explicit threshold (index lazy).
96+
*/
6897
public static ZeroBucket fromThreshold(double zeroThreshold, long count) {
6998
if (zeroThreshold < 0.0) {
7099
throw new IllegalArgumentException("zeroThreshold must be >= 0 (was " + zeroThreshold + ")");
71100
}
72101
return new ZeroBucket(
73-
0L, // placeholder until index computed
102+
0L,
74103
MAX_SCALE,
75104
count,
76-
false, // index not computed yet
77-
true, // threshold known
105+
false,
106+
true,
78107
zeroThreshold);
79108
}
80109

110+
/**
111+
* Create a ZeroBucket from an index + scale (threshold lazy).
112+
*/
81113
public static ZeroBucket fromIndexAndScale(long index, int scale, long count) {
82114
if (scale < MIN_SCALE || scale > MAX_SCALE) {
83115
throw new IllegalArgumentException("scale out of range: " + scale);
84116
}
117+
if (index < MIN_INDEX || index > MAX_INDEX) {
118+
throw new IllegalArgumentException("index out of range: " + index);
119+
}
85120
return new ZeroBucket(
86121
index,
87122
scale,
88123
count,
89-
true, // index known
90-
false, // threshold lazy
124+
true,
125+
false,
91126
Double.NaN);
92127
}
93128

129+
/**
130+
* @return singleton empty minimal bucket.
131+
*/
94132
public static ZeroBucket minimalEmpty() {
95133
return MINIMAL_EMPTY;
96134
}
97135

98-
/* ===================== Private Constructor ===================== */
136+
/**
137+
* Creates a zero bucket with the smallest possible threshold and a given count.
138+
* If count == 0 returns the singleton.
139+
*/
140+
public static ZeroBucket minimalWithCount(long count) {
141+
if (count == 0) {
142+
return MINIMAL_EMPTY;
143+
}
144+
// Resolve lazy threshold & index of singleton
145+
double threshold = MINIMAL_EMPTY.zeroThreshold();
146+
long idx = MINIMAL_EMPTY.index();
147+
return resolved(threshold, idx, MINIMAL_EMPTY.scale(), count);
148+
}
149+
150+
/* ===================== Private Constructors ===================== */
151+
99152
private ZeroBucket(
100153
long index,
101154
int scale,
@@ -111,44 +164,54 @@ private ZeroBucket(
111164
this.realThreshold = realThreshold;
112165
}
113166

114-
/* ===================== Public API ===================== */
167+
private static ZeroBucket resolved(double threshold, long index, int scale, long count) {
168+
return new ZeroBucket(index, scale, count, true, true, threshold);
169+
}
170+
171+
/* ===================== Accessors ===================== */
115172

116173
public long count() {
117174
return count;
118175
}
119176

177+
public int scale() {
178+
return scale;
179+
}
180+
181+
/**
182+
* Returns index; if threshold authoritative, compute with +1 rule (matches
183+
* original code).
184+
*/
120185
public long index() {
121186
computeIndexIfNeeded();
122187
return index;
123188
}
124189

190+
/**
191+
* Returns threshold; if index authoritative compute it lazily.
192+
*/
125193
public double zeroThreshold() {
126194
computeThresholdIfNeeded();
127195
return realThreshold;
128196
}
129197

130-
public int scale() {
131-
return scale;
132-
}
133-
134-
/* ===================== Lazy Computation ===================== */
198+
/* ===================== Lazy Computation Helpers ===================== */
135199

136200
private void computeIndexIfNeeded() {
137201
if (indexComputed == false) {
138-
index = computeIndex(realThreshold, scale);
202+
index = computeIndex(realThreshold, scale) + 1;
139203
indexComputed = true;
140204
}
141205
}
142206

143207
private void computeThresholdIfNeeded() {
144208
if (thresholdComputed == false) {
145-
realThreshold = exponentiallyScaledToDoubleValue(index, scale);
209+
realThreshold = exponentiallyScaledToDoubleValue(index(), scale);
146210
thresholdComputed = true;
147211
}
148212
}
149213

150-
/* ===================== Package-Private for Tests ===================== */
151-
214+
/* Package-private for tests */
152215
boolean isIndexComputed() {
153216
return indexComputed;
154217
}
@@ -157,10 +220,59 @@ boolean isThresholdComputed() {
157220
return thresholdComputed;
158221
}
159222

160-
/* ===================== Comparison Helper ===================== */
223+
/* ===================== Original Functional API ===================== */
161224

162-
int compareTo(long otherIndex, int otherScale) {
163-
return compareExponentiallyScaledValues(index(), scale, otherIndex, otherScale);
225+
public int compareZeroThreshold(ZeroBucket other) {
226+
return compareExponentiallyScaledValues(index(), scale(), other.index(), other.scale());
227+
}
228+
229+
public ZeroBucket merge(ZeroBucket other) {
230+
if (other.count == 0) {
231+
return this;
232+
} else if (this.count == 0) {
233+
return other;
234+
} else {
235+
long total = this.count + other.count;
236+
if (this.compareZeroThreshold(other) >= 0) {
237+
return resolved(this.zeroThreshold(), this.index(), this.scale(), total);
238+
} else {
239+
return resolved(other.zeroThreshold(), other.index(), other.scale(), total);
240+
}
241+
}
242+
}
243+
244+
public ZeroBucket collapseOverlappingBucketsForAll(BucketIterator... bucketIterators) {
245+
ZeroBucket current = this;
246+
ZeroBucket previous;
247+
do {
248+
previous = current;
249+
for (BucketIterator b : bucketIterators) {
250+
current = current.collapseOverlappingBuckets(b);
251+
}
252+
} while (previous.compareZeroThreshold(current) != 0);
253+
return current;
254+
}
255+
256+
public ZeroBucket collapseOverlappingBuckets(BucketIterator buckets) {
257+
long collapsedCount = 0;
258+
long highestCollapsedIndex = 0;
259+
while (buckets.hasNext()
260+
&& compareExponentiallyScaledValues(buckets.peekIndex(), buckets.scale(), index(), scale()) < 0) {
261+
highestCollapsedIndex = buckets.peekIndex();
262+
collapsedCount += buckets.peekCount();
263+
buckets.advance();
264+
}
265+
if (collapsedCount == 0) {
266+
return this;
267+
} else {
268+
long newZeroCount = count + collapsedCount;
269+
long collapsedUpperBoundIndex = highestCollapsedIndex + 1;
270+
if (compareExponentiallyScaledValues(index(), scale(), collapsedUpperBoundIndex, buckets.scale()) >= 0) {
271+
return resolved(this.zeroThreshold(), this.index(), this.scale(), newZeroCount);
272+
} else {
273+
return fromIndexAndScale(collapsedUpperBoundIndex, buckets.scale(), newZeroCount);
274+
}
275+
}
164276
}
165277

166278
/* ===================== Value Semantics ===================== */

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

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void testFromThresholdLazyIndex() {
3333
assertTrue(z.isThresholdComputed());
3434
assertFalse(z.isIndexComputed());
3535
assertEquals(1.25d, z.zeroThreshold(), 0.0);
36-
z.index(); // compute index
36+
z.index();
3737
assertTrue(z.isIndexComputed());
3838
}
3939

@@ -47,30 +47,80 @@ public void testFromIndexLazyThreshold() {
4747
assertTrue(z.isThresholdComputed());
4848
}
4949

50+
@Test
51+
public void testMinimalEmptySingleton() {
52+
ZeroBucket m1 = ZeroBucket.minimalEmpty();
53+
ZeroBucket m2 = ZeroBucket.minimalEmpty();
54+
assertSame(m1, m2);
55+
assertEquals(0L, m1.count());
56+
double threshold = m1.zeroThreshold();
57+
assertTrue("threshold should be non-negative", threshold >= 0.0);
58+
}
59+
60+
@Test
61+
public void testMinimalWithCount() {
62+
ZeroBucket m = ZeroBucket.minimalWithCount(5L);
63+
assertEquals(5L, m.count());
64+
assertTrue(m.zeroThreshold() >= 0.0);
65+
}
66+
67+
@Test
68+
public void testMergeKeepsLargerThreshold() {
69+
ZeroBucket a = ZeroBucket.fromThreshold(0.5d, 4L);
70+
ZeroBucket b = ZeroBucket.fromThreshold(1.0d, 6L);
71+
ZeroBucket merged = a.merge(b);
72+
assertEquals(10L, merged.count());
73+
assertEquals(b.zeroThreshold(), merged.zeroThreshold(), 0.0);
74+
}
75+
76+
@Test(expected = IllegalArgumentException.class)
77+
public void testInvalidNegativeThreshold() {
78+
ZeroBucket.fromThreshold(-0.01d, 1L);
79+
}
80+
5081
@Test
5182
public void testEqualityAndHashStable() {
5283
ZeroBucket a = ZeroBucket.fromThreshold(0.6d, 10L);
5384
ZeroBucket b = ZeroBucket.fromThreshold(0.6d, 10L);
5485
assertEquals(a, b);
5586
assertEquals(a.hashCode(), b.hashCode());
56-
a.index(); // force index
87+
a.index();
5788
assertEquals(a, b);
5889
b.index();
5990
assertEquals(a.hashCode(), b.hashCode());
6091
}
6192

6293
@Test
63-
public void testMinimalEmptySingleton() {
64-
ZeroBucket m1 = ZeroBucket.minimalEmpty();
65-
ZeroBucket m2 = ZeroBucket.minimalEmpty();
66-
assertSame(m1, m2);
67-
assertEquals(0L, m1.count());
68-
assertEquals(0.0, m1.zeroThreshold(), 0.0);
69-
}
94+
public void testCollapseNoOverlapReturnsSame() {
95+
ZeroBucket z = ZeroBucket.fromThreshold(0.5d, 2L);
96+
// Empty iterator scenario -> remains same
97+
BucketIterator empty = new BucketIterator() {
98+
@Override
99+
public void advance() {
100+
}
70101

71-
@Test(expected = IllegalArgumentException.class)
72-
public void testInvalidNegativeThreshold() {
73-
ZeroBucket.fromThreshold(-0.01d, 1L);
102+
@Override
103+
public boolean hasNext() {
104+
return false;
105+
}
106+
107+
@Override
108+
public long peekCount() {
109+
throw new IllegalStateException();
110+
}
111+
112+
@Override
113+
public long peekIndex() {
114+
throw new IllegalStateException();
115+
}
116+
117+
@Override
118+
public int scale() {
119+
return ExponentialHistogram.MAX_SCALE;
120+
}
121+
};
122+
ZeroBucket result = z.collapseOverlappingBuckets(empty);
123+
assertSame(z, result);
74124
}
75125

76126
@Test

0 commit comments

Comments
 (0)