/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014 ForgeRock AS.
*/
package org.forgerock.openam.shared.monitoring;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* This class maintains the rate of an event for a sample rate over a window of a particular size.
*
* The window is constructed with a given size and sample rate and the window will move continuously at interval
* defined by the sample rate. The min, max and average will always be of the current window position, ie as the window
* moves and sample are not covered by the window the sample will not be included in the min, max and average
* calculations.
*
* @since 12.0.0
*/
public class RateWindow {
private final RateTimer timer;
private final int size;
private final long sampleRate;
private final ConcurrentSkipListMap window = new ConcurrentSkipListMap();
private final Comparator atomicLongComparator = new Comparator() {
@Override
public int compare(AtomicLong rate, AtomicLong rate2) {
long x = rate.get();
long y = rate2.get();
return x > y ? 1 : x < y ? -1 : 0;
}
};
/**
* Constructs a new instance of the RateWindow.
*
* @param timer An instance of a Timer.
* @param size The size of the window.
* @param sampleRate The sample rate for the window.
*/
public RateWindow(final RateTimer timer, final int size, final long sampleRate) {
this.timer = timer;
this.size = size;
this.sampleRate = sampleRate;
}
/**
* Re-calculates the rate.
*
* @param timestamp The millisecond timestamp of the event.
*/
public void incrementForTimestamp(final long timestamp) {
long index = getIndexForTimestamp(timestamp);
if (isWithinWindow(index)) {
AtomicLong rate = window.get(index);
if (rate == null) {
//fill in the RateWindow until the current index
fillInWindow(index - 1);
rate = new AtomicLong(0);
AtomicLong previousValue = window.putIfAbsent(index, rate);
if (previousValue == null) {
//this is a new entry, hence we should clear out old entries to prevent memory leak
window.headMap(window.lastKey() - size, true).clear();
} else {
rate = previousValue;
}
}
rate.incrementAndGet();
}
}
/**
* Fills in the windows with 0 values until the index provided. This ensures that there are no empty spots between
* the indexes, so the information stored in the window actually represents a rolling window of data.
*
* @param index The index until which the window should be filled. The entry corresponding to the provided index
* will be also initialized
*/
private void fillInWindow(long index) {
if (!window.isEmpty()) {
Long lastKey = window.lastKey();
for (lastKey = lastKey + 1; lastKey <= index; lastKey++) {
window.putIfAbsent(lastKey, new AtomicLong(0));
}
window.headMap(window.lastKey() - size, true).clear();
}
}
/**
* Gets the average rate for the sample rate averaged across the whole window.
*
* Does not include the latest window slot if time has not passed beyond it yet as otherwise could skew the average
* as that time slot has not yet completed and may get more events made in it.
*
* @return The average event rate.
*/
public synchronized double getAverageRate() {
if (window.isEmpty()) {
return 0D;
}
fillInWindow(getCurrentIndex());
double averageRate = 0;
for (Map.Entry entry : window.entrySet()) {
if (entry.getKey().equals(getCurrentIndex())) {
/*
* If this is true then the latest window slot has not completed so the rate in it will not be
* accurate so skip it.
*/
continue;
}
averageRate += entry.getValue().get();
}
return averageRate / window.size();
}
/**
* Gets the minimum rate.
*
* @return The minimum event rate.
*/
public long getMinRate() {
if (window.isEmpty()) {
return 0L;
}
fillInWindow(getCurrentIndex());
return Collections.min(window.values(), atomicLongComparator).get();
}
/**
* Gets the maximum rate.
*
* @return The maximum event rate.
*/
public long getMaxRate() {
if (window.isEmpty()) {
return 0L;
}
fillInWindow(getCurrentIndex());
return Collections.max(window.values(), atomicLongComparator).get();
}
/**
* Converts the millisecond timestamp into a normalised sample rate.
*
* @param timestamp The millisecond timestamp.
* @return The sample rate.
*/
private long getIndexForTimestamp(final long timestamp) {
return timestamp / sampleRate;
}
private long getCurrentIndex() {
return getIndexForTimestamp(timer.now());
}
private boolean isWithinWindow(final long index) {
return getCurrentIndex() - size < index;
}
}