package org.radargun.stats;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.radargun.Operation;
import org.radargun.config.DefinitionElement;
import org.radargun.config.Property;
import org.radargun.stats.representation.AbstractSeries;
import org.radargun.utils.TimeConverter;
import org.radargun.utils.TimeService;
/**
* Keeps a series of {@link Statistics} instances and records the requests according to current timestamp.
*
* Useful when the request response time is expected to change during the test execution.
*
* @author Radim Vansa <rvansa@redhat.com>
*/
@DefinitionElement(name = "periodic", doc = "Periodically switches the statistics where the operation is recorded.")
public class PeriodicStatistics extends IntervalStatistics {
@Property(name = "implementation", doc = "Operation statistics prototype. Default is BasicStatistics.", complexConverter = Statistics.Converter.class)
private Statistics prototype = new BasicStatistics();
@Property(doc = "Duration of one sample.", optional = false, converter = TimeConverter.class)
private long period;
private final List<Statistics> buckets;
private long beginNanos = Long.MAX_VALUE;
public PeriodicStatistics() {
this.buckets = new ArrayList<>();
}
PeriodicStatistics(Statistics prototype, long period) {
this();
this.prototype = prototype;
this.period = period;
}
public PeriodicStatistics(PeriodicStatistics other) {
super(other);
this.prototype = other.prototype;
this.period = other.period;
this.buckets = new ArrayList<>(other.buckets.size());
for (Statistics s : other.buckets) {
this.buckets.add(s.copy());
}
}
public void setPeriod(long period) {
this.period = period;
}
private Statistics getCurrentBucket(long millis) {
int bucket = (int) (millis / period);
while (buckets.size() <= bucket) {
Statistics bucketStats = prototype.copy();
if (bucketStats instanceof IntervalStatistics) {
IntervalStatistics intervalStats = (IntervalStatistics) bucketStats;
intervalStats.setBegin(getBegin() + bucket * period);
intervalStats.setEnd(getBegin() + (bucket + 1) * period);
}
buckets.add(bucketStats);
}
return buckets.get(bucket);
}
@Override
public void registerOperationsGroup(String name, Set<Operation> operations) {
prototype.registerOperationsGroup(name, operations);
}
@Override
public void record(Request request, Operation operation) {
getCurrentBucket(TimeUnit.NANOSECONDS.toMillis(request.getRequestStartTime() - beginNanos)).record(request, operation);
}
@Override
public void record(Message message, Operation operation) {
getCurrentBucket(message.getSendStartTime() - getBegin()).record(message, operation);
}
@Override
public void record(RequestSet requestSet, Operation operation) {
getCurrentBucket(TimeUnit.NANOSECONDS.toMillis(requestSet.getBegin() - beginNanos)).record(requestSet, operation);
}
@Override
public Statistics newInstance() {
return new PeriodicStatistics(prototype, period);
}
@Override
public void reset() {
buckets.clear();
}
@Override
public void begin() {
super.begin();
beginNanos = TimeService.nanoTime();
}
@Override
public void end() {
super.end();
// Discard last bucket if it contains < 5% of the period, as those data are usually
// just some leftovers that screw the charts
int numBuckets = buckets.size();
if (numBuckets == 0) return;
Statistics lastStats = buckets.get(numBuckets - 1);
if (lastStats instanceof IntervalStatistics) {
IntervalStatistics intervalStats = (IntervalStatistics) lastStats;
if (getEnd() - intervalStats.getBegin() < period / 20) {
buckets.remove(numBuckets - 1);
}
}
}
@Override
public Statistics copy() {
return new PeriodicStatistics(this);
}
@Override
public void merge(Statistics otherStats) {
if (!(otherStats instanceof PeriodicStatistics)) {
throw new IllegalArgumentException(String.valueOf(otherStats));
}
PeriodicStatistics other = (PeriodicStatistics) otherStats;
if (other.period != period) {
throw new IllegalArgumentException("Different periods: " + period + " vs. " + other.period);
}
if (getBegin() > getEnd()) {
throw new IllegalArgumentException("This stats don't have begin/end set correctly: " + this);
}
if (other.getBegin() > other.getEnd()) {
throw new IllegalArgumentException("Other stats don't have begin/end set correctly: " + other);
}
long distance = Math.abs(other.getBegin() - getBegin());
int offset = (int) (distance / period);
if (2 * (distance - offset * period) > period) {
++offset;
}
if (other.getBegin() < getBegin() && offset > 0) {
buckets.addAll(0, other.buckets.subList(0, offset).stream()
.map(Statistics::copy).collect(Collectors.toList()));
for (int i = offset; i < other.buckets.size(); ++i) {
if (i < buckets.size()) {
buckets.get(i).merge(other.buckets.get(i));
} else {
buckets.add(other.buckets.get(i).copy());
}
}
} else {
for (int i = 0; i < other.buckets.size(); ++i) {
if (i + offset < buckets.size()) {
buckets.get(i + offset).merge(other.buckets.get(i));
} else {
buckets.add(other.buckets.get(i).copy());
}
}
}
// update beginTime/endTime after buckets
super.merge(otherStats);
}
@Override
public Set<String> getOperations() {
return buckets.stream().flatMap(s -> s.getOperations().stream()).collect(Collectors.toSet());
}
@Override
public List<Map<String, OperationStats>> getOperationsStats() {
return prototype.getOperationsStats();
}
@Override
public OperationStats getOperationStats(String operation) {
return prototype.getOperationStats(operation);
}
@Override
public String getOperationsGroup(Operation operation) {
return prototype.getOperationsGroup(operation);
}
@Override
public Map<String, Set<Operation>> getGroupOperationsMap() {
return prototype.getGroupOperationsMap();
}
@Override
public List<Map<String, OperationStats>> getOperationStatsForGroups() {
return prototype.getOperationStatsForGroups();
}
@Override
public <T> T getRepresentation(String operation, Class<T> clazz, Object... args) {
if (AbstractSeries.class.isAssignableFrom(clazz)) {
return (T) getRepresentationSeries(operation, (Class<? extends AbstractSeries>) clazz, args);
} else {
return null;
}
}
private <T extends AbstractSeries<R>, R> T getRepresentationSeries(String operation, Class<T> clazz, Object[] args) {
try {
// force the class being initialized, otherwise it wouldn't be registered
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException("No kidding!", e);
}
Class<R> representationClass = AbstractSeries.representation(clazz);
if (representationClass == null) {
throw new IllegalStateException(clazz.getName());
}
// haven't found a better API for this
R[] data = (R[]) Array.newInstance(representationClass, buckets.size());
for (int i = 0; i < buckets.size(); ++i) {
data[i] = buckets.get(i).getRepresentation(operation, representationClass, args);
}
Constructor<T> seriesCtor;
try {
seriesCtor = clazz.getConstructor(long.class, long.class, data.getClass());
} catch (NoSuchMethodException e) {
throw new IllegalStateException(clazz.getName() + " does not have long, long, " + data.getClass().getName() + " constructor", e);
}
try {
return seriesCtor.newInstance(getBegin(), period, data);
} catch (Exception e) {
throw new IllegalStateException("Cannot instantiate series " + clazz.getName(), e);
}
}
}