/* * -----------------------------------------------------------------------\ * PerfCake *   * Copyright (C) 2010 - 2016 the original author or authors. *   * Licensed 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. * -----------------------------------------------------------------------/ */ package org.perfcake.reporting.reporter; import org.perfcake.common.PeriodType; import org.perfcake.reporting.Measurement; import org.perfcake.reporting.MeasurementUnit; import org.perfcake.reporting.ReportingException; import org.perfcake.reporting.destination.Destination; import org.perfcake.reporting.reporter.accumulator.AvgAccumulator; import org.HdrHistogram.Histogram; import org.HdrHistogram.HistogramIterationValue; import org.HdrHistogram.PercentileIterator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.Locale; /** * <p>Reports response time in milliseconds using <a href="https://github.com/HdrHistogram/HdrHistogram">HDR Histogram</a> that can * computationally correct the Coordinated omission problem.</p> * * <p>The following paragraphs are based on <a href="https://github.com/HdrHistogram/HdrHistogram/blob/master/README.md">the HDR Histogram documentation</a>.</p> * * <p>This reporter depends on the features introduced by HDR Histogram to correct the coordinated omission. * To compensate for the loss of sampled values when a recorded value is larger than the expected, * interval between value samples, HDR Histogram will auto-generate an additional series of decreasingly-smaller value records. * The values go down to the {@link #expectedValue} in case of the {@link Correction#USER} correction mode, or down to the average response time in * case of the {@link Correction#AUTO} correction mode.</p> * * <p>The reporter could be configured to track the counts of observed response times in milliseconds between 0 and 3,600,000 * ({@link #maxExpectedValue}) while maintaining a value precision of 3 ({@link #precision}) significant digits across that range. * Value quantization within the range will thus be no larger than 1/1,000th (or 0.1%) of any value. This example reporter could be used to track * and analyze the counts of observed response times ranging between 1 millisecond and 1 hour in magnitude, while maintaining a value resolution * of 1 millisecond (or better) up to one second, and a resolution of 1 second (or better) up to 1,000 seconds. * At its maximum tracked value (1 hour), it would still maintain a resolution of 3.6 seconds (or better).</p> * * @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a> */ public class ResponseTimeHistogramReporter extends AbstractReporter { /** * The reporter's logger. */ private static final Logger log = LogManager.getLogger(ResponseTimeHistogramReporter.class); /** * Correction mode can be switched off (no correction), automatic or user specified. */ public enum Correction { OFF, AUTO, USER } /** * Precision of the resulting histogram (number of significant digits) in range 0 - 5. * This determines the memory used by the reporter. */ private int precision = 2; /** * The correction of coordinated omission in the resulting histogram. * {@link Correction#AUTO} is the default value and this means that the histogram is corrected * by the average measured value. */ private Correction correctionMode = Correction.AUTO; /** * The value of normal/typical/expected response time in ms to correct the histogram * while the {@link Correction#USER} correction mode is turned on. */ private long expectedValue = 1L; /** * Prefix of the percentile keys in the result map. */ private String prefix = "perc"; /** * Detail level of the result (the number of iteration steps per half-distance to 100%). * Must be greater than 0. */ private int detail = 2; /** * The maximum expected value to better organize the data in the histogram. The response time * reported must never exceed this value, otherwise the result will be skipped, an error reported and the * output will be invalid. -1 turns the optimization off. It is valuable to set some reasonable number like * 3_600_000 which equals to the resolution from 1 millisecond to 1 hour. */ private long maxExpectedValue = -1; /** * When set to true, the results are filter to keep just unique values. */ private boolean filter = false; /** * Accumulator to store average response rate for histogram auto-correction. */ private AvgAccumulator avg = new AvgAccumulator(); /** * Histogram instance to store the data. */ private Histogram histogram; /** * Format of the percentile expression. */ private static final String percentileFormatString = "%2.12f"; @Override protected void doReset() { avg.reset(); initRecorder(); } /** * Initializes a new histogram based on the configuration proeprties. */ private void initRecorder() { if (maxExpectedValue == -1) { histogram = new Histogram(precision); } else { histogram = new Histogram(maxExpectedValue, precision); } } @Override protected void doReport(final MeasurementUnit measurementUnit) throws ReportingException { avg.add(measurementUnit.getTotalTime()); long responseTime = Math.round(measurementUnit.getTotalTime()); if (maxExpectedValue != -1 && responseTime > maxExpectedValue) { log.error(String.format("Reported response time (%d) exceeds maximal trackable value (%d). Ignoring the value. Results are tampered!", responseTime, maxExpectedValue)); } else { histogram.recordValue(responseTime); } } @Override public void publishResult(final PeriodType periodType, final Destination destination) throws ReportingException { final Measurement m = newMeasurement(); publishAccumulatedResult(m); PercentileIterator pi; Histogram localHistogram = histogram.copy(); switch (correctionMode) { case AUTO: pi = new PercentileIterator(localHistogram.copyCorrectedForCoordinatedOmission(Math.round(avg.getResult())), detail); break; case USER: pi = new PercentileIterator(localHistogram.copyCorrectedForCoordinatedOmission(expectedValue), detail); break; default: pi = new PercentileIterator(localHistogram, detail); } String lastKey = null; String lastValue = null; while (pi.hasNext()) { HistogramIterationValue val = pi.next(); String key = prefix + String.format(Locale.US, percentileFormatString, val.getPercentileLevelIteratedTo() / 100d); String value = String.format(Locale.US, "%d", val.getValueIteratedTo()); if (filter) { if (lastValue != null) { if (!value.equals(lastValue)) { m.set(lastKey, lastValue); } else if (!pi.hasNext()) { m.set(key, value); } } lastKey = key; lastValue = value; } else { m.set(key, value); } } destination.report(m); } /** * Gets the precision as the number of significant digits that are recognized by this reporter. * * @return The number of significant digits that are recognized by this reporter. */ public int getPrecision() { return precision; } /** * Sets the precision as the number of significant digits that are recognized by this reporter. * Must be in interval 0 - 5. Default value is 2. * * @param precision * The number of significant digits that are recognized by this reporter. * @return Instance of this to support fluent API. */ public ResponseTimeHistogramReporter setPrecision(final int precision) { if (precision < 0 || precision > 5) { log.warn(String.format("Wrong level of precision set (%d). Keeping the original value (%d).", precision, this.precision)); } else { this.precision = precision; initRecorder(); } return this; } /** * Gets the expectedValue of coordinated omission in the resulting histogram. * {@link Correction#AUTO} is the default value and this means that the histogram is corrected * by the average measured value. In case of the {@link Correction#USER} mode, the user specifies * the expected response time manually for correct computation of the histogram. When the * {@link Correction#OFF} mode is used, no correction is performed. * * @return The expectedValue mode. */ public Correction getCorrectionMode() { return correctionMode; } /** * Sets the expectedValue of coordinated omission in the resulting histogram. * {@link Correction#AUTO} is the default value and this means that the histogram is corrected * by the average measured value. In case of the {@link Correction#USER} mode, the user specifies * the expected response time manually for correct computation of the histogram. When the * {@link Correction#OFF} mode is used, no correction is performed. * * @param correctionMode * The expectedValue mode to be used. * @return Instance of this to support fluent API. */ public ResponseTimeHistogramReporter setCorrectionMode(final Correction correctionMode) { this.correctionMode = correctionMode; return this; } /** * Gets the expectedValue value for the coordinated omission when the {@link Correction#USER} mode is set. * * @return The expectedValue value. */ public long getExpectedValue() { return expectedValue; } /** * Sets the expectedValue value for the coordinated omission when the {@link Correction#USER} mode is set. * * @param expectedValue * The expectedValue value. * @return Instance of this to support fluent API. */ public ResponseTimeHistogramReporter setExpectedValue(final long expectedValue) { this.expectedValue = expectedValue; return this; } /** * Gets the prefix of percentile values in the result map. * * @return The percentile value prefix. */ public String getPrefix() { return prefix; } /** * Sets the prefix of percentile values in the result map. * * @param prefix * The percentile value prefix. * @return Instance of this to support fluent API. */ public ResponseTimeHistogramReporter setPrefix(final String prefix) { this.prefix = prefix; return this; } /** * Gets the detail level of the result (the number of iteration steps per half-distance to 100%). * Must be greater than 0. The default value is 2. * * @return The detail level. */ public int getDetail() { return detail; } /** * Sets the detail level of the result (the number of iteration steps per half-distance to 100%). * Must be greater than 0. The default value is 2. * * @param detail * The detail level. * @return Instance of this to support fluent API. */ public ResponseTimeHistogramReporter setDetail(final int detail) { if (detail < 1) { log.warn(String.format("Wrong level of detail set (%d). Keeping the original value (%d).", detail, this.detail)); } else { this.detail = detail; } return this; } /** * Gets the maximum expected value to better organize the data in the histogram. * * @return The maximal expected value. -1 means that this optimization off. */ public long getMaxExpectedValue() { return maxExpectedValue; } /** * Sets the maximum expected value to better organize the data in the histogram. The response time * reported must never exceed this value, otherwise the result will be skipped, an error reported and the * output will be invalid. -1 turns the optimization off. It is valuable to set some reasonable number like * 3_600_000 which equals to the resolution from 1 millisecond to 1 hour. * * @param maxExpectedValue * The maximal expected value. -1 to turn this optimization off. -1 is the default value. * @return Instance of this to support fluent API. */ public ResponseTimeHistogramReporter setMaxExpectedValue(final long maxExpectedValue) { this.maxExpectedValue = maxExpectedValue; initRecorder(); return this; } /** * Gets the state of results filter. When true, the results with the same value are collapsed. * * @return The state of results filter. When true, the results with the same value are collapsed. */ public boolean isFilter() { return filter; } /** * Sets the state of results filter. When true, the results with the same value are collapsed. * * @param filter * The state of results filter. When true, the results with the same value are collapsed. * @return Instance of this to support fluent API. */ public ResponseTimeHistogramReporter setFilter(final boolean filter) { this.filter = filter; return this; } }