Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V., and/or licensed to Elasticsearch B.V.
* under one or more license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* This file is based on a modification of https://github.com/open-telemetry/opentelemetry-java which is licensed under the Apache 2.0 License.
*/

package org.elasticsearch.exponentialhistogram;

/**
* Basic implementation for {@link ExponentialHistogram} with common functionality.
*/
public abstract class AbstractExponentialHistogram implements ExponentialHistogram {

@Override
public long valueCount() {
return zeroBucket().count() + negativeBuckets().valueCount() + positiveBuckets().valueCount();
}

@Override
public int hashCode() {
return ExponentialHistogram.hashCode(this);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof ExponentialHistogram) {
return ExponentialHistogram.equals(this, (ExponentialHistogram) obj);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import java.util.OptionalLong;

class EmptyExponentialHistogram implements ReleasableExponentialHistogram {
class EmptyExponentialHistogram extends AbstractExponentialHistogram implements ReleasableExponentialHistogram {

static final EmptyExponentialHistogram INSTANCE = new EmptyExponentialHistogram();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ public interface ExponentialHistogram extends Accountable {
*/
double sum();

/**
* Returns the number of values represented by this histogram.
* In other words, this is the sum of the counts of all buckets including the zero bucket.
*
* @return the value count, guaranteed to be zero for empty histograms
*/
long valueCount();

/**
* Returns minimum of all values represented by this histogram.
*
Expand Down Expand Up @@ -132,6 +140,60 @@ interface Buckets {

}

/**
* Value-based equality for exponential histograms.
* @param a the first histogram (can be null)
* @param b the second histogram (can be null)
* @return true, if both histograms are equal
*/
static boolean equals(ExponentialHistogram a, ExponentialHistogram b) {
if (a == b) return true;
if (a == null) return false;
if (b == null) return false;

return a.scale() == b.scale()
&& a.sum() == b.sum()
&& equalsIncludingNaN(a.min(), b.min())
&& a.zeroBucket().equals(b.zeroBucket())
&& bucketIteratorsEqual(a.negativeBuckets().iterator(), b.negativeBuckets().iterator())
&& bucketIteratorsEqual(a.positiveBuckets().iterator(), b.positiveBuckets().iterator());
}

private static boolean equalsIncludingNaN(double a, double b) {
return (a == b) || (Double.isNaN(a) && Double.isNaN(b));
}

private static boolean bucketIteratorsEqual(BucketIterator a, BucketIterator b) {
if (a.scale() != b.scale()) {
return false;
}
while (a.hasNext() && b.hasNext()) {
if (a.peekIndex() != b.peekIndex() || a.peekCount() != b.peekCount()) {
return false;
}
a.advance();
b.advance();
}
return a.hasNext() == b.hasNext();
}

/**
* Default hash code implementation to be used with {@link #equals(ExponentialHistogram, ExponentialHistogram)}.
* @param histogram the histogram to hash
* @return the hash code
*/
static int hashCode(ExponentialHistogram histogram) {
int hash = histogram.scale();
hash = 31 * hash + Double.hashCode(histogram.sum());
hash = 31 * hash + Long.hashCode(histogram.valueCount());
hash = 31 * hash + Double.hashCode(histogram.min());
hash = 31 * hash + histogram.zeroBucket().hashCode();
// we intentionally don't include the hash of the buckets here, because that is likely expensive to compute
// instead, we assume that the value count and sum are a good enough approximation in most cases to minimize collisions
// the value count is typically available as a cached value and doesn't involve iterating over all buckets
return hash;
}

static ExponentialHistogram empty() {
return EmptyExponentialHistogram.INSTANCE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,16 @@ public static int getMaximumScaleIncrease(long index) {
return Long.numberOfLeadingZeros(index) - (64 - MAX_INDEX_BITS);
}

/**
* Returns a scale at to which the given index can be scaled down without changing the exponentially scaled number it represents.
* @param index the index of the number
* @param scale the current scale of the number
* @return the new scale
*/
static int normalizeScale(long index, int scale) {
return Math.max(MIN_SCALE, scale - Long.numberOfTrailingZeros(index));
}

/**
* Returns the upper boundary of the bucket with the given index and scale.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* Consumers must ensure that if the histogram is mutated, all previously acquired {@link BucketIterator}
* instances are no longer used.
*/
final class FixedCapacityExponentialHistogram implements ReleasableExponentialHistogram {
final class FixedCapacityExponentialHistogram extends AbstractExponentialHistogram implements ReleasableExponentialHistogram {

static final long BASE_SIZE = RamUsageEstimator.shallowSizeOfInstance(FixedCapacityExponentialHistogram.class) + ZeroBucket.SHALLOW_SIZE
+ 2 * Buckets.SHALLOW_SIZE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MAX_SCALE;
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_INDEX;
import static org.elasticsearch.exponentialhistogram.ExponentialHistogram.MIN_SCALE;
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.adjustScale;
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.compareExponentiallyScaledValues;
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.computeIndex;
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.exponentiallyScaledToDoubleValue;
import static org.elasticsearch.exponentialhistogram.ExponentialScaleUtils.normalizeScale;

/**
* Represents the bucket for values around zero in an exponential histogram.
Expand Down Expand Up @@ -62,13 +64,7 @@ public final class ZeroBucket {
// A singleton for an empty zero bucket with the smallest possible threshold.
private static final ZeroBucket MINIMAL_EMPTY = new ZeroBucket(MIN_INDEX, MIN_SCALE, 0);

/**
* Creates a new zero bucket with a specific threshold and count.
*
* @param zeroThreshold The threshold defining the bucket's range [-zeroThreshold, +zeroThreshold].
* @param count The number of values in the bucket.
*/
public ZeroBucket(double zeroThreshold, long count) {
private ZeroBucket(double zeroThreshold, long count) {
assert zeroThreshold >= 0.0 : "zeroThreshold must not be negative";
this.index = Long.MAX_VALUE; // compute lazily when needed
this.scale = MAX_SCALE;
Expand All @@ -85,11 +81,11 @@ private ZeroBucket(long index, int scale, long count) {
this.count = count;
}

private ZeroBucket(double realThreshold, long index, int scale, long count) {
this.realThreshold = realThreshold;
this.index = index;
this.scale = scale;
this.count = count;
private ZeroBucket(ZeroBucket toCopy, long newCount) {
this.realThreshold = toCopy.realThreshold;
this.index = toCopy.index;
this.scale = toCopy.scale;
this.count = newCount;
}

/**
Expand All @@ -109,8 +105,37 @@ public static ZeroBucket minimalWithCount(long count) {
if (count == 0) {
return MINIMAL_EMPTY;
} else {
return new ZeroBucket(MINIMAL_EMPTY.zeroThreshold(), MINIMAL_EMPTY.index(), MINIMAL_EMPTY.scale(), count);
return new ZeroBucket(MINIMAL_EMPTY, count);
}
}

/**
* Creates a zero bucket from the given threshold represented as double.
*
* @param zeroThreshold the zero threshold defining the bucket range [-zeroThreshold, +zeroThreshold], must be non-negative
* @param count the number of values in the bucket
* @return the new {@link ZeroBucket}
*/
public static ZeroBucket create(double zeroThreshold, long count) {
if (zeroThreshold == 0) {
return minimalWithCount(count);
}
return new ZeroBucket(zeroThreshold, count);
}

/**
* Creates a zero bucket from the given threshold represented as exponentially scaled number.
*
* @param index the index of the exponentially scaled number defining the zero threshold
* @param scale the corresponding scale for the index
* @param count the number of values in the bucket
* @return the new {@link ZeroBucket}
*/
public static ZeroBucket create(long index, int scale, long count) {
if (index == MINIMAL_EMPTY.index && scale == MINIMAL_EMPTY.scale) {
return minimalWithCount(count);
}
return new ZeroBucket(index, scale, count);
}

/**
Expand Down Expand Up @@ -158,9 +183,9 @@ public ZeroBucket merge(ZeroBucket other) {
long totalCount = count + other.count;
// Both are populated, so we need to use the higher zero-threshold.
if (this.compareZeroThreshold(other) >= 0) {
return new ZeroBucket(realThreshold, index, scale, totalCount);
return new ZeroBucket(this, totalCount);
} else {
return new ZeroBucket(other.realThreshold, other.index, other.scale, totalCount);
return new ZeroBucket(other, totalCount);
}
}
}
Expand Down Expand Up @@ -219,10 +244,33 @@ public ZeroBucket collapseOverlappingBuckets(BucketIterator buckets) {
long collapsedUpperBoundIndex = highestCollapsedIndex + 1;
if (compareExponentiallyScaledValues(index(), scale(), collapsedUpperBoundIndex, buckets.scale()) >= 0) {
// Our current zero-threshold is larger than the upper boundary of the largest collapsed bucket, so we keep it.
return new ZeroBucket(realThreshold, index, scale, newZeroCount);
return new ZeroBucket(this, newZeroCount);
} else {
return new ZeroBucket(collapsedUpperBoundIndex, buckets.scale(), newZeroCount);
}
}
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ZeroBucket that = (ZeroBucket) o;
if (count() != that.count()) return false;
if (Double.compare(zeroThreshold(), that.zeroThreshold()) != 0) return false;
if (compareExponentiallyScaledValues(index(), scale(), that.index(), that.scale()) != 0) return false;
return true;
}

@Override
public int hashCode() {
int normalizedScale = normalizeScale(index(), scale);
int scaleAdjustment = normalizedScale - scale;
long normalizedIndex = adjustScale(index(), scale, scaleAdjustment);

int result = normalizedScale;
result = 31 * result + Long.hashCode(normalizedIndex);
result = 31 * result + Double.hashCode(zeroThreshold());
result = 31 * result + Long.hashCode(count);
return result;
}
}
Loading