-
Notifications
You must be signed in to change notification settings - Fork 25.2k
Generalise exponential bucket histogram, add percentile calculation #127597
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9918299
b1d8df4
dfef676
2ceb965
dbed27f
c04f2ea
c95f625
f850bc9
1b450f0
e7f5bb6
d269195
80b8b3f
9811299
598d0a8
4153d27
d4b0818
3e3d9fc
be4b96c
8a313ab
8fa7dc6
2c36b97
420739e
3208423
9dd0457
516a4a9
d6e44ed
dfbd9ac
2b294bd
9e227ed
15acd80
368a66a
bb84ed9
273fc23
778e8b5
876ae78
51191eb
f96674c
31f2083
4b73745
5d2cbad
221e079
7e6a6ce
8ef9745
91261d6
a3707eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the "Elastic License | ||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
* Public License v 1"; you may not use this file except in compliance with, at | ||
* your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
* License v3.0 only", or the "Server Side Public License, v 1". | ||
*/ | ||
|
||
package org.elasticsearch.common.metrics; | ||
|
||
import java.util.Arrays; | ||
import java.util.concurrent.atomic.LongAdder; | ||
|
||
/** | ||
* A histogram with a fixed number of buckets of exponentially increasing width. | ||
* <p> | ||
* The bucket boundaries are defined by increasing powers of two, e.g. | ||
* <code> | ||
* (-∞, 1), [1, 2), [2, 4), [4, 8), ..., [2^({@link #bucketCount}-2), ∞) | ||
* </code> | ||
* There are {@link #bucketCount} buckets. | ||
*/ | ||
public class ExponentialBucketHistogram { | ||
|
||
private final int bucketCount; | ||
private final long lastBucketLowerBound; | ||
|
||
public static int[] getBucketUpperBounds(int bucketCount) { | ||
int[] bounds = new int[bucketCount - 1]; | ||
for (int i = 0; i < bounds.length; i++) { | ||
bounds[i] = 1 << i; | ||
} | ||
return bounds; | ||
} | ||
|
||
private int getBucket(long observedValue) { | ||
if (observedValue <= 0) { | ||
return 0; | ||
} else if (lastBucketLowerBound <= observedValue) { | ||
return bucketCount - 1; | ||
} else { | ||
return Long.SIZE - Long.numberOfLeadingZeros(observedValue); | ||
} | ||
} | ||
|
||
private final LongAdder[] buckets; | ||
|
||
public ExponentialBucketHistogram(int bucketCount) { | ||
if (bucketCount < 2 || bucketCount > Integer.SIZE) { | ||
throw new IllegalArgumentException("Bucket count must be in [2, " + Integer.SIZE + "], got " + bucketCount); | ||
} | ||
this.bucketCount = bucketCount; | ||
this.lastBucketLowerBound = getBucketUpperBounds(bucketCount)[bucketCount - 2]; | ||
buckets = new LongAdder[bucketCount]; | ||
for (int i = 0; i < bucketCount; i++) { | ||
buckets[i] = new LongAdder(); | ||
} | ||
} | ||
|
||
public int[] calculateBucketUpperBounds() { | ||
return getBucketUpperBounds(bucketCount); | ||
} | ||
|
||
public void addObservation(long observedValue) { | ||
buckets[getBucket(observedValue)].increment(); | ||
} | ||
|
||
/** | ||
* @return An array of frequencies of handling times in buckets with upper bounds as returned by {@link #calculateBucketUpperBounds()}, | ||
* plus an extra bucket for handling times longer than the longest upper bound. | ||
*/ | ||
public long[] getSnapshot() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed this from |
||
final long[] histogram = new long[bucketCount]; | ||
for (int i = 0; i < bucketCount; i++) { | ||
histogram[i] = buckets[i].longValue(); | ||
} | ||
return histogram; | ||
} | ||
|
||
/** | ||
* Calculate the Nth percentile value | ||
* | ||
* @param percentile The percentile as a fraction (in [0, 1.0]) | ||
* @return A value greater than the specified fraction of values in the histogram | ||
* @throws IllegalArgumentException if the requested percentile is invalid | ||
*/ | ||
public long getPercentile(float percentile) { | ||
return getPercentile(percentile, getSnapshot(), calculateBucketUpperBounds()); | ||
} | ||
|
||
/** | ||
* Calculate the Nth percentile value | ||
* | ||
* @param percentile The percentile as a fraction (in [0, 1.0]) | ||
* @param snapshot An array of frequencies of handling times in buckets with upper bounds as per {@link #calculateBucketUpperBounds()} | ||
* @param bucketUpperBounds The upper bounds of the buckets in the histogram, as per {@link #calculateBucketUpperBounds()} | ||
* @return A value greater than the specified fraction of values in the histogram | ||
* @throws IllegalArgumentException if the requested percentile is invalid | ||
*/ | ||
public long getPercentile(float percentile, long[] snapshot, int[] bucketUpperBounds) { | ||
assert snapshot.length == bucketCount && bucketUpperBounds.length == bucketCount - 1; | ||
if (percentile < 0 || percentile > 1) { | ||
throw new IllegalArgumentException("Requested percentile must be in [0, 1.0], percentile=" + percentile); | ||
} | ||
Comment on lines
+103
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be good to add a few quick There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 221e079 |
||
final long totalCount = Arrays.stream(snapshot).sum(); | ||
long percentileIndex = (long) Math.ceil(totalCount * percentile); | ||
// Find which bucket has the Nth percentile value and return the upper bound value. | ||
for (int i = 0; i < bucketCount; i++) { | ||
percentileIndex -= snapshot[i]; | ||
if (percentileIndex <= 0) { | ||
if (i == snapshot.length - 1) { | ||
return Long.MAX_VALUE; | ||
} else { | ||
return bucketUpperBounds[i]; | ||
} | ||
} | ||
} | ||
assert false : "We shouldn't ever get here"; | ||
return Long.MAX_VALUE; | ||
} | ||
|
||
/** | ||
* Clear all values in the histogram (non-atomic) | ||
*/ | ||
public void clear() { | ||
for (int i = 0; i < bucketCount; i++) { | ||
buckets[i].reset(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,58 +9,20 @@ | |
|
||
package org.elasticsearch.common.network; | ||
|
||
import java.util.concurrent.atomic.LongAdder; | ||
import org.elasticsearch.common.metrics.ExponentialBucketHistogram; | ||
|
||
/** | ||
* Tracks how long message handling takes on a transport thread as a histogram with fixed buckets. | ||
*/ | ||
public class HandlingTimeTracker { | ||
public class HandlingTimeTracker extends ExponentialBucketHistogram { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to keep the
|
||
|
||
public static int[] getBucketUpperBounds() { | ||
int[] bounds = new int[17]; | ||
for (int i = 0; i < bounds.length; i++) { | ||
bounds[i] = 1 << i; | ||
} | ||
return bounds; | ||
} | ||
public static final int BUCKET_COUNT = 18; | ||
|
||
private static int getBucket(long handlingTimeMillis) { | ||
if (handlingTimeMillis <= 0) { | ||
return 0; | ||
} else if (LAST_BUCKET_LOWER_BOUND <= handlingTimeMillis) { | ||
return BUCKET_COUNT - 1; | ||
} else { | ||
return Long.SIZE - Long.numberOfLeadingZeros(handlingTimeMillis); | ||
} | ||
public static int[] getBucketUpperBounds() { | ||
return ExponentialBucketHistogram.getBucketUpperBounds(BUCKET_COUNT); | ||
} | ||
|
||
public static final int BUCKET_COUNT = getBucketUpperBounds().length + 1; | ||
|
||
private static final long LAST_BUCKET_LOWER_BOUND = getBucketUpperBounds()[BUCKET_COUNT - 2]; | ||
|
||
private final LongAdder[] buckets; | ||
|
||
public HandlingTimeTracker() { | ||
buckets = new LongAdder[BUCKET_COUNT]; | ||
for (int i = 0; i < BUCKET_COUNT; i++) { | ||
buckets[i] = new LongAdder(); | ||
} | ||
super(BUCKET_COUNT); | ||
} | ||
|
||
public void addHandlingTime(long handlingTimeMillis) { | ||
buckets[getBucket(handlingTimeMillis)].increment(); | ||
} | ||
|
||
/** | ||
* @return An array of frequencies of handling times in buckets with upper bounds as returned by {@link #getBucketUpperBounds()}, plus | ||
* an extra bucket for handling times longer than the longest upper bound. | ||
*/ | ||
public long[] getHistogram() { | ||
final long[] histogram = new long[BUCKET_COUNT]; | ||
for (int i = 0; i < BUCKET_COUNT; i++) { | ||
histogram[i] = buckets[i].longValue(); | ||
} | ||
return histogram; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The observations are
long
but the bucket bounds are integers, I suppose because the scale of the numbers we've been putting in here are easily contained in the integer range. Perhaps we could revisit this if we need histograms to hold larger values, but that's not the case for the queue latency metric.