001package org.javasimon.callback.quantiles; 002 003import org.javasimon.Simon; 004import org.javasimon.Split; 005import org.javasimon.Stopwatch; 006import org.javasimon.clock.SimonClock; 007 008import java.util.ArrayList; 009import java.util.List; 010 011/** 012 * Callback which stores data in buckets to compute quantiles. 013 * Quantiles can only be obtained after warmup period, after which buckets are 014 * initialized. 015 * For each Simon the following lifecycle occurs: 016 * <ol> 017 * <li><em>Warm up</em>: 018 * <ul> 019 * <li>Buckets do not exist</li> 020 * <li>Quantiles can not be computed</li> 021 * <li>Splits are kept</li> 022 * </ul></li> 023 * <li><em>Trigger</em>: splits count <= warmup count threshold 024 * <ul> 025 * <li>Buckets are created, configuration (min, max) is determined from kept splits, bucket number is constant</li> 026 * <li>Buckets are filled with previously stored splits</li> 027 * <li>Retained splits are removed</li> 028 * <li>From now on, quantiles can not be computed and splits are not kept anymore</li> 029 * </ul></li> 030 * <li><em>Normal</em>: 031 * <ul> 032 * <li>Buckets are filled/updated with new splits as they come</li> 033 * <li>Quantiles can be computed (provided there is enough splits and buckets are properly configured)</li> 034 * </ul></li> 035 * </ol> 036 * 037 * @author gquintana 038 * @see Buckets 039 * @since 3.2 040 */ 041@SuppressWarnings("UnusedDeclaration") 042public class AutoQuantilesCallback extends QuantilesCallback { 043 044 /** Simon attribute name of the list of split values stored in Simons before warmup time. */ 045 public static final String ATTR_NAME_BUCKETS_VALUES = "bucketsValues"; 046 047 /** 048 * Number of splits before buckets are initialized. 049 * Default 10 050 */ 051 private final long warmupCounter; 052 053 /** Number of buckets of data for each Simon. */ 054 private final int bucketNb; 055 056 /** Default constructor. */ 057 public AutoQuantilesCallback() { 058 this.warmupCounter = 10; 059 this.bucketNb = 8; 060 } 061 062 /** Constructor with warmup counter and number of linear buckets for each Simon. */ 063 public AutoQuantilesCallback(long warmupCounter, int bucketNb) { 064 this.warmupCounter = warmupCounter; 065 this.bucketNb = bucketNb; 066 } 067 068 /** 069 * Constructor with all configuration. 070 * 071 * @param bucketsType Linear or exponential 072 * @param warmupCounter Number of splits before init 073 * @param bucketNb Bucket number 074 */ 075 public AutoQuantilesCallback(BucketsType bucketsType, long warmupCounter, int bucketNb) { 076 super(bucketsType); 077 this.warmupCounter = warmupCounter; 078 this.bucketNb = bucketNb; 079 } 080 081 /** Get the bucket values attribute or create it if it does not exist. */ 082 @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") 083 private List<Long> getOrCreateBucketsValues(final Stopwatch stopwatch) { 084 synchronized (stopwatch) { 085 List<Long> values = getBucketsValues(stopwatch); 086 if (values == null) { 087 values = new ArrayList<>((int) warmupCounter); 088 stopwatch.setAttribute(ATTR_NAME_BUCKETS_VALUES, values); 089 } 090 return values; 091 } 092 } 093 094 /** Get the bucket values attribute. */ 095 @SuppressWarnings("unchecked") 096 private List<Long> getBucketsValues(final Stopwatch stopwatch) { 097 return (List<Long>) stopwatch.getAttribute(ATTR_NAME_BUCKETS_VALUES); 098 } 099 100 /** Remove the bucket values attribute (after warmup). */ 101 private void removeBucketsValues(final Stopwatch stopwatch) { 102 stopwatch.removeAttribute(ATTR_NAME_BUCKETS_VALUES); 103 } 104 105 /** 106 * Create the buckets after warmup time. 107 * Can be overridden to customize buckets configuration. 108 * By default buckets are create with: 109 * <ul> 110 * <li>Min: stopwatch min-10% rounded to inferior millisecond</li> 111 * <li>Max: stopwatch max+10 rounded to superior millisecond</li> 112 * <li>Nb buckets: {@link #bucketNb}</li> 113 * </ul> 114 * 115 * @param stopwatch Stopwatch (containing configuration) 116 * @return new Buckets objects 117 */ 118 protected Buckets createBucketsAfterWarmup(Stopwatch stopwatch) { 119 // Compute min 120 long min = stopwatch.getMin() * 90L / 100L; // min -10% 121 min = Math.max(0, min); // no negative mins 122 min = (min / SimonClock.NANOS_IN_MILLIS) * SimonClock.NANOS_IN_MILLIS; // round to lower millisecond 123 // Compute max 124 long max = (stopwatch.getMax() * 110L) / 100L; // max +10% 125 max = (max / SimonClock.NANOS_IN_MILLIS + 1) * SimonClock.NANOS_IN_MILLIS; // round to upper millisecond 126 return createBuckets(stopwatch, min, max, bucketNb); 127 } 128 129 /** When warmup ends, buckets are create and retained splits are sorted in the buckets. */ 130 protected final Buckets createBuckets(Stopwatch stopwatch) { 131 if (stopwatch.getCounter() > warmupCounter) { 132 Buckets buckets = createBucketsAfterWarmup(stopwatch); 133 // Add retained splits to buckets 134 buckets.addValues(getBucketsValues(stopwatch)); 135 removeBucketsValues(stopwatch); 136 return buckets; 137 } else { 138 return null; 139 } 140 } 141 142 /** When simon is created, the list containing Split values is added to stopwatch attributes. */ 143 @Override 144 public void onSimonCreated(Simon simon) { 145 if (simon instanceof Stopwatch) { 146 Stopwatch stopwatch = (Stopwatch) simon; 147 getOrCreateBucketsValues(stopwatch); 148 } 149 } 150 151 /** 152 * Called when there is a new split on a Stopwatch, either 153 * {@link #onStopwatchStop} or {@link #onStopwatchAdd}. 154 * If buckets have been initialized, the value is added to appropriate bucket. 155 * Else if stopwatch is warming up value is added to value list. 156 */ 157 @Override 158 protected void onStopwatchSplit(Stopwatch stopwatch, Split split) { 159 Buckets buckets = getOrCreateBuckets(stopwatch); 160 long value = split.runningFor(); 161 if (buckets == null) { 162 // Warming up 163 getOrCreateBucketsValues(stopwatch).add(value); 164 } else { 165 // Warm 166 buckets.addValue(value); 167 buckets.log(split); 168 } 169 } 170}