/* PercentileTracker.java
*
* Copyright 2009-2015 Comcast Interactive Media, LLC.
*
* 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.fishwife.jrugged.examples;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* This class implements a way to get percentiles from a list of samples within
* a given time range using the algorithm described at
* <a href="http://cnx.org/content/m10805/latest/">http://cnx.org/content/m10805/latest/</a>.
* The percentile does not sample itself; it merely computes the asked for percentile
* when called.
*/
public class PercentileTracker {
private long windowMillis;
protected ConcurrentSkipListMap<Long, Double> cslm = new ConcurrentSkipListMap<Long, Double>();
public PercentileTracker(long windowMillis) {
this.windowMillis = windowMillis;
}
/** Updates the average with the latest measurement.
* @param sample the latest measurement in the rolling average */
public synchronized void update(double sample) {
long now = System.currentTimeMillis();
removeOutOfTimeWindowEntries();
cslm.put(now, sample);
}
/**
* Returns a computed percentile value.
*
* @param requestedPercentile Which whole number percentile value needs to be calculated and returned
* @return double the percentile
*/
public double getPercentile(int requestedPercentile) {
return getPercentile(requestedPercentile, new ArrayList<Double>(cslm.values()));
}
protected double getPercentile(int requestedPercentile, ArrayList<Double> values) {
if (values == null || values.size() == 0) {
return 0d;
}
Collections.sort(values);
Double[] mySampleSet = values.toArray(new Double[values.size()]);
//This is the Excel Percentile Rank
double rank = (((double) requestedPercentile / 100d) * (values.size() - 1)) + 1;
//This is the Weighted Percentile Rank
//double rank = ((double) requestedPercentile / 100d) * (values.size() + 1);
double returnPercentile;
int integerRank = (int) rank;
double remainder = rank - integerRank;
if (remainder > 0) {
//Interpolate the percentile
double valueAtRankIr = mySampleSet[integerRank - 1];
double valueAtRankIrPlusOne = mySampleSet[integerRank];
returnPercentile = remainder * (valueAtRankIrPlusOne - valueAtRankIr) + valueAtRankIr;
}
else {
// Use the rank to find the 'exact' percentile.
returnPercentile = mySampleSet[integerRank - 1];
}
return returnPercentile;
}
private void removeOutOfTimeWindowEntries() {
if (cslm.isEmpty()) {
return;
}
// Optimization - if the last entry is also outside
// the time window, all items in the list can be cleared.
if (System.currentTimeMillis() - cslm.lastKey() > windowMillis) {
cslm.clear();
return;
}
while ((cslm.lastKey() - cslm.firstKey()) > windowMillis) {
cslm.pollFirstEntry(); //The first entry is now too old, remove it
}
}
}