org.glowroot.shaded.HdrHistogram.ConcurrentHistogram Maven / Gradle / Ivy
/**
* Written by Gil Tene of Azul Systems, and released to the public domain,
* as explained at http://creativecommons.org/publicdomain/zero/1.0/
*
* @author Gil Tene
*/
package org.glowroot.shaded.HdrHistogram;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.ByteBuffer;
import java.nio.LongBuffer;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.zip.DataFormatException;
/**
* An integer values High Dynamic Range (HDR) Histogram that supports safe concurrent recording operations.
* A ConcurrentHistogram guarantees lossless recording of values into the histogram even when the
* histogram is updated by multiple threads, and supports auto-resize and shift operations that may
* result from or occur concurrently with other recording operations.
*
* It is important to note that concurrent recording, auto-sizing, and value shifting are the only thread-safe
* behaviors provided by {@link ConcurrentHistogram}, and that it is not otherwise synchronized. Specifically, {@link
* ConcurrentHistogram} provides no implicit synchronization that would prevent the contents of the histogram
* from changing during queries, iterations, copies, or addition operations on the histogram. Callers wishing to make
* potentially concurrent, multi-threaded updates that would safely work in the presence of queries, copies, or
* additions of histogram objects should either take care to externally synchronize and/or order their access,
* use the {@link SynchronizedHistogram} variant, or (recommended) use {@link Recorder} or
* {@link SingleWriterRecorder} which are intended for this purpose.
*
* Auto-resizing: When constructed with no specified value range range (or when auto-resize is turned on with {@link
* Histogram#setAutoResize}) a {@link Histogram} will auto-resize its dynamic range to include recorded values as
* they are encountered. Note that recording calls that cause auto-resizing may take longer to execute, as resizing
* incurs allocation and copying of internal data structures.
*
* See package description for {@link org.glowroot.shaded.HdrHistogram} for details.
*/
public class ConcurrentHistogram extends Histogram {
static final AtomicLongFieldUpdater totalCountUpdater =
AtomicLongFieldUpdater.newUpdater(ConcurrentHistogram.class, "totalCount");
volatile long totalCount;
volatile AtomicLongArrayWithNormalizingOffset activeCounts;
volatile AtomicLongArrayWithNormalizingOffset inactiveCounts;
WriterReaderPhaser wrp = new WriterReaderPhaser();
@Override
long getCountAtIndex(final int index) {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
long activeCount = activeCounts.get(
normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()));
long inactiveCount = inactiveCounts.get(
normalizeIndex(index, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length()));
return activeCount + inactiveCount;
} finally {
wrp.readerUnlock();
}
}
@Override
long getCountAtNormalizedIndex(final int index) {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
long activeCount = activeCounts.get(index);
long inactiveCount = inactiveCounts.get(index);
return activeCount + inactiveCount;
} finally {
wrp.readerUnlock();
}
}
@Override
void incrementCountAtIndex(final int index) {
long criticalValue = wrp.writerCriticalSectionEnter();
try {
activeCounts.incrementAndGet(
normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()));
} finally {
wrp.writerCriticalSectionExit(criticalValue);
}
}
@Override
void addToCountAtIndex(final int index, final long value) {
long criticalValue = wrp.writerCriticalSectionEnter();
try {
activeCounts.addAndGet(
normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), value);
} finally {
wrp.writerCriticalSectionExit(criticalValue);
}
}
@Override
void setCountAtIndex(int index, long value) {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
activeCounts.lazySet(
normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), value);
inactiveCounts.lazySet(
normalizeIndex(index, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length()), 0);
} finally {
wrp.readerUnlock();
}
}
@Override
void setCountAtNormalizedIndex(int index, long value) {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
inactiveCounts.lazySet(index, value);
activeCounts.lazySet(index, 0);
} finally {
wrp.readerUnlock();
}
}
@Override
int getNormalizingIndexOffset() {
return activeCounts.getNormalizingIndexOffset();
}
@Override
void setNormalizingIndexOffset(int normalizingIndexOffset) {
setNormalizingIndexOffset(normalizingIndexOffset, 0, false);
}
private void setNormalizingIndexOffset(
int normalizingIndexOffset,
int shiftedAmount,
boolean lowestHalfBucketPopulated) {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
if (normalizingIndexOffset == activeCounts.getNormalizingIndexOffset()) {
return; // Nothing to do.
}
// Save and clear the inactive 0 value count:
int zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
long inactiveZeroValueCount = inactiveCounts.get(zeroIndex);
inactiveCounts.lazySet(zeroIndex, 0);
// Change the normalizingIndexOffset on the current inactiveCounts:
inactiveCounts.setNormalizingIndexOffset(normalizingIndexOffset);
// Handle the inactive lowest half bucket:
if ((shiftedAmount > 0) && lowestHalfBucketPopulated) {
shiftLowestInactiveHalfBucketContentsLeft(shiftedAmount);
}
// Restore the inactive 0 value count:
zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
inactiveCounts.lazySet(zeroIndex, inactiveZeroValueCount);
// switch active and inactive:
AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
activeCounts = inactiveCounts;
inactiveCounts = tmp;
wrp.flipPhase();
// Save and clear the newly inactive 0 value count:
zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
inactiveZeroValueCount = inactiveCounts.get(zeroIndex);
inactiveCounts.lazySet(zeroIndex, 0);
// Change the normalizingIndexOffset on the newly inactiveCounts:
inactiveCounts.setNormalizingIndexOffset(normalizingIndexOffset);
// Handle the newly inactive lowest half bucket:
if ((shiftedAmount > 0) && lowestHalfBucketPopulated) {
shiftLowestInactiveHalfBucketContentsLeft(shiftedAmount);
}
// Restore the newly inactive 0 value count:
zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
inactiveCounts.lazySet(zeroIndex, inactiveZeroValueCount);
// switch active and inactive again:
tmp = activeCounts;
activeCounts = inactiveCounts;
inactiveCounts = tmp;
wrp.flipPhase();
// At this point, both active and inactive have normalizingIndexOffset safely set,
// and the switch in each was done without any writers using the wrong value in flight.
} finally {
wrp.readerUnlock();
}
}
private void shiftLowestInactiveHalfBucketContentsLeft(int shiftAmount) {
final int numberOfBinaryOrdersOfMagnitude = shiftAmount >> subBucketHalfCountMagnitude;
// The lowest inactive half-bucket (not including the 0 value) is special: unlike all other half
// buckets, the lowest half bucket values cannot be scaled by simply changing the
// normalizing offset. Instead, they must be individually re-recorded at the new
// scale, and cleared from the current one.
//
// We know that all half buckets "below" the current lowest one are full of 0s, because
// we would have overflowed otherwise. So we need to shift the values in the current
// lowest half bucket into that range (including the current lowest half bucket itself).
// Iterating up from the lowermost non-zero "from slot" and copying values to the newly
// scaled "to slot" (and then zeroing the "from slot"), will work in a single pass,
// because the scale "to slot" index will always be a lower index than its or any
// preceding non-scaled "from slot" index:
//
// (Note that we specifically avoid slot 0, as it is directly handled in the outer case)
for (int fromIndex = 1; fromIndex < subBucketHalfCount; fromIndex++) {
long toValue = valueFromIndex(fromIndex) << numberOfBinaryOrdersOfMagnitude;
int toIndex = countsArrayIndex(toValue);
int normalizedToIndex =
normalizeIndex(toIndex, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
long countAtFromIndex = inactiveCounts.get(fromIndex);
inactiveCounts.lazySet(normalizedToIndex, countAtFromIndex);
inactiveCounts.lazySet(fromIndex, 0);
}
// Note that the above loop only creates O(N) work for histograms that have values in
// the lowest half-bucket (excluding the 0 value). Histograms that never have values
// there (e.g. all integer value histograms used as internal storage in DoubleHistograms)
// will never loop, and their shifts will remain O(1).
}
@Override
void shiftNormalizingIndexByOffset(int offsetToAdd, boolean lowestHalfBucketPopulated) {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
int newNormalizingIndexOffset = getNormalizingIndexOffset() + offsetToAdd;
setNormalizingIndexOffset(newNormalizingIndexOffset, offsetToAdd, lowestHalfBucketPopulated);
} finally {
wrp.readerUnlock();
}
}
@Override
void resize(long newHighestTrackableValue) {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
int newArrayLength = determineArrayLengthNeeded(newHighestTrackableValue);
int countsDelta = newArrayLength - countsArrayLength;
if (countsDelta <= 0) {
// This resize need was already covered by a concurrent resize op.
return;
}
int oldNormalizedZeroIndex =
normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
// Resize the current inactiveCounts:
AtomicLongArray oldInactiveCounts = inactiveCounts;
inactiveCounts =
new AtomicLongArrayWithNormalizingOffset(
newArrayLength,
inactiveCounts.getNormalizingIndexOffset()
);
// Copy inactive contents to newly sized inactiveCounts:
for (int i = 0 ; i < oldInactiveCounts.length(); i++) {
inactiveCounts.lazySet(i, oldInactiveCounts.get(i));
}
if (oldNormalizedZeroIndex != 0) {
// We need to shift the stuff from the zero index and up to the end of the array:
int newNormalizedZeroIndex = oldNormalizedZeroIndex + countsDelta;
int lengthToCopy = (newArrayLength - countsDelta) - oldNormalizedZeroIndex;
int src, dst;
for (src = oldNormalizedZeroIndex, dst = newNormalizedZeroIndex;
src < oldNormalizedZeroIndex + lengthToCopy;
src++, dst++) {
inactiveCounts.lazySet(dst, oldInactiveCounts.get(src));
}
}
// switch active and inactive:
AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
activeCounts = inactiveCounts;
inactiveCounts = tmp;
wrp.flipPhase();
// Resize the newly inactiveCounts:
oldInactiveCounts = inactiveCounts;
inactiveCounts =
new AtomicLongArrayWithNormalizingOffset(
newArrayLength,
inactiveCounts.getNormalizingIndexOffset()
);
// Copy inactive contents to newly sized inactiveCounts:
for (int i = 0 ; i < oldInactiveCounts.length(); i++) {
inactiveCounts.lazySet(i, oldInactiveCounts.get(i));
}
if (oldNormalizedZeroIndex != 0) {
// We need to shift the stuff from the zero index and up to the end of the array:
int newNormalizedZeroIndex = oldNormalizedZeroIndex + countsDelta;
int lengthToCopy = (newArrayLength - countsDelta) - oldNormalizedZeroIndex;
int src, dst;
for (src = oldNormalizedZeroIndex, dst = newNormalizedZeroIndex;
src < oldNormalizedZeroIndex + lengthToCopy;
src++, dst++) {
inactiveCounts.lazySet(dst, oldInactiveCounts.get(src));
}
}
// switch active and inactive again:
tmp = activeCounts;
activeCounts = inactiveCounts;
inactiveCounts = tmp;
wrp.flipPhase();
// At this point, both active and inactive have been safely resized,
// and the switch in each was done without any writers modifying it in flight.
// We resized things. We can now make the historam establish size accordingly for future recordings:
establishSize(newHighestTrackableValue);
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
} finally {
wrp.readerUnlock();
}
}
@Override
public void setAutoResize(boolean autoResize) {
this.autoResize = true;
}
@Override
void clearCounts() {
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
for (int i = 0; i < activeCounts.length(); i++) {
activeCounts.lazySet(i, 0);
inactiveCounts.lazySet(i, 0);
}
totalCountUpdater.set(this, 0);
} finally {
wrp.readerUnlock();
}
}
@Override
public ConcurrentHistogram copy() {
ConcurrentHistogram copy = new ConcurrentHistogram(this);
copy.add(this);
return copy;
}
@Override
public ConcurrentHistogram copyCorrectedForCoordinatedOmission(final long expectedIntervalBetweenValueSamples) {
ConcurrentHistogram toHistogram = new ConcurrentHistogram(this);
toHistogram.addWhileCorrectingForCoordinatedOmission(this, expectedIntervalBetweenValueSamples);
return toHistogram;
}
@Override
public long getTotalCount() {
return totalCountUpdater.get(this);
}
@Override
void setTotalCount(final long totalCount) {
totalCountUpdater.set(this, totalCount);
}
@Override
void incrementTotalCount() {
totalCountUpdater.incrementAndGet(this);
}
@Override
void addToTotalCount(final long value) {
totalCountUpdater.addAndGet(this, value);
}
@Override
int _getEstimatedFootprintInBytes() {
return (512 + (2 * 8 * activeCounts.length()));
}
/**
* Construct an auto-resizing ConcurrentHistogram with a lowest discernible value of 1 and an auto-adjusting
* highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
*
* @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
* decimal digits to which the histogram will maintain value resolution
* and separation. Must be a non-negative integer between 0 and 5.
*/
public ConcurrentHistogram(final int numberOfSignificantValueDigits) {
this(1, 2, numberOfSignificantValueDigits);
setAutoResize(true);
}
/**
* Construct a ConcurrentHistogram given the Highest value to be tracked and a number of significant decimal digits. The
* histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
*
* @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
* integer that is {@literal >=} 2.
* @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
* decimal digits to which the histogram will maintain value resolution
* and separation. Must be a non-negative integer between 0 and 5.
*/
public ConcurrentHistogram(final long highestTrackableValue, final int numberOfSignificantValueDigits) {
this(1, highestTrackableValue, numberOfSignificantValueDigits);
}
/**
* Construct a ConcurrentHistogram given the Lowest and Highest values to be tracked and a number of significant
* decimal digits. Providing a lowestDiscernibleValue is useful is situations where the units used
* for the histogram's values are much smaller that the minimal accuracy required. E.g. when tracking
* time values stated in nanosecond units, where the minimal accuracy required is a microsecond, the
* proper value for lowestDiscernibleValue would be 1000.
*
* @param lowestDiscernibleValue The lowest value that can be tracked (distinguished from 0) by the histogram.
* Must be a positive integer that is {@literal >=} 1. May be internally rounded
* down to nearest power of 2.
* @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
* integer that is {@literal >=} (2 * lowestDiscernibleValue).
* @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
* decimal digits to which the histogram will maintain value resolution
* and separation. Must be a non-negative integer between 0 and 5.
*/
public ConcurrentHistogram(final long lowestDiscernibleValue, final long highestTrackableValue,
final int numberOfSignificantValueDigits) {
super(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits, false);
activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
wordSizeInBytes = 8;
}
/**
* Construct a histogram with the same range settings as a given source histogram,
* duplicating the source's start/end timestamps (but NOT it's contents)
* @param source The source histogram to duplicate
*/
public ConcurrentHistogram(final AbstractHistogram source) {
super(source, false);
activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
wordSizeInBytes = 8;
}
/**
* Construct a new histogram by decoding it from a ByteBuffer.
* @param buffer The buffer to decode from
* @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
* @return The newly constructed histogram
*/
public static ConcurrentHistogram decodeFromByteBuffer(final ByteBuffer buffer,
final long minBarForHighestTrackableValue) {
return (ConcurrentHistogram) decodeFromByteBuffer(buffer, ConcurrentHistogram.class,
minBarForHighestTrackableValue);
}
/**
* Construct a new histogram by decoding it from a compressed form in a ByteBuffer.
* @param buffer The buffer to decode from
* @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
* @return The newly constructed histogram
* @throws java.util.zip.DataFormatException on error parsing/decompressing the buffer
*/
public static ConcurrentHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
final long minBarForHighestTrackableValue) throws DataFormatException {
return (ConcurrentHistogram) decodeFromCompressedByteBuffer(buffer, ConcurrentHistogram.class,
minBarForHighestTrackableValue);
}
private void readObject(final ObjectInputStream o)
throws IOException, ClassNotFoundException {
o.defaultReadObject();
}
@Override
synchronized void fillCountsArrayFromBuffer(final ByteBuffer buffer, final int length) {
LongBuffer logbuffer = buffer.asLongBuffer();
for (int i = 0; i < length; i++) {
inactiveCounts.lazySet(i, logbuffer.get());
activeCounts.lazySet(i, 0);
}
}
// We try to cache the LongBuffer used in output cases, as repeated
// output form the same histogram using the same buffer is likely:
private LongBuffer cachedDstLongBuffer = null;
private ByteBuffer cachedDstByteBuffer = null;
private int cachedDstByteBufferPosition = 0;
@Override
synchronized void fillBufferFromCountsArray(final ByteBuffer buffer, final int length) {
if ((cachedDstLongBuffer == null) ||
(buffer != cachedDstByteBuffer) ||
(buffer.position() != cachedDstByteBufferPosition)) {
cachedDstByteBuffer = buffer;
cachedDstByteBufferPosition = buffer.position();
cachedDstLongBuffer = buffer.asLongBuffer();
}
cachedDstLongBuffer.rewind();
try {
wrp.readerLock();
assert (countsArrayLength == activeCounts.length());
assert (countsArrayLength == inactiveCounts.length());
int zeroIndex = normalizeIndex(0, getNormalizingIndexOffset(), countsArrayLength);
int lengthFromZeroIndexToEnd = Math.min(length, (countsArrayLength - zeroIndex));
int remainingLengthFromNormalizedZeroIndex = length - lengthFromZeroIndexToEnd;
for (int i = 0; i < lengthFromZeroIndexToEnd; i++) {
cachedDstLongBuffer.put(activeCounts.get(zeroIndex + i) + inactiveCounts.get(zeroIndex + i));
}
for (int i = 0; i < remainingLengthFromNormalizedZeroIndex; i++) {
cachedDstLongBuffer.put(activeCounts.get(i) + inactiveCounts.get(i));
}
} finally {
wrp.readerUnlock();
}
}
static class AtomicLongArrayWithNormalizingOffset extends AtomicLongArray {
private int normalizingIndexOffset;
AtomicLongArrayWithNormalizingOffset(int length, int normalizingIndexOffset) {
super(length);
this.normalizingIndexOffset = normalizingIndexOffset;
}
public int getNormalizingIndexOffset() {
return normalizingIndexOffset;
}
public void setNormalizingIndexOffset(int normalizingIndexOffset) {
this.normalizingIndexOffset = normalizingIndexOffset;
}
}
}