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 &lt;= 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}