/* * Copyright 2009-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.springframework.integration.support.management; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; /** * Cumulative statistics for a series of real numbers with higher weight given to recent data. * Clients call {@link #append(double)} every time there is a new measurement, and then can collect summary * statistics from the convenience getters (e.g. {@link #getStatistics()}). Older values are given exponentially smaller * weight, with a decay factor determined by a "window" size chosen by the caller. The result is a good approximation to * the statistics of the series but with more weight given to recent measurements, so if the statistics change over time * those trends can be approximately reflected. For performance reasons, the calculation is performed on retrieval, * {@code window * 5} samples are retained meaning that the earliest retained value contributes just 0.5% to the * sum. * * @author Dave Syer * @author Gary Russell * @since 2.0 */ public class ExponentialMovingAverage { private volatile long count; private volatile double min = Double.MAX_VALUE; private volatile double max; private final Deque<Double> samples = new ArrayDeque<Double>(); private final int retention; private final int window; private final double factor; /** * Create a moving average accumulator with decay lapse window provided. Measurements older than this will have * smaller weight than <code>1/e</code>. * @param window the exponential lapse window (number of measurements) */ public ExponentialMovingAverage(int window) { this(window, 1); } /** * Create a moving average accumulator with decay lapse window provided. Measurements older than this will have * smaller weight than <code>1/e</code>. * @param window the exponential lapse window (number of measurements) * @param factor a factor by which raw values are reduced during analysis; e.g. to analyze in ms and * raw values are ns, set the factor to 1000000.0. * @since 4.2 */ public ExponentialMovingAverage(int window, double factor) { this.window = window; this.retention = window * 5; // last retained value contributes just 0.5% to the sum this.factor = factor; } public synchronized void reset() { this.count = 0; this.min = Double.MAX_VALUE; this.max = 0; this.samples.clear(); } /** * Add a new measurement to the series. * @param value the measurement to append */ public synchronized void append(double value) { if (this.samples.size() == this.retention) { this.samples.poll(); } this.samples.add(value); this.count++; //NOSONAR - false positive, we're synchronized } private Statistics calc() { List<Double> copy; long count; synchronized (this) { copy = new ArrayList<Double>(this.samples); count = this.count; } double sum = 0; double decay = 1 - 1. / this.window; double sumSquares = 0; double weight = 0; double min = this.min; double max = this.max; for (Double value : copy) { value /= this.factor; if (value > max) { max = value; } if (value < min) { min = value; } sum = decay * sum + value; sumSquares = decay * sumSquares + value * value; weight = decay * weight + 1; } synchronized (this) { if (max > this.max) { this.max = max; } if (min < this.min) { this.min = min; } } double mean = weight > 0 ? sum / weight : 0.; double var = weight > 0 ? sumSquares / weight - mean * mean : 0.; double standardDeviation = var > 0 ? Math.sqrt(var) : 0; return new Statistics(count, min == Double.MAX_VALUE ? 0 : min, max, mean, standardDeviation); //NOSONAR } /** * @return the number of measurements recorded */ public int getCount() { return (int) this.count; } /** * @return the number of measurements recorded */ public long getCountLong() { return this.count; } /** * @return the mean value */ public double getMean() { return calc().getMean(); } /** * @return the approximate standard deviation */ public double getStandardDeviation() { return calc().getStandardDeviation(); } /** * @return the maximum value recorded (not weighted) */ public double getMax() { return calc().getMax(); } /** * @return the minimum value recorded (not weighted) */ public double getMin() { return calc().getMin(); } /** * @return summary statistics (count, mean, standard deviation etc.) */ public Statistics getStatistics() { return calc(); } @Override public String toString() { return getStatistics().toString(); } }