All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sviolet.thistle.model.statistic.SlidingWindowArray Maven / Gradle / Ivy

There is a newer version: 22.1.0
Show newest version
/*
 * Copyright (C) 2015-2019 S.Violet
 *
 * 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.
 *
 * Project GitHub: https://github.com/shepherdviolet/thistle
 * Email: [email protected]
 */

package sviolet.thistle.model.statistic;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 

An array implements sliding window algorithm, For statistical

* * @author S.Violet */ public class SlidingWindowArray { public static final int DEFAULT_TIME_REVERSE_THRESHOLD = 64; private final int bucketSize; private final long durationPerBucket; private final float floatDurationPerBucket; private final int timeReverseThreshold; private final AtomicReferenceArray> buckets; private final ElementOperator operator; private final Lock lock; private final AtomicInteger timeReverseCount = new AtomicInteger(0); /** * @param bucketSize bucket size, > 0 * @param durationPerBucket duration per bucket, milliseconds, >= 10 ms * @param operator create element when bucket creating, reset element when the bucket is expired */ public SlidingWindowArray(int bucketSize, long durationPerBucket, ElementOperator operator) { this(bucketSize, durationPerBucket, DEFAULT_TIME_REVERSE_THRESHOLD, operator); } /** * @param bucketSize bucket size, > 0 * @param durationPerBucket duration per bucket, milliseconds, >= 10 ms * @param timeReverseThreshold reset all statistical data if time reverse exceeds the specified number of times, Set Integer.MAX_VALUE to disable reset * @param operator create element when bucket creating, reset element when the bucket is expired */ public SlidingWindowArray(int bucketSize, long durationPerBucket, int timeReverseThreshold, ElementOperator operator) { if (bucketSize <= 0) { throw new IllegalArgumentException("bucketSize must > 0"); } if (durationPerBucket < 10) { throw new IllegalArgumentException("durationPerBucket must >= 10 ms"); } if (operator == null) { throw new IllegalArgumentException("ElementOperator is null"); } this.bucketSize = bucketSize; this.durationPerBucket = durationPerBucket; this.floatDurationPerBucket = durationPerBucket; this.timeReverseThreshold = timeReverseThreshold; this.buckets = new AtomicReferenceArray<>(bucketSize); this.operator = operator; this.lock = buildLock(); } /** * Get the element to record current statistics * @param currentTime current timestamp * @return the statistical element of current time */ public E getElement(long currentTime){ return getByGeneration(generation(currentTime), true).element; } /** *

Get the elements of recent period for data calculation.

* *

Rough Mode: The smaller the ratio of statisticDuration and durationPerBucket (statisticDuration / durationPerBucket), * the greater the deviation. Therefore, we recommend using a larger ratio (statisticDuration / durationPerBucket > 10). * If statisticDuration / durationPerBucket = 1, it will return data of two buckets, the deviation can be up to 100%.

* *

For Example, if you want to get statistics for the last 10 seconds: * slidingWindowArray.getElementsRoughly(System.currentTimeMillis(), 10000L);

* * @param currentTime current timestamp * @param statisticDuration statistic duration, milliseconds, statisticDuration <= bucketSize * durationPerBucket * @return elements of recent period */ public List getElementsRoughly(long currentTime, long statisticDuration) { long statisticStartTime = currentTime - Math.max(statisticDuration, 0); int endGeneration = (int) (currentTime / durationPerBucket); int startGeneration = Math.max((int) (statisticStartTime / durationPerBucket), endGeneration - bucketSize + 1); List result = new ArrayList<>(endGeneration - startGeneration + 1); for (int generation = startGeneration ; generation <= endGeneration ; generation++) { //put all result.add(getByGeneration(generation, false).element); } return result; } /** *

Get the elements of recent period for data calculation.

* *

Accurate Mode: Each element will have a weight value, you can multiply the statistical value by the weight, * to get more accurate result.

* *

For Example, if you want to get statistics for the last 10 seconds: * slidingWindowArray.getElementsRoughly(System.currentTimeMillis(), 10000L);

* * @param currentTime current timestamp * @param statisticDuration statistic duration, milliseconds, statisticDuration <= bucketSize * durationPerBucket * @return elements of recent period */ public List> getElementsAccurately(long currentTime, long statisticDuration) { long statisticStartTime = currentTime - Math.max(statisticDuration, 0); int endGeneration = (int) (currentTime / durationPerBucket); int startGeneration = Math.max((int) (statisticStartTime / durationPerBucket), endGeneration - bucketSize + 1); List> result = new ArrayList<>(endGeneration - startGeneration + 1); for (int generation = startGeneration ; generation <= endGeneration ; generation++) { Bucket bucket = getByGeneration(generation, false); //bucket info long bucketStartTime = bucket.startTime; long bucketEndTime = bucketStartTime + durationPerBucket; //weight calculation long validDuration = durationPerBucket; if (bucketStartTime < statisticStartTime) { validDuration -= statisticStartTime - bucketStartTime; } if (bucketEndTime > currentTime) { validDuration -= bucketEndTime - currentTime; } //element with infos result.add(new Element<>( bucketStartTime, bucketEndTime, validDuration == durationPerBucket ? 1.0f : Math.max((float)validDuration / floatDurationPerBucket, 0.0f), bucket.element)); } return result; } /** * bucket size * @return bucket size */ public int size(){ return bucketSize; } /** * ReentrantLock by default */ protected Lock buildLock(){ return new ReentrantLock(); } /** * @param generation currentTime / durationPerBucket */ private Bucket getByGeneration(long generation, boolean resetEnabled){ //index of bucket int index = (int) (generation % bucketSize); if (index < 0) { index += bucketSize; } //bucket start time long startTime = generation * durationPerBucket; //get bucket Bucket bucket = buckets.get(index); //create bucket if (bucket == null) { lock.lock(); try { bucket = buckets.get(index); if (bucket == null) { //new bucket bucket = new Bucket<>(startTime, operator.reset(null)); buckets.set(index, bucket); } } finally { lock.unlock(); } } //get element while (true) { //return new element if time reverse, Please avoid this situation ! if (startTime < bucket.startTime) { //reset disabled if (!resetEnabled) { //return dummy return new Bucket<>(startTime, operator.reset(null)); } //reset enabled if (timeReverseCount.incrementAndGet() <= timeReverseThreshold) { //return dummy return new Bucket<>(startTime, operator.reset(null)); } else { //reset all if time reverse exceeded threshold lock.lock(); try { for (int i = 0 ; i < bucketSize ; i++) { Bucket b = buckets.get(i); b.element = operator.reset(b.element); b.startTime = Integer.MIN_VALUE; } } finally { lock.unlock(); } } } //return current bucket if (startTime == bucket.startTime) { return bucket; } //clear time reverse count if (resetEnabled) { timeReverseCount.set(0); } //reset bucket and re-judge lock.lock(); try { if (startTime > bucket.startTime) { bucket.element = operator.reset(bucket.element); bucket.startTime = startTime; } } finally { lock.unlock(); } } } private long generation(long currentTime) { return currentTime / durationPerBucket; } private static class Bucket { private volatile long startTime; private volatile E element; private Bucket(long startTime, E element) { this.startTime = startTime; this.element = element; } } /** * Statistical data / start time / end time * @param Element type */ public static class Element { private final long startTime; private final long endTime; private final float weight; private E value; public Element(long startTime, long endTime, float weight, E value) { this.startTime = startTime; this.endTime = endTime; this.weight = weight; this.value = value; } /** * Start time of the bucket */ public long getStartTime() { return startTime; } /** * End time of the bucket */ public long getEndTime() { return endTime; } /** * You can multiply the statistical value by the weight to get more accurate result */ public float getWeight() { return weight; } /** * Statistical data */ public E getValue() { return value; } public void setValue(E value) { this.value = value; } @Override public String toString() { return "Element{" + "startTime=" + startTime + ", endTime=" + endTime + ", weight=" + weight + ", value=" + value + '}'; } } /** * Create element when bucket creating, reset element when the bucket is expired * @param */ public interface ElementOperator { /** * Create element when bucket creating, reset element when the bucket is expired * * @param element The old element, null if creating bucket * @return return new element, Or you can reuse the old one */ E reset(E element); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy