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