/* * Copyright 2009-2017 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.Iterator; import java.util.List; /** * Cumulative statistics for success ratio with higher weight given to recent data. * Clients call {@link #success()} or {@link #failure()} when an event occurs, and the ratio of success to total events * is accumulated. Older values are given exponentially smaller weight, with a decay factor determined by a duration * chosen by the client. The rate measurement weights decay in two dimensions: * <ul> * <li>in time according to the lapse period supplied: <code>weight = exp((t0-t)/T)</code> where <code>t0</code> is the * last measurement time, <code>t</code> is the current time and <code>T</code> is the lapse period)</li> * <li>per measurement according to the lapse window supplied: <code>weight = exp(-i/L)</code> where <code>L</code> is * the lapse window and <code>i</code> is the sequence number of the measurement.</li> * </ul> * 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 * @author Steven Swor * @since 2.0 */ public class ExponentialMovingAverageRatio { private volatile double t0; private volatile long count; private volatile double min = Double.MAX_VALUE; private volatile double max; private final double lapse; private final Deque<Long> times = new ArrayDeque<Long>(); private final Deque<Integer> values = new ArrayDeque<Integer>(); private final int retention; private final int window; private final double factor; /** * @param lapsePeriod the exponential lapse rate for the rate average (in seconds) * @param window the exponential lapse window (number of measurements) */ public ExponentialMovingAverageRatio(double lapsePeriod, int window) { this(lapsePeriod, window, false); } /** * @param lapsePeriod the exponential lapse rate for the rate average (in seconds) * @param window the exponential lapse window (number of measurements) * @param millis when true, analyze the data as milliseconds instead of the native nanoseconds * @since 4.2 */ public ExponentialMovingAverageRatio(double lapsePeriod, int window, boolean millis) { this.lapse = lapsePeriod > 0 ? 0.001 / lapsePeriod : 0; // convert to milliseconds this.window = window; this.retention = window * 5; this.factor = millis ? 1000000 : 1; this.t0 = System.nanoTime() / this.factor; } /** * Add a new event with successful outcome. */ public void success() { append(1, System.nanoTime()); } /** * Add a new event with successful outcome at time t. * @param t the System.nanoTime(). */ public void success(long t) { append(1, t); } /** * Add a new event with failed outcome. */ public void failure() { append(0, System.nanoTime()); } /** * Add a new event with failed outcome at time t. * @param t a new event with failed outcome in milliseconds. */ public void failure(long t) { append(0, t); } public synchronized void reset() { this.t0 = System.nanoTime() / this.factor; this.times.clear(); this.values.clear(); this.count = 0; this.max = 0; this.min = Double.MAX_VALUE; } private synchronized void append(int value, long t) { if (this.times.size() == this.retention) { this.times.poll(); this.values.poll(); } this.times.add(t); this.values.add(value); this.count++; //NOSONAR - false positive, we're synchronized } private Statistics calcStatic() { List<Long> copyTimes; List<Integer> copyValues; long count; synchronized (this) { copyTimes = new ArrayList<Long>(this.times); copyValues = new ArrayList<Integer>(this.values); count = this.count; } ExponentialMovingAverage cumulative = new ExponentialMovingAverage(this.window); double t0 = 0; double sum = 0; double weight = 0; double min = this.min; double max = this.max; int size = copyTimes.size(); Iterator<Integer> values = copyValues.iterator(); for (Long time : copyTimes) { double t = time / this.factor; if (size == 1) { t0 = this.t0; } else if (t0 == 0) { t0 = t; values.next(); continue; } double alpha = Math.exp((t0 - t) * this.lapse); t0 = t; sum = alpha * sum + values.next(); weight = alpha * weight + 1; double value = sum / weight; if (value > max) { max = value; } if (value < min) { min = value; } cumulative.append(value); } synchronized (this) { if (max > this.max) { this.max = max; } if (min < this.min) { this.min = min; } } return new Statistics(count, min < Double.MAX_VALUE ? min : 0, max, cumulative.getMean(), cumulative.getStandardDeviation()); } /** * @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 time in seconds since the last measurement */ public double getTimeSinceLastMeasurement() { double delta = System.nanoTime() - lastTime(); return delta / 1000. / this.factor; } /** * @return the mean success rate */ public double getMean() { if (this.count == 0) { // Optimistic to start: success rate is 100% return 1; } return decayMean(calcStatic()); } /** * Decay the mean using the current time. * @param staticStats the static statistics. * @return the new mean. */ private double decayMean(Statistics statistics) { double t = System.nanoTime() / this.factor; double mean = statistics.getMean(); double alpha = Math.exp((lastTime() / this.factor - t) * this.lapse); return alpha * mean + 1 - alpha; } private synchronized double lastTime() { if (this.times.size() > 0) { return this.times.peekLast(); } else { return this.t0 * this.factor; } } /** * @return the approximate standard deviation of the success rate measurements */ public double getStandardDeviation() { return calcStatic().getStandardDeviation(); } /** * @return the maximum value recorded of the exponential weighted average (per measurement) success rate */ public double getMax() { return calcStatic().getMax(); } /** * @return the minimum value recorded of the exponential weighted average (per measurement) success rate */ public double getMin() { return calcStatic().getMin(); } /** * @return summary statistics (count, mean, standard deviation etc.) */ public Statistics getStatistics() { Statistics staticStats = calcStatic(); staticStats.setMean(decayMean(staticStats)); return staticStats; } @Override public String toString() { return String.format("[%s, timeSinceLast=%f]", getStatistics(), getTimeSinceLastMeasurement()); } }