package org.radargun.stats;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import org.HdrHistogram.AbstractHistogram;
import org.HdrHistogram.HistogramIterationValue;
import org.radargun.config.DefinitionElement;
import org.radargun.config.Init;
import org.radargun.config.Property;
import org.radargun.stats.representation.DefaultOutcome;
import org.radargun.stats.representation.Histogram;
import org.radargun.stats.representation.MeanAndDev;
import org.radargun.stats.representation.OperationThroughput;
import org.radargun.stats.representation.Percentile;
import org.radargun.utils.NanoTimeConverter;
/**
* Keeps several buckets for response time ranges and stores number of requests falling into this range.
* Does not differentiate between successful and error requests.
*
* @author Radim Vansa <rvansa@redhat.com>
*/
@DefinitionElement(name = "histogram", doc = "Stores data required for producing histogram or percentiles.")
public final class HistogramOperationStats implements OperationStats {
@Property(doc = "Maximum value that could be recorded. Default is one hour.", converter = NanoTimeConverter.class)
private long maxValue = TimeUnit.HOURS.toNanos(1);
@Property(doc = "Number of significant digits. Default is 2.")
private int digits = 2;
private AbstractHistogram histogram;
private SoftReference<AbstractHistogram> soft;
private long errors = 0;
private Histogram compacted;
public HistogramOperationStats() {
}
private HistogramOperationStats(AbstractHistogram histogram, long maxValue, int digits) {
this.histogram = histogram;
this.maxValue = maxValue;
this.digits = digits;
}
@Init
public void init() {
if (histogram != null) throw new IllegalStateException("This histogram was already initialized!");
histogram = new org.HdrHistogram.Histogram(maxValue, digits);
}
public void init(long maxValue, int digits) {
this.maxValue = maxValue;
this.digits = digits;
init();
}
@Override
public OperationStats newInstance() {
HistogramOperationStats newInstance = new HistogramOperationStats();
newInstance.maxValue = maxValue;
newInstance.digits = digits;
newInstance.init();
return newInstance;
}
@Override
public OperationStats copy() {
return new HistogramOperationStats(getHistogram().copy(), maxValue, digits);
}
@Override
public void merge(OperationStats other) {
if (other instanceof HistogramOperationStats) {
HistogramOperationStats otherStats = (HistogramOperationStats) other;
histogram = getHistogram();
histogram.add(otherStats.getHistogram());
compact();
} else {
throw new IllegalArgumentException(String.valueOf(other));
}
}
@Override
public void record(Request request) {
histogram.recordValue(request.duration());
if (!request.isSuccessful()) {
errors++;
}
}
@Override
public void record(Message message) {
if (message.isValid()) {
histogram.recordValue(message.totalTime());
} else {
errors++;
}
}
@Override
public void record(RequestSet requestSet) {
histogram.recordValue(requestSet.sumDurations());
if (!requestSet.isSuccessful()) {
errors++;
}
}
@Override
public <T> T getRepresentation(Class<T> clazz, Statistics ownerStatistics, Object... args) {
AbstractHistogram histogram = getHistogram();
if (clazz == DefaultOutcome.class) {
return (T) new DefaultOutcome(histogram.getTotalCount(), errors, histogram.getMean(), histogram.getMaxValue());
} else if (clazz == MeanAndDev.class) {
return (T) new MeanAndDev(histogram.getMean(), histogram.getStdDeviation());
} else if (clazz == OperationThroughput.class) {
return (T) OperationThroughput.compute(histogram.getTotalCount(), errors, ownerStatistics);
} else if (clazz == Percentile.class) {
double percentile = Percentile.getPercentile(args);
return (T) new Percentile(histogram.getValueAtPercentile(percentile));
} else if (clazz == Histogram.class) {
if (args.length == 0) {
return (T) getFullHistogram(histogram);
} else {
return (T) getReformattedHistogram(histogram, args);
}
} else {
return null;
}
}
// this is different from the compacted histogram by having the lower bound as count, and including zero values, too
private Histogram getFullHistogram(AbstractHistogram histogram) {
AbstractHistogram.AllValues values = histogram.allValues();
ArrayList<Long> ranges = new ArrayList<>();
ArrayList<Long> counts = new ArrayList<>();
ranges.add(histogram.getMinValue());
for (HistogramIterationValue value : values) {
ranges.add(value.getValueIteratedTo());
counts.add(value.getCountAddedInThisIterationStep());
}
return new Histogram(ranges.stream().mapToLong(l -> l).toArray(), counts.stream().mapToLong(l -> l).toArray());
}
private Histogram getReformattedHistogram(AbstractHistogram histogram, Object[] args) {
int buckets = Histogram.getBuckets(args);
double percentile = Histogram.getPercentile(args);
AbstractHistogram.AllValues values = histogram.allValues();
ArrayList<Long> ranges = new ArrayList<>();
ArrayList<Long> counts = new ArrayList<>();
long min = Math.max(histogram.getMinValue(), 1);
long max = Math.max(histogram.getValueAtPercentile(percentile), 1);
if (max < min) max = Math.max(histogram.getMaxValue(), min + 1);
double exponent = Math.pow((double) max / (double) min, 1d / buckets);
double current = min * exponent;
long accCount = 0, lastCount = 0;
for (HistogramIterationValue value : values) {
accCount += value.getCountAddedInThisIterationStep();
if (value.getValueIteratedTo() >= current) {
ranges.add(value.getValueIteratedTo());
counts.add(accCount - lastCount);
lastCount = accCount;
current = current * exponent;
}
if (value.getValueIteratedTo() >= max) {
break;
}
}
if (accCount > 0) {
ranges.add(max);
counts.add(accCount - lastCount);
}
return new Histogram(ranges.stream().mapToLong(l -> l).toArray(), counts.stream().mapToLong(l -> l).toArray());
}
@Override
public boolean isEmpty() {
if (compacted != null) {
return compacted.counts.length == 0;
} else if (histogram != null) {
return histogram.getTotalCount() == 0;
} else {
throw new IllegalArgumentException();
}
}
public void compact() {
if (compacted != null) {
// make sure that we always store only the compacted
histogram = null;
return;
}
if (histogram == null) throw new IllegalStateException("Either compacted or expanded form has to be defined!");
AbstractHistogram.AllValues values = histogram.allValues();
ArrayList<Long> ranges = new ArrayList<>();
ArrayList<Long> counts = new ArrayList<>();
for (HistogramIterationValue value : values) {
if (value.getCountAddedInThisIterationStep() > 0) {
ranges.add(value.getValueIteratedTo());
counts.add(value.getCountAddedInThisIterationStep());
}
}
compacted = new Histogram(ranges.stream().mapToLong(l -> l).toArray(), counts.stream().mapToLong(l -> l).toArray());
soft = new SoftReference<>(histogram);
histogram = null;
}
protected AbstractHistogram getHistogram() {
if (histogram != null) {
return histogram;
}
AbstractHistogram hist;
if (soft != null && (hist = soft.get()) != null) {
return hist;
}
hist = new org.HdrHistogram.Histogram(maxValue, digits);
for (int i = 0; i < compacted.ranges.length; ++i) {
hist.recordValueWithCount(compacted.ranges[i], compacted.counts[i]);
}
return hist;
}
private void writeObject(ObjectOutputStream s) throws IOException {
s.writeLong(maxValue);
s.writeInt(digits);
s.writeLong(errors);
compact();
s.writeInt(compacted.ranges.length);
for (int i = 0; i < compacted.ranges.length; ++i) {
s.writeLong(compacted.ranges[i]);
s.writeLong(compacted.counts[i]);
}
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
maxValue = s.readLong();
digits = s.readInt();
errors = s.readLong();
int length = s.readInt();
long[] ranges = new long[length];
long[] counts = new long[length];
for (int i = 0; i < length; ++i) {
ranges[i] = s.readLong();
counts[i] = s.readLong();
}
compacted = new Histogram(ranges, counts);
}
}