001package io.prometheus.metrics.core.metrics; 002 003import java.lang.reflect.Array; 004import java.util.concurrent.TimeUnit; 005import java.util.function.LongSupplier; 006import java.util.function.ObjDoubleConsumer; 007import java.util.function.Supplier; 008 009/** 010 * Maintains a ring buffer of T to implement a sliding time window. 011 * <p> 012 * This is used to maintain a sliding window of {@link CKMSQuantiles} for {@link Summary} metrics. 013 * <p> 014 * It is implemented in a generic way so that 3rd party libraries can use it for implementing sliding windows. 015 * <p> 016 * TODO: The current implementation is {@code synchronized}. There is likely room for optimization. 017 */ 018public class SlidingWindow<T> { 019 020 private final Supplier<T> constructor; 021 private final ObjDoubleConsumer<T> observeFunction; 022 private final T[] ringBuffer; 023 private int currentBucket; 024 private long lastRotateTimestampMillis; 025 private final long durationBetweenRotatesMillis; 026 LongSupplier currentTimeMillis = System::currentTimeMillis; // to be replaced in unit tests 027 028 /** 029 * Example: If the {@code maxAgeSeconds} is 60 and {@code ageBuckets} is 3, then 3 instances of {@code T} 030 * are maintained and the sliding window moves to the next instance of T every 20 seconds. 031 * 032 * @param clazz type of T 033 * @param constructor for creating a new instance of T as the old one gets evicted 034 * @param observeFunction for observing a value (e.g. calling {@code t.observe(value)} 035 * @param maxAgeSeconds after this amount of time an instance of T gets evicted. 036 * @param ageBuckets number of age buckets. 037 */ 038 public SlidingWindow(Class<T> clazz, Supplier<T> constructor, ObjDoubleConsumer<T> observeFunction, long maxAgeSeconds, int ageBuckets) { 039 this.constructor = constructor; 040 this.observeFunction = observeFunction; 041 this.ringBuffer = (T[]) Array.newInstance(clazz, ageBuckets); 042 for (int i = 0; i < ringBuffer.length; i++) { 043 this.ringBuffer[i] = constructor.get(); 044 } 045 this.currentBucket = 0; 046 this.lastRotateTimestampMillis = System.currentTimeMillis(); 047 this.durationBetweenRotatesMillis = TimeUnit.SECONDS.toMillis(maxAgeSeconds) / ageBuckets; 048 } 049 050 /** 051 * Get the currently active instance of {@code T}. 052 */ 053 public synchronized T current() { 054 return rotate(); 055 } 056 057 /** 058 * Observe a value. 059 */ 060 public synchronized void observe(double value) { 061 rotate(); 062 for (T t : ringBuffer) { 063 observeFunction.accept(t, value); 064 } 065 } 066 067 private T rotate() { 068 long timeSinceLastRotateMillis = currentTimeMillis.getAsLong() - lastRotateTimestampMillis; 069 while (timeSinceLastRotateMillis > durationBetweenRotatesMillis) { 070 ringBuffer[currentBucket] = constructor.get(); 071 if (++currentBucket >= ringBuffer.length) { 072 currentBucket = 0; 073 } 074 timeSinceLastRotateMillis -= durationBetweenRotatesMillis; 075 lastRotateTimestampMillis += durationBetweenRotatesMillis; 076 } 077 return ringBuffer[currentBucket]; 078 } 079}