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}