/*
* -----------------------------------------------------------------------\
* 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.common;
import java.util.Deque;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.function.Consumer;
/**
* <p>A structure to keep record of elements for a certain period of time. It supports concurrent access and
* has amortized time complexity to add and remove element of O(1) (each removal is prepaid while the element is added).</p>
*
* <p>The window keeps time order of the elements and although it offers the possibility of providing artificial time, it still
* needs that the objects are added in a correct time order (older objects first).</p>
*
* <p>The time window is closed on both ends. For example, a sliding window all length 500 at the current time 600 will have both objects
* for time 100 and 600.</p>
*
* @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a>
*/
public class TimeSlidingWindow<E> {
/**
* The internal data structure maintaining the records of objects together with the time when they were added.
* The data are kept ordered because we assume mainly linear behavior.
*/
private Deque<TemporalObject<E>> window = new ConcurrentLinkedDeque<>();
/**
* The length of the window in milliseconds.
*/
private final long length;
/**
* Creates a new time sliding window of the given length in milliseconds.
*
* @param windowLength
* The length of the sliding window in milliseconds.
*/
public TimeSlidingWindow(final long windowLength) {
length = windowLength;
}
/**
* Gets the length of the sliding window in milliseconds.
*
* @return The length of the sliding window in milliseconds.
*/
public long getLength() {
return length;
}
/**
* Adds an object to the sliding window. It stays in the window for the time specified by the sliding window length.
*
* @param object
* Object to be added.
*/
public void add(final E object) {
add(object, System.currentTimeMillis());
}
/**
* Adds an object to the sliding window with an information about artificial time. The object cannot be older than the one added previously
* (the structure needs to keep time order of elements).
*
* @param object
* The object to be added.
* @param time
* Artificial time when the object was added to the window.
*/
public void add(final E object, final long time) {
if (window.size() == 0 || window.getLast().time <= time) {
window.add(new TemporalObject<>(object, time));
} else {
// very expensive inserting in the middle, values inserted in the meanwhile may be lost
final Deque<TemporalObject<E>> newWindow = new ConcurrentLinkedDeque<>();
final Iterator<TemporalObject<E>> iterator = window.descendingIterator();
TemporalObject<E> t = null;
while (iterator.hasNext() && (t = iterator.next()).time >= time) {
newWindow.push(t);
}
newWindow.push(new TemporalObject<>(object, time));
if (t != null && t.time < time) { // if there was just one entry, it might have been still bigger and we did not copy it
newWindow.push(t);
}
iterator.forEachRemaining(newWindow::push);
window = newWindow;
}
}
/**
* Removes all elements from the sliding window.
*/
public void clear() {
window.clear();
}
public long size() {
gc();
return window.size();
}
/**
* <p>Performs the given action for each element of the sliding window
* until all elements have been processed or the action throws an
* exception.</p>
*
* <p>Only the elements in the valid time window are processed. All older elements are garbage collected.</p>
*
* @param action
* The action to be performed for each element.
*/
public void forEach(final Consumer<E> action) {
gc();
if (window.size() > 0) {
window.iterator().forEachRemaining(te -> action.accept(te.object));
}
}
/**
* <p>Performs the given action for each element of the sliding window until all elements have been processed or the action throws an
* exception.</p>
*
* <p>Only the elements in the valid time window are processed. All older elements are garbage collected.</p>
*
* <p>This action might not return all elements when 1) it was already called with a higher (later) current time, 2) {@link #forEach(Consumer)}
* was already called which actually propagated a higher (later) time to this method.</p>
*
* @param currentTime
* Artificial time to control the position of the sliding window.
* @param action
* The action to be performed for each element.
*/
public void forEach(final long currentTime, final Consumer<E> action) {
gc(currentTime);
for (final TemporalObject<E> te : window) {
if (te.time <= currentTime) {
action.accept(te.object);
} else {
break;
}
}
}
/**
* Performs a garbage collection of elements that are out of the time window.
* It is not necessary to call this method manually, however, it might be useful to cut
* the memory costs when {@link #forEach} was not called for a long time.
*/
public void gc() {
gc(System.currentTimeMillis());
}
/**
* Performs a garbage collection based on the provided time information.
* This might be useful when controlling the time artificially.
*
* @param time
* Current artificial time.
*/
public void gc(final long time) {
window.removeIf(it -> it.time < time - length);
}
/**
* Carries object with a temporal information.
*
* @param <E>
* The type of object to carry.
*/
private static class TemporalObject<E> implements Comparable<TemporalObject<E>> {
/**
* The object carried.
*/
private final E object;
/**
* The temporal information related to the object.
*/
private final long time;
/**
* Binds an object with the current time.
*
* @param object
* The object to be carried.
*/
private TemporalObject(final E object) {
this.object = object;
this.time = System.currentTimeMillis();
}
/**
* Binds an object with the provided artificial time.
*
* @param object
* The object to be carried.
* @param time
* Artificial time.
*/
private TemporalObject(final E object, final long time) {
this.object = object;
this.time = time;
}
/**
* Gets the carried object.
*
* @return The object stored.
*/
public E getObject() {
return object;
}
/**
* Gets the temporal information bound to the carried object.
*
* @return The temporal information bound to the carried object.
*/
public long getTime() {
return time;
}
@Override
public int compareTo(final TemporalObject<E> o) {
long result = o.time - this.time;
return result == 0 ? 0 : (result < 0 ? -1 : 1);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final TemporalObject<?> that = (TemporalObject<?>) o;
if (time != that.time) {
return false;
}
return object != null ? object.equals(that.object) : that.object == null;
}
@Override
public int hashCode() {
int result = object != null ? object.hashCode() : 0;
result = 31 * result + (int) (time ^ (time >>> 32));
return result;
}
}
}