21
21
22
22
package org .elasticsearch .exponentialhistogram ;
23
23
24
+ import org .elasticsearch .core .Releasable ;
24
25
import org .elasticsearch .core .Releasables ;
25
26
26
27
import java .util .TreeMap ;
27
28
28
29
/**
29
30
* A builder for building a {@link ReleasableExponentialHistogram} directly from buckets.
30
- * Note that this class is not optimized regarding memory allocations or performance, so it is not intended for high-throughput usage.
31
31
*/
32
- public class ExponentialHistogramBuilder {
32
+ public class ExponentialHistogramBuilder implements Releasable {
33
+
34
+ private static final int DEFAULT_ESTIMATED_BUCKET_COUNT = 32 ;
33
35
34
36
private final ExponentialHistogramCircuitBreaker breaker ;
35
37
@@ -39,8 +41,16 @@ public class ExponentialHistogramBuilder {
39
41
private Double min ;
40
42
private Double max ;
41
43
42
- private final TreeMap <Long , Long > negativeBuckets = new TreeMap <>();
43
- private final TreeMap <Long , Long > positiveBuckets = new TreeMap <>();
44
+ private int estimatedBucketCount = DEFAULT_ESTIMATED_BUCKET_COUNT ;
45
+
46
+ // If the buckets are provided in order, we directly build the histogram to avoid unnecessary copies and allocations
47
+ // If a bucket is received out of order, we fallback to storing the buckets in the TreeMaps and build the histogram at the end.
48
+ private FixedCapacityExponentialHistogram result ;
49
+ // Visible for testing to ensure that the low-allocation path is taken for ordered buckets
50
+ TreeMap <Long , Long > negativeBuckets ;
51
+ TreeMap <Long , Long > positiveBuckets ;
52
+
53
+ private boolean resultAlreadyReturned = false ;
44
54
45
55
ExponentialHistogramBuilder (int scale , ExponentialHistogramCircuitBreaker breaker ) {
46
56
this .breaker = breaker ;
@@ -53,6 +63,7 @@ public class ExponentialHistogramBuilder {
53
63
sum (toCopy .sum ());
54
64
min (toCopy .min ());
55
65
max (toCopy .max ());
66
+ estimatedBucketCount (toCopy .negativeBuckets ().bucketCount () + toCopy .positiveBuckets ().bucketCount ());
56
67
BucketIterator negBuckets = toCopy .negativeBuckets ().iterator ();
57
68
while (negBuckets .hasNext ()) {
58
69
setNegativeBucket (negBuckets .peekIndex (), negBuckets .peekCount ());
@@ -65,6 +76,19 @@ public class ExponentialHistogramBuilder {
65
76
}
66
77
}
67
78
79
+ /**
80
+ * If known, sets the estimated total number of buckets to minimize unnecessary allocations.
81
+ * Only has an effect if invoked before the first call to
82
+ * {@link #setPositiveBucket(long, long)} and {@link #setNegativeBucket(long, long)}.
83
+ *
84
+ * @param totalBuckets the total number of buckets expected to be added
85
+ * @return the builder
86
+ */
87
+ public ExponentialHistogramBuilder estimatedBucketCount (int totalBuckets ) {
88
+ estimatedBucketCount = totalBuckets ;
89
+ return this ;
90
+ }
91
+
68
92
public ExponentialHistogramBuilder scale (int scale ) {
69
93
this .scale = scale ;
70
94
return this ;
@@ -106,69 +130,160 @@ public ExponentialHistogramBuilder max(double max) {
106
130
}
107
131
108
132
/**
109
- * Sets the given bucket of the positive buckets.
110
- * Buckets may be set in arbitrary order. If the bucket already exists, it will be replaced.
133
+ * Sets the given bucket of the positive buckets. If the bucket already exists, it will be replaced.
134
+ * Buckets may be set in arbitrary order. However, for best performance and minimal allocations,
135
+ * buckets should be set in order of increasing index and all negative buckets should be set before positive buckets.
111
136
*
112
137
* @param index the index of the bucket
113
138
* @param count the count of the bucket, must be at least 1
114
139
* @return the builder
115
140
*/
116
141
public ExponentialHistogramBuilder setPositiveBucket (long index , long count ) {
117
- if (count < 1 ) {
118
- throw new IllegalArgumentException ("Bucket count must be at least 1" );
119
- }
120
- positiveBuckets .put (index , count );
142
+ setBucket (index , count , true );
121
143
return this ;
122
144
}
123
145
124
146
/**
125
- * Sets the given bucket of the negative buckets.
126
- * Buckets may be set in arbitrary order. If the bucket already exists, it will be replaced.
147
+ * Sets the given bucket of the negative buckets. If the bucket already exists, it will be replaced.
148
+ * Buckets may be set in arbitrary order. However, for best performance and minimal allocations,
149
+ * buckets should be set in order of increasing index and all negative buckets should be set before positive buckets.
127
150
*
128
151
* @param index the index of the bucket
129
152
* @param count the count of the bucket, must be at least 1
130
153
* @return the builder
131
154
*/
132
155
public ExponentialHistogramBuilder setNegativeBucket (long index , long count ) {
156
+ setBucket (index , count , false );
157
+ return this ;
158
+ }
159
+
160
+ private void setBucket (long index , long count , boolean isPositive ) {
133
161
if (count < 1 ) {
134
162
throw new IllegalArgumentException ("Bucket count must be at least 1" );
135
163
}
136
- negativeBuckets .put (index , count );
137
- return this ;
164
+ if (negativeBuckets == null && positiveBuckets == null ) {
165
+ // so far, all received buckets were in order, try to directly build the result
166
+ if (result == null ) {
167
+ // Initialize the result buffer if required
168
+ reallocateResultWithCapacity (estimatedBucketCount , false );
169
+ }
170
+ if ((isPositive && result .wasLastAddedBucketPositive () == false )
171
+ || (isPositive == result .wasLastAddedBucketPositive () && index > result .getLastAddedBucketIndex ())) {
172
+ // the new bucket is in order too, we can directly add the bucket
173
+ addBucketToResult (index , count , isPositive );
174
+ return ;
175
+ }
176
+ }
177
+ // fallback to TreeMap if a bucket is received out of order
178
+ initializeBucketTreeMapsIfNeeded ();
179
+ if (isPositive ) {
180
+ positiveBuckets .put (index , count );
181
+ } else {
182
+ negativeBuckets .put (index , count );
183
+ }
184
+ }
185
+
186
+ private void initializeBucketTreeMapsIfNeeded () {
187
+ if (negativeBuckets == null ) {
188
+ negativeBuckets = new TreeMap <>();
189
+ positiveBuckets = new TreeMap <>();
190
+ // copy existing buckets to the maps
191
+ if (result != null ) {
192
+ BucketIterator it = result .negativeBuckets ().iterator ();
193
+ while (it .hasNext ()) {
194
+ negativeBuckets .put (it .peekIndex (), it .peekCount ());
195
+ it .advance ();
196
+ }
197
+ it = result .positiveBuckets ().iterator ();
198
+ while (it .hasNext ()) {
199
+ positiveBuckets .put (it .peekIndex (), it .peekCount ());
200
+ it .advance ();
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ private void addBucketToResult (long index , long count , boolean isPositive ) {
207
+ if (resultAlreadyReturned ) {
208
+ // we cannot modify the result anymore, create a new one
209
+ reallocateResultWithCapacity (result .getCapacity (), true );
210
+ }
211
+ assert resultAlreadyReturned == false ;
212
+ boolean sufficientCapacity = result .tryAddBucket (index , count , isPositive );
213
+ if (sufficientCapacity == false ) {
214
+ int newCapacity = Math .max (result .getCapacity () * 2 , DEFAULT_ESTIMATED_BUCKET_COUNT );
215
+ reallocateResultWithCapacity (newCapacity , true );
216
+ boolean bucketAdded = result .tryAddBucket (index , count , isPositive );
217
+ assert bucketAdded : "Output histogram should have enough capacity" ;
218
+ }
219
+ }
220
+
221
+ private void reallocateResultWithCapacity (int newCapacity , boolean copyBucketsFromPreviousResult ) {
222
+ FixedCapacityExponentialHistogram newResult = FixedCapacityExponentialHistogram .create (newCapacity , breaker );
223
+ if (copyBucketsFromPreviousResult && result != null ) {
224
+ BucketIterator it = result .negativeBuckets ().iterator ();
225
+ while (it .hasNext ()) {
226
+ boolean added = newResult .tryAddBucket (it .peekIndex (), it .peekCount (), false );
227
+ assert added : "Output histogram should have enough capacity" ;
228
+ it .advance ();
229
+ }
230
+ it = result .positiveBuckets ().iterator ();
231
+ while (it .hasNext ()) {
232
+ boolean added = newResult .tryAddBucket (it .peekIndex (), it .peekCount (), true );
233
+ assert added : "Output histogram should have enough capacity" ;
234
+ it .advance ();
235
+ }
236
+ }
237
+ if (result != null && resultAlreadyReturned == false ) {
238
+ Releasables .close (result );
239
+ }
240
+ resultAlreadyReturned = false ;
241
+ result = newResult ;
138
242
}
139
243
140
244
public ReleasableExponentialHistogram build () {
141
- FixedCapacityExponentialHistogram result = FixedCapacityExponentialHistogram .create (
142
- negativeBuckets .size () + positiveBuckets .size (),
143
- breaker
144
- );
145
- boolean success = false ;
146
- try {
245
+ if (resultAlreadyReturned ) {
246
+ // result was already returned on a previous call, return a new instance
247
+ reallocateResultWithCapacity (result .getCapacity (), true );
248
+ }
249
+ assert resultAlreadyReturned == false ;
250
+ if (negativeBuckets != null ) {
251
+ // copy buckets from tree maps into result
252
+ reallocateResultWithCapacity (negativeBuckets .size () + positiveBuckets .size (), false );
147
253
result .resetBuckets (scale );
148
- result .setZeroBucket (zeroBucket );
149
254
negativeBuckets .forEach ((index , count ) -> result .tryAddBucket (index , count , false ));
150
255
positiveBuckets .forEach ((index , count ) -> result .tryAddBucket (index , count , true ));
151
-
152
- double sumVal = (sum != null )
153
- ? sum
154
- : ExponentialHistogramUtils .estimateSum (result .negativeBuckets ().iterator (), result .positiveBuckets ().iterator ());
155
- double minVal = (min != null )
156
- ? min
157
- : ExponentialHistogramUtils .estimateMin (zeroBucket , result .negativeBuckets (), result .positiveBuckets ()).orElse (Double .NaN );
158
- double maxVal = (max != null )
159
- ? max
160
- : ExponentialHistogramUtils .estimateMax (zeroBucket , result .negativeBuckets (), result .positiveBuckets ()).orElse (Double .NaN );
161
-
162
- result .setMin (minVal );
163
- result .setMax (maxVal );
164
- result .setSum (sumVal );
165
-
166
- success = true ;
167
- } finally {
168
- if (success == false ) {
169
- Releasables .close (result );
256
+ } else {
257
+ if (result == null ) {
258
+ // no buckets were added
259
+ reallocateResultWithCapacity (0 , false );
170
260
}
261
+ result .setScale (scale );
171
262
}
263
+
264
+ result .setZeroBucket (zeroBucket );
265
+ double sumVal = (sum != null )
266
+ ? sum
267
+ : ExponentialHistogramUtils .estimateSum (result .negativeBuckets ().iterator (), result .positiveBuckets ().iterator ());
268
+ double minVal = (min != null )
269
+ ? min
270
+ : ExponentialHistogramUtils .estimateMin (zeroBucket , result .negativeBuckets (), result .positiveBuckets ()).orElse (Double .NaN );
271
+ double maxVal = (max != null )
272
+ ? max
273
+ : ExponentialHistogramUtils .estimateMax (zeroBucket , result .negativeBuckets (), result .positiveBuckets ()).orElse (Double .NaN );
274
+
275
+ result .setMin (minVal );
276
+ result .setMax (maxVal );
277
+ result .setSum (sumVal );
278
+
279
+ resultAlreadyReturned = true ;
172
280
return result ;
173
281
}
282
+
283
+ @ Override
284
+ public void close () {
285
+ if (resultAlreadyReturned == false ) {
286
+ Releasables .close (result );
287
+ }
288
+ }
174
289
}
0 commit comments